Error codes
A failed response always looks like this:
json
{ "ok": false, "error": { "code": "INSUFFICIENT_BALANCE", "message": "insufficient available balance" } }- Branch on
code, never onmessage:messageis a developer-facing explanation (mostly English) and may change. - HTTP status codes are meaningful:
400bad input,401authentication failed,403not allowed (IP, tenant, player),404not found,409conflict,413body too large,429rate limited,5xxserver error. - A
5xxbody may not be JSON (for example when it fails at the Cloudflare layer); always treat it as temporary.
Retry rules
| Situation | What to do |
|---|---|
429, 5xx, timeout, connection error | Back off and retry (for example 1, 2, 4, 8 seconds with jitter), with a new nonce and timestamp each time |
| Deposit or withdrawal with an unknown outcome | Retry with the same txnId and content until you get a 2xx or 4xx; see Wallet: timeouts and retries |
Any other 4xx | Do not retry as is: fix the request according to the code |
401 (nonce already used) | The request was not processed; send it again with a new nonce |
The tables below are generated from the OpenAPI spec and list every error code the implementation can return.
Common errors (any endpoint)
| Code | HTTP | Meaning | What to do | Retry |
|---|---|---|---|---|
UNAUTHORIZED | 401 | Authentication failed. invalid credentials: the key ID is malformed, unknown or revoked (a rotated key more than 24 hours old counts), the nonce is malformed, or the signature does not match. timestamp outside the ±300 s window: X-Timestamp is malformed or more than 300 seconds away from server time. nonce already used: the nonce was used again within 10 minutes (this request was not processed).message: invalid credentials, timestamp outside the ±300 s window, nonce already used | Compare your string to sign with the Signature debugger. Check that the path starts with /api/tenant/v1 and includes the query string, and that you hash the exact body bytes you send. Sync your server clock (NTP). Use a new nonce and timestamp for every request, including retries. | fix first |
IP_NOT_ALLOWED | 403 | You have an IP allowlist and the request came from an IP that is not on it.message: source IP is not in the allowlist | Add your server's outbound IP (IPv4/IPv6 or CIDR) in the Console under "Go-live & integration → IP allowlist and Webhook". | fix first |
TENANT_SUSPENDED | 403 | The tenant is disabled, suspended or closed; the whole API is unavailable.message: tenant is suspended | Check the account state in the Console or contact support. | fix first |
RATE_LIMITED | 429 | Too many requests per second (100/s on the paid plan, 20/s in the sandbox, 5/s on the free plan; short bursts up to 2×).message: too many requests | Back off and retry (for example 200 ms, 400 ms, 800 ms… with jitter) with a new nonce and timestamp. Reduce calls by paging with cursors. | yes (new nonce and timestamp) |
PAYLOAD_TOO_LARGE | 413 | The request body is larger than 64 KB.message: request body too large | Send a smaller body; normal requests to this API are far below the limit. | fix first |
INVALID_JSON | 400 | The body is not valid JSON, or not a JSON object.message: request body is not valid JSON, request body must be an object | Send Content-Type: application/json and a JSON object ({…}). | fix first |
NOT_FOUND | 404 | unknown endpoint: no such method and path (for example GET on a POST endpoint, or a round ID that is not a 26-character upper-case ULID). table not found: the table to enable or disable does not exist.message: unknown endpoint, table not found | Check the method and path in the API reference; take table IDs from GET /tables. | fix first |
INTERNAL_ERROR | 500 | A temporary server error. Treat any 5xx (including ones whose body is not JSON) the same way.message: internal error, please retry | Back off and retry. Retry transfers with the same txnId, or look them up with GET /wallet/transfer first. If it persists, check the status page or contact support. | yes (new nonce and timestamp) |
Endpoint errors
| Code | HTTP | Meaning | What to do | Retry |
|---|---|---|---|---|
INVALID_PARAMETER | 400 | A required field is missing, or a field has the wrong format or value; message names the field.message: <field> is required, <field> must be a string, unsupported lang, lobbyUrl must be an http(s) URL, status must be active, locked or no_bet, password must be 6–64 characters, limitProfileId must be a positive integer or null, limitProfileId not found, limitProfileId currency is <CUR>, player currency is <CUR>, txnId must be 1–64 characters of A-Z a-z 0-9 _ . : -, txnId is required, from must be an ISO time, date must be a valid YYYY-MM-DD, from and to must be YYYY-MM-DD | Fix the field according to the API reference and send again. | fix first |
INVALID_USERNAME | 400 | The username breaks the rules (1–32 characters of letters, digits and _ . @ -), or username is missing from a query.message: username must be 1–32 characters of A-Z a-z 0-9 _ . @ -, invalid username | Map your players to usernames that follow the rules (for example derived from your member ID). | fix first |
INVALID_CURRENCY | 400 | The currency code given for a new player is invalid (3–5 letters required).message: invalid currency | Use a valid currency code, or leave currency out to use the tenant default. | fix first |
CURRENCY_NOT_ENABLED | 400 | The currency given for a new player is not enabled for the operator (Console → Bet limits → Currencies). Existing players are not affected.message: currency <CUR> is not enabled for this operator (enabled: <CUR>, <CUR>) | Enable the currency in Console and set up its bet-limit profiles first, or leave currency out to use the primary currency. | fix first |
INVALID_AMOUNT | 400 | The amount is not a positive decimal string with at most 4 decimals.message: amount must be a positive decimal string with at most 4 decimals, invalid amount | Send the amount as a string (for example "100.5"), never as a float or in scientific notation. | fix first |
INVALID_CURSOR | 400 | cursor is malformed.message: cursor must come from a previous response | Send back the previous page's nextCursor unchanged. If you lost it, resync from a time with from and de-duplicate on (slipId, rev). | fix first |
CURRENCY_MISMATCH | 400 | The transfer currency differs from the player's currency, which is fixed when the player is created.message: player currency is <CUR> | Transfer in the player's currency (see GET /player). Use separate usernames for different currencies. | fix first |
PLAYER_NOT_FOUND | 404 | No player with this username.message: player not found | Players are created on their first launch or first deposit; do one of those first. | fix first |
PLAYER_LOCKED | 403 | The player is locked and cannot be launched.message: player is locked | If appropriate, set status back to active with POST /player/update. | fix first |
PLAYER_LIMIT | 403 | The plan's player limit is reached (50 by default on the free demo plan, 500 in the sandbox).message: demo plan allows at most <N> players | Reuse existing test players, or upgrade to the paid plan. | fix first |
TXN_CONFLICT | 409 | This txnId was already used for a transfer with a different amount or direction, or for another player within the last two months.message: txnId was already used with different content, txnId was already used for another player | This is an ID bug on your side. Every new transfer needs a new txnId, unique across your tenant, and a retry must be identical to the first attempt. | fix first |
INSUFFICIENT_BALANCE | 409 | The withdrawal is larger than the available balance (stakes of unsettled bets cannot be withdrawn).message: insufficient available balance | Read the available balance with GET /wallet/balance and withdraw at most that. | fix first |
TXN_NOT_FOUND | 404 | No transfer with this txnId was found. With username in the query this means the transfer was not executed (or is still being processed); without it only the tenant ledger (current and previous 3 months) is searched.message: transfer not found | Resend the transfer with the same txnId and content until you get a definite success or 4xx. Do not treat it as failed and switch to a new ID. | fix first |
ROUND_NOT_FOUND | 404 | The round does not exist or is not settled yet.message: round not found | Rounds become available once settled; any roundId from a bet record can be looked up. | fix first |
QUOTA_EXCEEDED | 409 | You have reached the free plan's table quota (2 tables by default).message: 超過方案配額(<N> 桌) | Disable another table first, or upgrade in the Console under "Plan & billing → Plan". | fix first |
ACCOUNT_LOCKED | 409 | The account is suspended or closed, so tables cannot be enabled.message: account is suspended | Contact support. | fix first |