API overview and authentication
The Ody REST API lives at https://api.ody.co/v1/api and authenticates every request with an API key sent as a Bearer token — create one in Settings → Developer API, then call the endpoints below.
Authentication
Send your key in the Authorization header on every request:
curl https://api.ody.co/v1/api/contacts \
-H "Authorization: Bearer ody_live_…"
- Keys come in two modes:
ody_live_…keys are live,ody_test_…keys behave identically but message sends are simulated (recorded withdeliveryStatus: "simulated", never transmitted). See Build safely with test mode. - A missing, invalid, or revoked key returns
401 unauthenticated. - A key narrowed by scopes returns
403 permission_denied— naming the missing scope — when it calls an endpoint outside its grants. - Every call runs with the permissions of the member who created the key, inside that workspace only.
Endpoints
| Method & path | What it does | Required scope |
|---|---|---|
GET /v1/api/contacts |
List or search contacts (?q=, paginated) |
contacts:read |
POST /v1/api/contacts |
Create a contact | contacts:write |
GET /v1/api/contacts/:id |
Fetch a contact's full detail | contacts:read |
PATCH /v1/api/contacts/:id |
Update a contact (only provided fields change) | contacts:write |
DELETE /v1/api/contacts/:id |
Delete a contact | contacts:write |
GET /v1/api/conversations |
List conversations (?status=open|done, paginated) |
conversations:read |
GET /v1/api/conversations/:id |
Fetch a full thread | conversations:read |
PATCH /v1/api/conversations/:id |
Set status — body {"status":"open"} or {"status":"done"} |
conversations:write |
POST /v1/api/messages |
Send an SMS/MMS (supports Idempotency-Key) |
messages:send |
GET /v1/api/messages/search |
Search message bodies and transcripts (?q=) |
conversations:read |
GET /v1/api/calls |
List call & voicemail history | conversations:read |
POST /v1/api/calls |
Place an outbound AI call — your published agent dials | calls:place |
GET /v1/api/numbers |
List your workspace's numbers | numbers:read |
GET /v1/api/numbers/available |
Search numbers you can buy (?country=, ?startsWith=, ?contains=, ?locality=, ?state=, ?limit=) |
numbers:manage |
POST /v1/api/numbers |
Buy a number — billed like an in-app purchase (supports Idempotency-Key) |
numbers:manage |
POST /v1/api/numbers/:e164/connect and …/disconnect |
Route a number to or away from your AI agent | agent:manage |
GET /v1/api/messaging |
10DLC registration status (step, canText) |
numbers:manage |
POST /v1/api/messaging/registration |
Submit your 10DLC business + campaign (billed) | numbers:manage |
POST /v1/api/messaging/refresh |
Re-check and advance 10DLC carrier review | numbers:manage |
GET/PATCH /v1/api/agent |
Read or update your AI agent's config (draft) | agent:manage |
POST /v1/api/agent/publish |
Push your agent config live | agent:manage |
GET /v1/api/webhook-events |
List subscribable webhook event types | webhooks:manage |
GET/POST /v1/api/webhooks and /v1/api/webhooks/:id… |
Manage outbound webhook endpoints | webhooks:manage |
GET /v1/api/openapi.json |
The OpenAPI 3.1 spec | none |
List endpoints paginate with ?limit= and ?cursor= — see Pagination, idempotency, and error handling.
Contacts
POST /v1/api/contacts needs at least one identity field: firstName, lastName, company, a phone, or an email. Phones and emails accept a simple string ("phone": "+15125550123") or full labeled arrays ("phones": [{"value": "+15125550123", "label": "work"}]). On PATCH, only the fields you provide change; phones, emails, notes, properties, and tags are replaced wholesale when supplied.
Sending a message
POST /v1/api/messages accepts either an existing conversationId or a recipient phone number in to (E.164 format). With to, Ody picks your workspace's sending number (or honors a from you own), creates the conversation if needed, and threads the message. Add up to 10 publicly fetchable mediaUrls for MMS:
curl https://api.ody.co/v1/api/messages \
-H "Authorization: Bearer ody_live_…" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-shipped" \
-d '{"to":"+15125550123","body":"Your order shipped!"}'
A workspace with no active phone number gets 409 failed_precondition. Retries with the same Idempotency-Key within 24 hours replay the first response instead of double-sending.
Numbers, texting, AI calls, and your agent
The API can also run a phone line end to end: search and buy numbers, register 10DLC texting, configure and publish your AI agent, route numbers to it, and place outbound AI calls. These endpoints use three scopes of their own — numbers:manage (Buy numbers & 10DLC), calls:place (Place AI calls), and agent:manage (Manage AI agent) — and everything they do lands in your account exactly as if you did it in the app, with the same billing and rules. See Buy numbers and register texting via the API and Place AI calls and manage Astra via the API.
Errors, request IDs, and rate limits
Every error uses a uniform envelope, and every response (success or error) carries an X-Request-Id header — quote it when you contact support:
{ "error": { "code": "not_found", "message": "Contact not found", "requestId": "req_…" } }
Every key gets 600 requests/minute with standard RateLimit-* headers. Details in API rate limits and best practices and Pagination, idempotency, and error handling.
Related articles
Frequently asked questions
What can the API do today?
Full contact CRUD, list and update conversations, send SMS/MMS, search messages, list call history, search and buy numbers, register 10DLC texting, place outbound AI calls, configure and publish your AI agent, and manage outbound webhooks.
Can an API key access other workspaces?
No. A key is bound to the workspace it was created in and acts as the member who created it — the same permission rules as the app apply.
Is there a machine-readable spec?
Yes — an OpenAPI 3.1 document at https://api.ody.co/v1/api/openapi.json, no authentication required.