# Errors

Every error is JSON:

```json
{ "error": { "code": "not_found", "message": "ISBN 9780000000000 not found" } }
```

`message` is safe to log. Do not parse it. Branch on `code` and status.

| Status | `code` | Meaning |
|---|---|---|
| `400` | `bad_request` | Invalid ISBN, missing `q` / `since` / `title|isbn`, bad JSON |
| `401` | — | Missing, malformed, unknown, or revoked key |
| `404` | `not_found` | Valid request, no row (or no release / file) |
| `429` | `rate_limited` | More than 120 requests in 60 seconds for this key |
| `500` | `internal` | Server fault — retry with backoff |

MCP tools return the same JSON as text in the tool result. A 401 on `POST /mcp` never reaches a tool.

## Redirects

Point lookups on a **merged** id return `301` with `Location` pointing at the surviving entity (`/v1/books/…`, `/v1/works/…`, or `/v1/authors/…`). Follow them. Ids stay stable; the old id remains as a redirect.

## Validation examples

| Call | Typical `400` |
|---|---|
| `GET /v1/isbn/not-an-isbn` | Invalid ISBN |
| `GET /v1/books/search` (no `q`) | `q is required` |
| `POST /v1/match` `{}` | `title or isbn required` |
| `GET /v1/changes` (no `since`) | `since cursor is required` |

## See also

- [Rate limits and cache](/docs/limits)
- [Authentication](/docs/authentication)
