# Errors > The error envelope, status codes, the retryable flag, and how to handle failures well. Source: https://casinoapipro.com/docs/errors Every error, from any endpoint, has the same shape. Parse the `code` — the `message` is written for a human reading a log and may change without notice. ## The envelope ```json { "error": { "code": "INSUFFICIENT_FUNDS", "message": "player balance is below the requested stake", "request_id": "fa1bc6b7-bd54-480e-92c1-6ffa700d6039", "retryable": false } } ``` | Name | Type | Required | Description | | --- | --- | --- | --- | | `code` | string | yes | Stable machine-readable identifier. Branch on this. | | `message` | string | yes | Human-readable detail. Not stable; do not parse. | | `request_id` | string | yes | Quote this to support and we can find the exact request. Also returned as the `X-Request-Id` header on every response. | | `retryable` | boolean | yes | Whether retrying the identical request with the same `transaction_id` can succeed. | > **retryable is the field that matters** > > It removes the guesswork from your retry loop. If it is `true`, back off and send the same request again. If it is `false`, retrying will fail the same way forever — fix the request or surface it. ## Status codes | Name | Type | Meaning | | --- | --- | --- | | `400` | Bad Request | Malformed body — not JSON, or a field of the wrong shape. | | `401` | Unauthorized | Missing, expired or invalid token. | | `403` | Forbidden | Authenticated, but not permitted — wrong audience, suspended account, or an IP outside the allowlist. | | `404` | Not Found | No such resource, or it belongs to another operator. We do not distinguish the two. | | `409` | Conflict | Idempotency conflict — same `transaction_id`, different body. | | `422` | Unprocessable | Well-formed but invalid: a stake outside limits, an unsupported currency, an illegal move. | | `429` | Rate limited | Too many requests. Honour `Retry-After`. | | `500` | Server error | Our fault. Retryable. Quote the request id if it persists. | | `504` | Wallet timeout | Your wallet did not answer in time. Retryable with the same id — see [Idempotency](/docs/idempotency). | ## Why 404 and not 403 Asking for another operator's session returns `404`, not `403`. Answering "forbidden" would confirm the resource exists, which leaks the shape of other tenants' data to anyone willing to enumerate ids. Absence and denial look identical from outside. ## Handling errors well ```js const res = await fetch(url, options); const body = await res.json(); if (!res.ok) { const { code, retryable, request_id } = body.error; // Log the request id with everything. It is the only thing that lets // support find your exact call in our logs. logger.warn({ code, request_id, status: res.status }, "casinoapipro error"); if (retryable) return scheduleRetry(); switch (code) { case "INSUFFICIENT_FUNDS": return tellPlayer("Not enough balance"); case "BET_LIMIT_EXCEEDED": return tellPlayer("Stake above the table limit"); case "IDEMPOTENCY_CONFLICT": return alertEngineering(request_id); default: return alertEngineering(request_id); } } ``` A full list of codes lives in [Error codes](/docs/error-codes).