Errors
One error shape, eleven 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 eleven are the whole vocabulary. Nothing else is ever returned — and note there is no 403: a call your key is not allowed to make 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. |
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. |
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 that
is not allowed to make this particular call. Distinguishing them would let someone probe
which key ids are real without holding a secret.
So if your credentials are definitely correct, check that the key is still active and that
you are calling from an allowlisted address. Quote the requestId and support can say
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 · 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 |
Retrying an operation that spends money can spend it twice. A purchase, a top-up, an OTP rental and a lookup submission all take effect on every call, and there is no key or header that makes a retry safe.
If one of those requests times out or fails in a way that leaves the outcome unknown, do
not resend it blind. Read the resource back first — list your orders, or poll the order
or job the first attempt would have created — and only retry once you know nothing was
created. x-request-id from the failed attempt lets us confirm what happened if you
cannot tell.
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".