# Errors

> The error envelope, every error code and what to do about each.

Page: https://docs.aggre.org/concepts/errors · Section: Concepts · All pages: https://docs.aggre.org/llms.txt

Every error from the data API has the same JSON shape, plus the response header `x-request-id`:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "unknown query parameter: limt",
    "request_id": "8f5c0c3e-..."
  }
}
```

`code` is the stable machine code; branch on it, not on `message` (which is for people and may change). A client must tolerate codes it does not know. The JSON Schema is [`error`](https://docs.aggre.org/api/schemas.md#error). Assert on the **code**, not just the status: several different problems share a status (two kinds of `429`, several kinds of `503`).

## Codes of the data API {#data-api}

| Status | `error.code` | Meaning | What to do |
|---|---|---|---|
| 400 | `invalid_request` | Unknown or repeated query parameter, value out of range, malformed id/date, a cursor from another route, a wrong combination of parameters. | Fix the request. Do not retry unchanged. |
| 401 | `unauthorized` | No key, malformed key, unknown key or revoked key. | Check the key and header. |
| 403 | `ip_not_allowed` | The key has an IP allowlist and the request came from outside it. | Call from an allowed address or edit the allowlist. |
| 403 | `forbidden` | The account is not allowed to use the key (for example it is disabled). | Contact the account owner. |
| 404 | `not_found` | The id is well formed but nothing is stored under it (an unknown series, statute, record), or the path has no id. | Check the id. For stored-in-stages datasets the record may not be loaded yet. |
| 410 | `taken_down` | The judgment has been taken down and is hidden (judgment routes, when the deployment hides takedowns). | Do not retry. |
| 429 | `rate_limited` | Too many requests from your source IP, checked before the key. | Wait `Retry-After` seconds, then retry with backoff. |
| 429 | `usage_limit_exceeded` | The key's quota for its period is spent. | Do **not** retry. Use another key or wait for the period to roll. |
| 501 | `corpus_not_migrated` | A judgment route whose corpus has not been loaded yet. | Try later. |
| 503 | `store_unavailable` | The database behind the route is not reachable or not configured. | Retry with backoff; if it persists, report the `request_id`. |
| 503 | `terms_unread` | The source's publisher terms have not been recorded as read, so the route will not serve its data. | Not a transient error. See [Freshness and availability](https://docs.aggre.org/concepts/freshness.md). |
| 503 | `index_unavailable` | The search index behind a retrieval route has no index yet. | Retry later. |
| 503 | `retrieval_unavailable` | The retrieval service is not reachable. | Retry with backoff. |
| 500 | `internal` | Something went wrong on our side. | Retry once; if it persists, report the `request_id`. |

## Codes of the accounts API {#accounts-api}

The accounts service (the console's backend) uses the same envelope and adds codes for sessions and writes: `conflict` (409), `origin_not_allowed`, `csrf_required`, `step_up_required`, `account_disabled`, `email_mismatch` (403), `validation_failed`, plus codes for platform-staff features. See the [accounts API](https://docs.aggre.org/api/accounts.md).

## Handling errors well {#handling}

1. Log `error.code` and `request_id` for every non-2xx answer.
2. Retry only what can change: `429 rate_limited` (after `Retry-After`) and `503 store_unavailable` / `500 internal` (with exponential backoff and a cap). Never retry `400`, `401`, `403`, `404`, `429 usage_limit_exceeded` or `503 terms_unread` unchanged.
3. Treat `meta.status: "empty"` on a 200 as a valid, empty answer, not an error.

## The legacy MCP routes {#mcp}

`POST /v1/mcp/call` keeps the older flat error shape `{ "error": "message" }` so existing tool clients keep working. See [MCP compatibility](https://docs.aggre.org/agents/mcp.md).
