Every API error, and what your integration should do about each one
7 min read
Branch on the stable code, never on the message. Which errors charged you and which did not, which are safe to retry, and which need a person rather than a retry loop.
Every error comes back in one shape, and your integration should branch on error.code — never on the message. The code is stable and is meant to be switched on; the message is Arabic prose written to be shown to a person on your own screen, and it can be reworded without warning. Below is every code, what your program should do about it, and — the part that matters at month end — whether any money moved.
{
"error": {
"code": "insufficient_balance",
"message": "الرصيد لا يكفي لإتمام هذه العملية."
}
}
The short version
Only three of these are ever worth an automatic retry: 429, a 408-style transport failure, and a 5xx. Everything else is either a bug in your request or a state of the world that a retry cannot change, and retrying it in a loop turns one problem into a rate-limit problem as well.
And of every error below, not one of them leaves money moved. A refused order is an order that was not created and not charged. The only thing that spends your balance is a 201.
Authentication and permission
401 unauthenticated
The key is missing, unknown or revoked. Do not retry — no number of attempts makes an unknown key known. Check that the header is Authorization: Bearer <key> and that the key has not been replaced: issuing a new key for a holder revokes the previous one immediately. Alert a person; an integration that has lost its key is down, not degraded.
403 email_unverified
The account's address has not been confirmed, and no money moves on this account until it is. Nothing your code can do will clear it — somebody has to open the link in the confirmation mail. Surface this to a human immediately and stop sending orders.
403 forbidden
The key is valid but does not hold the ability this call needs. Keys can be scoped, and a key issued to read the catalogue cannot place an order. Check the key's abilities in your dashboard. This is a configuration error, not a transient one.
The request itself
404 not_found
An unknown SKU, or an order number that is not on your account. Another account's order number answers 404 rather than 403, because confirming that somebody else's order numbers exist would be an answer in itself. If a SKU that used to work now answers 404, re-read the catalogue: your stored SKU list has drifted.
422 validation_failed
The body is incomplete or invalid, and fields says which. A bug in your request builder. Log the fields payload — that is the whole diagnosis — and do not retry the same body.
422 invalid_quantity
The quantity is outside the denomination's minimum, maximum or step. No order was created and nothing was charged. Nothing is silently clamped into range, so correct the quantity against min_quantity, max_quantity and quantity_step from the catalogue and send again. If you are hitting this on a per-unit denomination, read the two kinds of order — quantity there means units, not blocks.
422 player_id_required
The denomination is credited straight to an account and the body carried no account id. Refused before anything was charged, and no order was created. Collect the id in your own checkout and send again. It is a 422 rather than a 409 on purpose: this is one field short of being fillable, which is the caller's to fix, whereas a 409 is a state of the catalogue you cannot affect.
Money and state
402 insufficient_balance
The balance does not cover the order. Nothing was charged. Stop placing orders, alert a person, and have the account owner top the wallet up from the dashboard — the balance is credited once the network has confirmed the USDT payment, so this is not a wait of seconds. Watching GET /v1/balance against a threshold of your own is better than discovering it on a customer's order.
409 package_unavailable
The denomination is off sale or cannot be filled at the moment. Nothing was charged. Do not retry in a loop: this is a state of the catalogue rather than a transient failure. Take the item out of your shop, or mark it unavailable, and re-read the catalogue on a schedule to learn when it comes back.
409 idempotency_conflict
The idempotency key has already been used for a *different* order — another SKU, or another quantity. This is always a bug in how your keys are generated, and it will never succeed on retry. Look at your own order record for that key. Retrying an order without buying it twice is the guide for it.
429 rate_limited
You have gone over the ceiling counted per key. The default is sixty requests a minute, and the value that actually applies to your account comes back in X-RateLimit-Limit on every response, with X-RateLimit-Remaining beside it. This is the one error to retry automatically: back off exponentially with jitter, and read the remaining header rather than counting requests yourself.
What is not an error
Two answers look like problems in a log and are not.
A 201 with "status": "pending" and an empty codes array is a successful order. The order is accepted before it is filled so that your connection is never held open; fulfilment is automatic and takes seconds, and completion reaches you as order.delivered or, if it could not be filled, as order.refunded with the whole amount already back in your wallet.
An order that stays pending unusually long is under review rather than failed. No code has been delivered, no money has been returned, and it will never be sent again. Do not re-send it with a new key to hurry it along — you would be buying a second one. You are not charged twice for it under any circumstances.
A retry policy worth copying
-
Retry:
429, connection failures, timeouts and5xx— always carrying the sameIdempotency-Keyas the attempt being retried. -
Do not retry, fix the request:
422 validation_failed,422 invalid_quantity,422 player_id_required,409 idempotency_conflict. -
Do not retry, tell a person:
401,403 email_unverified,403 forbidden,402 insufficient_balance. -
Do not retry, change your catalogue:
404 not_found,409 package_unavailable.
The status codes, the messages and the request shapes are all in the API reference.
Questions
Should I ever show the message to my customer?
It is written to be shown on your own screen, to you or to your staff, and it is in Arabic whichever language you read the reference in. Map the code to your own wording for anything a customer sees.
A request timed out with no response at all. Which error is that?
None — you have no answer, which is a different thing from a refusal. Send the identical request again with the identical idempotency key; if the first one did place an order you will get that order back rather than a second one.
Is method_not_allowed or bad_request something I will see?
Only from a malformed call — a wrong verb or an unparseable body. Both are bugs in the caller and neither is worth a retry.
Last updated