# API error codes

Every failure answers with an HTTP status and a stable machine-readable code. **Branch on the code, display the message.** Messages are translated and may change wording; codes do not.

```json
{ "error": { "code": "invalid_request", "message": "…" } }
```

## Codes

| Code | Status | Meaning | What the client should do |
|---|---|---|---|
| `route_not_found` | 404 | No such endpoint | Fix the path; do not retry |
| `method_not_allowed` | 405 | Endpoint exists, wrong method | Use a method from the `Allow` header |
| `invalid_request` | 422 | Malformed or failed validation | Fix the payload; do not retry unchanged |
| `unauthorized` | 401 | Missing, malformed or revoked token | Re-authenticate |
| `forbidden_scope` | 403 | Authenticated, but not permitted | Do not retry with the same token |
| `rate_limited` | 429 | Too many requests | Back off, then retry |
| `mutation_failed` | 4xx | The write was refused by policy | Read the message; usually a content or state rule |
| `not_implemented` | 501 | Reserved endpoint, contract not final | Do not retry; watch the changelog |
| `database_unavailable` | 503 | The database could not be reached | Retry with backoff |
| `server_error` | 500 | Unexpected failure, already logged | Retry once, then report |

## Distinctions that matter

- **`route_not_found` vs `not_implemented`** — the first means the path will never exist as written; the second means it is planned and reserved. Do not treat them the same.
- **`unauthorized` vs `forbidden_scope`** — the first is fixable by authenticating again; the second is not fixable by the client at all.
- **`invalid_request` vs `mutation_failed`** — the first is a bad payload; the second is a well-formed request refused by policy (rate limit, moderation, duplicate, closed thread).

## What errors never contain

Stack traces, SQL, file paths, and secrets. Server-side failures are logged with detail; the response carries only the code and a translated message.
