Conventions

How every endpoint retries, pages and fails.

Idempotency

Retry any create safely. The same key returns the first answer and never acts twice.

How it works

Send a unique Idempotency-Key header, a UUID for example. The first request with that key runs, and Omnio keeps its response for 24 hours. The same key with the same request returns that response with Idempotent-Replayed: true.

POST /v1/messages
curl -X POST https://api.omnio.sawego.com/v1/messages \
  -H "Authorization: Bearer $OMNIO_API_KEY"
  -H "Idempotency-Key: $(uuidgen)"
  -H "Content-Type: application/json"
  -d '{
    "conversation_id": "b1c7e0a4-52a3-4a3c-8d0e-6c1b9e5f7a20",
    "type": "text",
    "content": {
      "text": "Your order 4821 ships Thursday."
    }
  }'

Which endpoints take it

Every create takes it and none requires it. Without a key, each call runs, so a retried send can go out twice. Each page in the API reference shows whether it applies.

Retrying

  • Keep the key with the job that sends and reuse it on every retry of that job.
  • A request that failed frees its key, so a retry after a 4xx or 5xx runs again.
  • Generate a new key only for a new intent.

Conflicts

StatusCodeCause
409idempotency_conflictThe key was used with a different request
409idempotency_in_progressThe first request with this key is still running

Pagination

Lists come in pages. Follow next_cursor until it is null.

Cursors

A list answers { "data": [...], "next_cursor": "..." }. Pass limit (1 to 200, default 50) and the previous page's cursor. Cursors are opaque, and rows added while you page never repeat or go missing.

Read every contact
let cursor = null
do {
  const url = new URL("https://api.omnio.sawego.com/v1/contacts")
  url.searchParams.set("limit", "200")
  if (cursor) url.searchParams.set("cursor", cursor)
  const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.OMNIO_API_KEY}` } })
  const page = await res.json()
  for (const contact of page.data) console.log(contact.customer_id)
  cursor = page.next_cursor
} while (cursor)

Exceptions

Sorted contacts
A cursor works only with the sort and order it came from. Anything else is 400 invalid_cursor.
Messages
List messages pages by seq. before_seq scrolls back, after_seq catches up, and a page shorter than limit is the last in that direction.
Search
Search returns contacts and messages as two groups, each with its own contacts_cursor or messages_cursor.

Errors

Every error has the same shape and a stable code to branch on.

Error shape

422 response
{
  "error": {
    "code": "validation_error",
    "message": "content.text is required",
    "details": { "fields": [{ "field": "content.text", "reason": "is required" }] }
  }
}
code
Stable. Branch on it.
message
For people. It may change, so never parse it.
details
What you can act on, such as details.fields. Present only when there is something to say.
provider_code
Meta's own number, when Meta refused the call.

Validation errors

A 422 validation_error lists every field at fault in details.fields, so you can fix them all in one try.

422 response
{
  "error": {
    "code": "validation_error",
    "message": "attributes.plan is not a custom field of this organisation; language must be two lowercase letters, such as fr",
    "details": {
      "fields": [
        { "field": "attributes.plan", "reason": "is not a custom field of this organisation" },
        { "field": "language", "reason": "must be two lowercase letters, such as fr" }
      ]
    }
  }
}
  • field is the path in your body, with a dot per level. An empty or non-JSON body is reported as body.
  • Omnio never echoes the value you sent.
  • Some older endpoints list plain field names in details.fields. Accept both forms.

Error codes

StatusCodes
400invalid_cursor
401unauthorized
403insufficient_scope forbidden
404not_found
409idempotency_conflict idempotency_in_progress channel_disconnected customer_id_taken contact_deleted tag_exists
422validation_error window_closed contact_blocked media_invalid template_invalid customer_id_invalid
503unavailable

Failed sends

When WhatsApp refuses a message after the 202, there is no HTTP error. The message turns rejected or failed and its error holds the code. Omnio retries rate limits and Meta outages only, up to five tries.

Meta codeerror.codeRetried
131047window_closedNo. Send a template
131026undeliverableNo
131056recipient_rate_limitedYes
130429throughput_exceededYes
131048spam_rate_limitedNo
132000, 132001template_invalidNo
131053media_errorNo
190channel_disconnectedNo. Sends wait until the channel reconnects

Any other Meta code reads meta_ followed by the number.

Rate limits

Omnio does not rate limit API keys today. WhatsApp paces each number, so Omnio queues sends and spaces them out for you.