Errors
One error shape, thirteen codes, and what to do about each.
Every failure — from any product, at any layer — has the same shape:
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Your account balance is too low for this operation.",
"requestId": "9f1c7b2e-4a83-4d21-9c55-1f0b3a7e2d64"
}
}Branch on code, never on message. The code is contract; the message is prose we may
improve. requestId is the same value as the x-request-id response header — quote it when
you contact support and we can find your exact request.
fields appears on validation failures only, naming the offending fields without repeating
their values:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request is not valid. See `fields` for what to correct.",
"requestId": "…",
"fields": ["body.msisdns", "query.pageSize"]
}
}The codes
These thirteen are the whole vocabulary. Nothing else is ever returned — and note there is no 403: a permissions failure answers 401 like any other credential problem.
Your request needs changing
| Code | HTTP | What to do |
|---|---|---|
VALIDATION_ERROR | 400 | Fix the fields listed in fields. Retrying unchanged will fail identically. |
IDEMPOTENCY_KEY_REQUIRED | 400 | Add an Idempotency-Key header — see Idempotency. |
NOT_FOUND | 404 | The identifier does not exist, or is not yours. See below. |
CONFLICT | 409 | The resource is not in a state that allows this — an order already cancelled, a job already finished. |
IDEMPOTENCY_CONFLICT | 409 | You reused an idempotency key with a different request body. Use a new key. |
Your credentials
| Code | HTTP | What to do |
|---|---|---|
AUTHENTICATION_REQUIRED | 401 | You sent no credentials. Add x-key-id and x-api-secret. |
INVALID_API_KEY | 401 | The credentials you sent were not accepted. |
INVALID_API_KEY is deliberately one answer to several situations: a key that does not
exist, a wrong secret, a revoked key, an IP outside the key's allowlist, and a key
without the permissions this operation needs. Distinguishing them would let someone
probe which key ids are real without holding a secret.
So if your credentials are definitely correct, the likely cause is permissions — check
with whoever issued the key. Quote the requestId and we can tell you exactly which
check failed.
Something is unavailable
| Code | HTTP | What to do |
|---|---|---|
INSUFFICIENT_BALANCE | 409 | Top up. Retrying without doing so will fail identically. |
PRODUCT_UNAVAILABLE | 409 | This product cannot be sold right now. Try another, or retry later. |
RATE_LIMITED | 429 | Wait for the interval in the Retry-After header, then retry. |
SERVICE_UNAVAILABLE | 503 | A dependency is temporarily down. Retry with backoff. |
SERVICE_RESPONSE_INVALID | 502 | A downstream service answered in a way that does not match our contract. We rejected it rather than pass it on. Retry; if it persists, report it with the requestId. |
INTERNAL_ERROR | 500 | Our bug. Retry once, then report it with the requestId. |
Which errors are worth retrying
| Behaviour | Codes |
|---|---|
| Do not retry — the outcome will not change | VALIDATION_ERROR · IDEMPOTENCY_KEY_REQUIRED · IDEMPOTENCY_CONFLICT · NOT_FOUND · CONFLICT · AUTHENTICATION_REQUIRED · INVALID_API_KEY |
| Retry after acting | INSUFFICIENT_BALANCE (top up) · RATE_LIMITED (wait for Retry-After) |
| Retry with backoff | SERVICE_UNAVAILABLE · SERVICE_RESPONSE_INVALID · INTERNAL_ERROR · PRODUCT_UNAVAILABLE |
When retrying anything that spends money, send the same Idempotency-Key as the
original attempt. That is what makes a retry safe rather than a second charge — see
Idempotency.
A 404 never confirms someone else's data
If you ask for an identifier that belongs to another account, you get exactly the same
404 NOT_FOUND as for an identifier that has never existed. The two are indistinguishable
on purpose: an answer that distinguished them would confirm that a guessed order id is
real and belongs to somebody.
So a 404 means "not yours, or not a thing" — never "exists, but you may not see it".