# Errors: one model

## Errors on GA's side, in one place

Your wallet's codes are Appendix A.1. What GA returns to you is Appendix A.2. Every error from `/v2/aggregator/*` and `/v2/features/*` has a non-200 HTTP status and one JSON body, `Content-Type: application/json`:

```json
{
  "code": "ERROR_CODE_MAINTENANCE",
  "message": "provider_unavailable",
  "retryable": true
}
```

`code` is one of the 15 `ERROR_CODE_*` values of Appendix A.1. `message` is for your logs and GA support. It starts with a stable token (`signature_invalid`, `game_unavailable`, `provider_timeout`, …). GA omits `retryable` when false. This holds for signature and key problems (401, 403) and for a read-only key on a write call (403) too.

Retry only `429`, `503`, and `504` (`retryable: true` in the body), with backoff. Every other status, `502` included, is final for that request.

## Codes your wallet returns to GA

Always HTTP 200 with `{"status":"error","error":{"code":…}}` (§2.5). 15 codes. The list is closed.

| Code | GA retries? | When to return it |
|---|---|---|
| `ERROR_CODE_INSUFFICIENT_FUNDS` | no | The balance can't cover the stake, or a credit rollback under `DEBIT_MODE_REJECT` |
| `ERROR_CODE_PLAYER_NOT_FOUND` | no | No such player |
| `ERROR_CODE_PLAYER_BLOCKED` | no | Player blocked, self-excluded, or disabled. **New bets only.** Still accept wins and rollbacks |
| `ERROR_CODE_CURRENCY_MISMATCH` | no | Currency or exponent doesn't match the player's account |
| `ERROR_CODE_BUSINESS_REJECTED` | no | Your own business rule refused it. Also a new bet into a closed or voided round |
| `ERROR_CODE_UNKNOWN_ORIGINAL` | no | A credit names an original you don't have. **Never for a rollback or a reconcile** |
| `ERROR_CODE_ALREADY_ROLLED_BACK` | no | You already rolled back the `op_id` (Rule 4), or the round is voided |
| `ERROR_CODE_UNSUPPORTED_ACTION` | no | You don't implement this optional action (HTTP 200 or 404, with the envelope) |
| `ERROR_CODE_RATE_LIMITED` | yes | You are rate-limiting GA (HTTP 200 or 429, with the envelope) |
| `ERROR_CODE_MAINTENANCE` | yes | Your wallet is down for maintenance |
| `ERROR_CODE_INVALID_REQUEST` | no | Malformed request: missing field, zero or negative amount, overflow, unknown enum value, missing token on a session call |
| `ERROR_CODE_SESSION_EXPIRED` | no | Launch token or session expired. **New bets only** |
| `ERROR_CODE_LIMIT_EXCEEDED` | no | Deposit or loss limit reached. **New bets only** |
| `ERROR_CODE_INTERNAL` | yes | Unexpected error on your side (HTTP 200 or 500, with the envelope) |
| `ERROR_CODE_IDEMPOTENCY_CONFLICT` | no | Same `op_id`, different content (Rule 1) |

Bare statuses without the envelope (`401`, `403`, `404`, `429`, `5xx`) are all "no answer": GA retries, and a bet ends in a rollback.

## What GA returns to you

Every `/v2/aggregator/*` and `/v2/features/*` error is the body of §2.8. `message` starts with the token in the table.

| HTTP | `code` | Retry? | `message` starts with | Meaning |
|---|---|---|---|---|
| `400` | `ERROR_CODE_INVALID_REQUEST` | no | `invalid json body`, `invalid request`, `invalid_request`, `invalid_provider_ref`, `capability_not_supported`, `bonus_buy_not_cancellable` | Malformed JSON, missing or invalid field, GA can't read the body, or the game provider doesn't offer the capability |
| `401` | `ERROR_CODE_INVALID_REQUEST` | no | `unauthorized`, `signature_missing`, `signature_invalid`, `signature_stale` | Wrong or missing key id, signature, or a timestamp outside 300 s |
| `403` | `ERROR_CODE_BUSINESS_REJECTED` | no | `forbidden` | The operator is suspended, or the key isn't allowed to do this (for example a read-only key on `launch_game`) |
| `403` | `ERROR_CODE_BUSINESS_REJECTED` or `ERROR_CODE_INVALID_REQUEST` | no | `operator_suspended`, `operator forbidden`, `restricted`, `game_unavailable`, `PLAYER_SELF_EXCLUDED`, `PLAYER_COOL_OFF` | Not allowed for this operator, game, or player |
| `404` | `ERROR_CODE_INVALID_REQUEST` | no | `provider not found`, `provider_not_found`, `game not found`, `player_not_found`, `bonus_buy_not_found` | Provider, game, player, grant, or bonus not found |
| `409` | `ERROR_CODE_IDEMPOTENCY_CONFLICT` | no | `session_token_conflict` | You reused the `token` of `launch_game` with another player, game, or currency, or after its session closed or expired, or the `idempotency_key` of `issue_grant` with a different body (§4.1, §4.4) |
| `412` | `ERROR_CODE_INVALID_REQUEST` | no | `DEMO_NOT_SUPPORTED` | `launch_demo` on a game with `demo_supported: false` |
| `422` | `ERROR_CODE_INVALID_REQUEST` | no | `unsupported action` | Action not supported for this game or provider |
| `429` | `ERROR_CODE_RATE_LIMITED` | yes | | Rate limit |
| `500` | `ERROR_CODE_INTERNAL` | no | `internal error` | GA-side failure. GA gets an alert. Contact support if it repeats |
| `501` | `ERROR_CODE_UNSUPPORTED_ACTION` | no | | Call not implemented on this binding (`settlement_mode PER_ROUND`) |
| `502` | `ERROR_CODE_INTERNAL` | no (`retryable` not set) | `provider_error` | The game provider returned an error. Repeating the same call returns the same error |
| `503` | `ERROR_CODE_MAINTENANCE` | yes | `provider_unavailable` | The game provider is unavailable |
| `503` | `ERROR_CODE_INTERNAL` | yes | `launch_in_progress` | The first `launch_game` with this `token` is still running. Retry with the same `token` (§4.1) |
| `504` | `ERROR_CODE_INTERNAL` | yes | `provider_timeout` | The game provider timed out |

Retry only `429`, `503`, and `504`, with backoff.
