Every failure returns the same JSON shape with a non-2xx status:
"code": "insufficient_balance",
"message": "Your balance does not cover this order.",
"request_id": "req_8f3c2b1a9d7e",
{ "field": "items.0.quantity", "message": "Must be 1 or greater" }
| Field |
Notes |
code |
Branch on this. Stable forever — a code’s meaning never changes; new codes may be added. |
message |
For humans and logs. Wording can change at any time, so never match on it. |
request_id |
Quote this to support and we can find the exact request. Log it. |
details |
Present on validation failures — which field, and why. |
| Code |
Status |
What happened |
What to do |
unauthorized |
401 |
No token, or it is not valid |
Send Authorization: Bearer <token> |
invalid_credentials |
401 |
Wrong client_id or client_secret |
Check the key; rotate if lost |
token_expired |
401 |
The token passed its hour |
Fetch a new token and retry once |
insufficient_scope |
403 |
The key lacks the scope for this endpoint |
Grant the scope in the portal and issue a new key |
ip_not_allowed |
403 |
Caller IP is outside the key’s allowlist |
Add the egress IP, or clear the allowlist |
forbidden |
403 |
Allowed to authenticate, not to do this |
Check the account’s permissions |
| Code |
Status |
What happened |
What to do |
validation_failed |
422 |
Malformed body, or a missing Idempotency-Key |
Read details and fix the payload |
not_found |
404 |
No such record — or it is not yours |
Verify the id and the environment |
conflict |
409 |
Clashes with the current state |
Re-read the record, then decide |
rate_limited |
429 |
Over the per-minute allowance |
Back off — see Rate limits |
idempotency_key_reused |
409 |
Key already used for a different operation |
Use a fresh key for a new operation |
pending_request_exists |
409 |
A refund request for this eSIM is already waiting for review |
Wait for the decision, or check List refund requests |
| Code |
Status |
What happened |
What to do |
insufficient_balance |
422 |
The order costs more than you hold |
Top up, then retry |
billing_required |
422 |
No billing details on the account |
Complete billing in the portal |
billing_not_verified |
422 |
Billing details are awaiting verification |
Wait for verification |
partner_pending_approval |
403 |
Account registered, not yet approved |
Wait for approval |
| Code |
Status |
What happened |
What to do |
package_not_available |
422 |
Package is withdrawn or not sold to you |
Re-read the catalog |
package_not_priced |
422 |
No price applies to your account |
Contact support |
price_changed |
422 |
Price moved away from your expected_total |
Re-read the price and confirm with the buyer |
order_not_refundable |
422 |
Order is past the refundable point |
Nothing to do |
brand_not_configured |
422 |
No brand username on the account, so a share link cannot be built |
Set up the brand in the portal under Settings, Brand |
| Code |
Status |
What happened |
What to do |
internal_error |
500 |
A fault on our side |
Retry with backoff; quote request_id if it persists |
service_unavailable |
503 |
Temporarily down or in maintenance |
Retry with backoff |
Retry 429, 500 and 503 with exponential backoff. Retry token_expired
once, after refreshing.
Never blind-retry a 422 — the request is wrong and will stay wrong until you
change it.
Whenever you retry an order, send the same Idempotency-Key. That is what
makes a retry safe rather than expensive. See Idempotency.