OdyOdy Help

Pagination, idempotency, and error handling

Updated Sun Aug 16 2026 00:00:00 GMT+0000 (Coordinated Universal Time)

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) and GET /v1/api/calls (default 50, max 200) take ?limit= only, without cursors.
  • The official SDKs follow nextCursor for you with listAll() / 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: true response 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.

More in Developer API