

## Every error code in one place [#every-error-code-in-one-place]

Status codes tell the HTTP story; the `error.code` field tells the API story. Codes are
SCREAMING\_SNAKE\_CASE and stable across versions — pattern-match on them, not on the message.

| Code                       | Status    | What it means                                                                                                        | Retry?                                                                 |
| -------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| `BAD_REQUEST`              | 400       | A parameter is malformed or missing (bad date format, invalid id shape).                                             | No — fix the request                                                   |
| `UNKNOWN_STAT`             | 400       | The `stat` key on an analysis endpoint isn't a real measure. The message lists the valid keys.                       | No — fix the stat key                                                  |
| `STAGE_NOT_SUPPORTED`      | 400       | The `stage` selector isn't valid for this competition.                                                               | No — fix the selector                                                  |
| `UNAUTHORIZED`             | 401       | Missing or invalid API key (`X-API-Key` or `Authorization: Bearer`).                                                 | No — fix the key                                                       |
| `INVALID_API_KEY`          | 401       | Legacy variant of the same condition; treat identically to `UNAUTHORIZED`.                                           | No — fix the key                                                       |
| `TIER_REQUIRES_PAID`       | 403       | Free-tier key calling a paid endpoint (`/v1/analysis/*`).                                                            | Upgrade                                                                |
| `NOT_FOUND`                | 404       | The resource id (person, team, contest, competition) doesn't exist.                                                  | No                                                                     |
| `QUOTA_EXCEEDED`           | 429       | Monthly unit allowance is gone.                                                                                      | Upgrade or wait — `X-RateLimit-Reset` carries the period-end timestamp |
| `RATE_LIMIT_EXCEEDED`      | 429       | Too many requests in the per-second/per-window cap.                                                                  | Back off briefly, then retry                                           |
| `INTERNAL_ERROR`           | 500       | Something broke on our side.                                                                                         | Retry once, then page support with `request_id`                        |
| `CACHE_ERROR`              | 500       | The cache layer failed while serving the request.                                                                    | Retry once, then page support                                          |
| `NOT_SUPPORTED`            | 501       | The competition doesn't support this capability. The body carries machine-readable `league` and `capability` fields. | No — feature-detect via `/v1/competitions/{comp}/capabilities`         |
| `PROVIDER_ERROR`           | 502 / 503 | An upstream data provider failed (502) or all of them did (503).                                                     | Exponential back-off, 3–5 attempts                                     |
| `DB_UNAVAILABLE`           | 503       | Database connection pool exhausted or unreachable.                                                                   | Exponential back-off, 3–5 attempts                                     |
| `AUTH_BACKEND_UNAVAILABLE` | 503       | Key validation backend is down (this is *not* an invalid key).                                                       | Exponential back-off, 3–5 attempts                                     |

## Example body [#example-body]

Every error body has the same envelope — just the `error` object, no `meta`. Some codes add
extra fields inside `error` beside `code`/`message`; `NOT_SUPPORTED` carries `league` and
`capability` so clients can feature-detect without string-matching:

```json title="error envelope"
{
  "error": {
    "code": "NOT_SUPPORTED",
    "message": "play_by_play is not available for epl",
    "league": "epl",
    "capability": "play_by_play"
  }
}
```

<Callout type="info" title="when to retry">
  The retry column is intentionally narrow — `RATE_LIMIT_EXCEEDED`, `PROVIDER_ERROR`,
  `DB_UNAVAILABLE`, `AUTH_BACKEND_UNAVAILABLE`, and a single attempt at `INTERNAL_ERROR` /
  `CACHE_ERROR` are the retryable set. A `429 QUOTA_EXCEEDED` and everything in the 4xx
  family will yield the same result until you change the request, the key, or the plan.
</Callout>

## See also [#see-also]

* [Errors](/docs/getting-started/errors) — status code semantics and back-off curves
* [Rate limits](/docs/getting-started/rate-limits) — the throttling headers
* [Quotas](/docs/getting-started/quotas) — what a unit is, and how the cap works
