Errors and idempotency
The error envelope, status codes, retry-safety and rate limits.
Errors and idempotency
Error format
Every failure on a matched /api/v1 route returns JSON. No stack traces, class
names or file paths reach you — those are recorded server-side, whatever the
server's debug setting.
{ "message": "The connection was not found." }
Validation failures add a per-field breakdown:
{
"message": "The given data was invalid.",
"errors": {
"connection_id": ["The connection id field is required."]
}
}
| Status | Meaning | What to do |
|---|---|---|
401 |
Missing, invalid or revoked key | Check the Authorization header |
403 |
Key valid, scope or workspace not permitted | Check the key's scopes |
409 |
Idempotency conflict | See below |
422 |
Validation failed, or the request cannot be carried out | Read the message |
429 |
Rate limited | Back off and retry |
5xx |
Our fault | Retry with backoff; safe if you sent an idempotency key |
422 is broader than validation
It covers both "your fields are wrong" and "your fields are fine but this cannot
be done". The first form carries errors; the second is a bare message:
{ "message": "The selected connection is not ready to send." }
Others you will meet: The connection was not found. ·
The contact was not found. · A valid E.164 phone number is required. ·
The selected media object is not available for sending. ·
The contact has opted out of outbound messages. ·
The contact is currently suppressed from outbound messages.
A missing id is not always 404. Media has its own routes, so an unknown
media id returns 404. A connection, contact or media_id referenced inside
a send is validated by the service and returns 422 — the request was
well-formed, it just cannot be carried out.
Treat any 422 without an errors object as "well-formed but not possible",
and read the message rather than inferring the cause from the status alone.
Malformed JSON
A truncated or invalid JSON body is parsed as an empty request rather than
rejected outright, so you get a 422 about missing fields rather than a 400
about syntax. If you see required-field errors for fields you know you sent,
check that your body is valid JSON and your Content-Type is
application/json.
Idempotency
Send an Idempotency-Key header so a retry cannot repeat an action.
POST /api/v1/messages
Idempotency-Key: order-4821-confirmation
Applies to POST /contacts and POST /messages. It is optional — omit it
and each request is treated as new. POST /media ignores the header; uploading
the same file twice simply produces two media objects, which is harmless.
The key must be 8–255 characters of A–Z a–z 0–9 . _ : -. Anything else is
rejected with 422. Use something derived from the thing you are acting on — an
order id, a job id, a UUID you generate and store — so a retry after a timeout
naturally reuses it.
Keys are scoped to the API key, method and path. The same value on
POST /messages and POST /contacts never collides.
What happens on a repeat
| Case | Result |
|---|---|
| Same key, same body | Original status and body replayed, with Idempotency-Replayed: true |
| Same key, different body | 409 — the key is bound to a different request |
| Request still in flight | 409 The idempotent request is still in progress. |
| Previous attempt threw | The key is released and can be reused |
A replay is the case you are protecting against: your process died after we accepted the message but before you recorded it, and your retry must not produce a second WhatsApp message.
Check for Idempotency-Replayed: true if you need to tell a fresh action from a
replayed one.
How a repeat is recognised
We fingerprint the parsed request — the endpoint, the field values sorted by key, and a content digest of any uploaded file. Two requests match when they would do the same thing, regardless of JSON key order or how the body was encoded.
File uploads are covered by content, not filename: re-uploading identical bytes under a different filename is the same request.
It also makes failures clean
A request sent with an idempotency key runs inside a database transaction, so a failure rolls back everything it touched. The same request without one can leave a partially-applied side effect behind — a contact created for a message that was then refused, for instance.
Send the header on every write you care about.