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.
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
| Status | Code | Cause |
|---|---|---|
| 409 | idempotency_conflict | The key was used with a different request |
| 409 | idempotency_in_progress | The 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.
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
sortandorderit came from. Anything else is400invalid_cursor. - Messages
- List messages pages by
seq.before_seqscrolls back,after_seqcatches up, and a page shorter thanlimitis the last in that direction. - Search
- Search returns contacts and messages as two groups, each with its own
contacts_cursorormessages_cursor.
Errors
Every error has the same shape and a stable code to branch on.
Error shape
{
"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.
{
"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" }
]
}
}
}
fieldis the path in your body, with a dot per level. An empty or non-JSON body is reported asbody.- Omnio never echoes the value you sent.
- Some older endpoints list plain field names in
details.fields. Accept both forms.
Error codes
| Status | Codes |
|---|---|
| 400 | invalid_cursor |
| 401 | unauthorized |
| 403 | insufficient_scope forbidden |
| 404 | not_found |
| 409 | idempotency_conflict idempotency_in_progress channel_disconnected customer_id_taken contact_deleted tag_exists |
| 422 | validation_error window_closed contact_blocked media_invalid template_invalid customer_id_invalid |
| 503 | unavailable |
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 code | error.code | Retried |
|---|---|---|
| 131047 | window_closed | No. Send a template |
| 131026 | undeliverable | No |
| 131056 | recipient_rate_limited | Yes |
| 130429 | throughput_exceeded | Yes |
| 131048 | spam_rate_limited | No |
| 132000, 132001 | template_invalid | No |
| 131053 | media_error | No |
| 190 | channel_disconnected | No. 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.