Pagination, idempotency, and error handling
The Ody API uses opaque cursor pagination on its list endpoints, an Idempotency-Key header for safe message-send retries, and one uniform error envelope with a request ID on every response — this guide covers all three.
Cursor pagination
GET /v1/api/contacts and GET /v1/api/conversations paginate with ?limit= and ?cursor=:
curl "https://api.ody.co/v1/api/contacts?limit=100" \
-H "Authorization: Bearer ody_live_…"
The response includes a nextCursor — pass it back as ?cursor= to fetch the next page, and stop when it's null:
{ "contacts": [ … ], "nextCursor": "eyJ…" }
- Cursors are opaque — always pass them back verbatim; a modified or stale cursor returns
400 invalid_argument. - Page-size limits: contacts default 50, max 200; conversations default 50, max 100.
GET /v1/api/messages/search(default 25, max 100) andGET /v1/api/calls(default 50, max 200) take?limit=only, without cursors. - The official SDKs follow
nextCursorfor you withlistAll()/list_all()— see SDK quickstart: TypeScript and Python.
Idempotent message sends
A network failure after you POST /v1/api/messages leaves you not knowing whether the text went out — retrying blindly risks double-sending. Send an Idempotency-Key header (any string up to 200 characters, unique per logical send) and retries become safe:
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!"}'
- The first successful response is stored for 24 hours; any retry with the same key in that window replays it verbatim instead of sending again, with an
Idempotency-Replayed: trueresponse header. - Derive the key from your own business event ("order-1042-shipped"), not a random value per attempt — the whole point is that retries share it.
The error envelope
Every error response uses the same shape:
{ "error": { "code": "permission_denied", "message": "This API key is missing the 'messages:send' scope", "requestId": "req_…" } }
| Status | Code | What to do |
|---|---|---|
400 |
invalid_argument |
Fix the request — the message says what's wrong |
401 |
unauthenticated |
The key is missing, invalid, or revoked. Don't retry; alert the integration owner |
403 |
permission_denied |
The message names the missing scope — create a key that has it |
404 |
not_found |
The resource doesn't exist in this workspace |
409 |
failed_precondition |
The workspace can't do this yet (e.g. no active number to send from). Surface it, don't retry |
429 |
resource_exhausted |
Rate limited — wait Retry-After seconds, then retry with backoff |
500 |
internal |
Retry with exponential backoff and jitter; cap the retries |
Request IDs
Every response — success or error — carries an X-Request-Id header, echoed as requestId in error bodies. Log it alongside your own request logs, and quote it when you contact support: it pinpoints the exact request on our side. You can also supply your own X-Request-Id (up to 64 characters, letters/digits/_/-) and Ody will echo it back, which makes correlating retries easy.
Related articles
Frequently asked questions
How do I get the next page of results?
Pass the previous response's nextCursor as ?cursor= on the next request. A null nextCursor means you've reached the end.
What happens if I retry a send with the same Idempotency-Key?
Within 24 hours, Ody replays the stored first response instead of sending again, and adds an Idempotency-Replayed: true header.
What should I send support when something fails?
The requestId from the error body (also the X-Request-Id response header) — it lets us find the exact request in our logs.