# Errors

> One error envelope, a stable code and a request id. Failed requests are never billed.

Every error answers with the same body:

```json
{
  "error": {
    "code": "invalid_request",
    "message": "limit must be a whole number from 1 to 100.",
    "param": "limit",
    "request_id": "req_4f8Kq2ZtW9mXb7Lc1RnP"
  }
}
```

- **`code`** is stable. Branch on it, not on the message.
- **`message`** says what went wrong in plain words. It may change.
- **`param`** names the parameter to fix, on `invalid_request`.
- **`request_id`** is also the `X-Request-Id` header. Every answer carries it, errors and successes alike. Quote it when you ask us about a request.

An error is never cached, and **an error is never billed**. Only an answer in the `2xx` range counts toward your bill.

## Error codes

| Code                   | Status | What it means                                                                                                                      |
| ---------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| `unauthorized`         | `401`  | No key, or not a valid one: unknown, revoked, or not a Hey Town Hall Data key. The answer carries `WWW-Authenticate: Bearer`.      |
| `forbidden`            | `403`  | Signed in, but not allowed. The dashboard answers it for an action only an account owner can take; `/v1` and `/mcp` do not use it. |
| `live_not_open`        | `403`  | A live key, or a request made with one, while live keys are closed. They open once the rates are final; sandbox keys work now.     |
| `invalid_request`      | `400`  | A parameter is missing or not valid, a parameter the endpoint does not take was sent, or one was sent twice. `param` names it.     |
| `not_found`            | `404`  | No such record, or it is no longer published. Also a path the API does not have.                                                   |
| `rate_limited`         | `429`  | Past the key's requests per minute. `Retry-After` says how many seconds to wait.                                                   |
| `spend_cap_reached`    | `402`  | This month's bill has reached the spending cap you set. Live requests stop until you raise or remove the cap.                      |
| `billing_required`     | `402`  | A live key with no payment method on file, or live keys paused for an unpaid invoice. Add billing in the dashboard.                |
| `upstream_unavailable` | `503`  | The data could not be read right now. It is never an empty answer in disguise; try again shortly.                                  |
| `internal`             | `500`  | Something went wrong on our side. Try again shortly.                                                                               |

## What to do

- **`400`:** fix the parameter that `param` names. Retrying the same request gets the same answer.
- **`401`:** check the key and the header. See [Authentication](https://data.heytownhall.com/docs/authentication).
- **`402`:** add billing or raise your spending cap in the dashboard. See [Billing](https://data.heytownhall.com/docs/billing).
- **`404`:** the record is gone or never existed. Check the id against a list.
- **`429`:** wait the seconds that `Retry-After` gives, then try again. See [Rate limits](https://data.heytownhall.com/docs/rate-limits).
- **`500` and `503`:** try again shortly, waiting a little longer each time. A `503` is never an empty answer in disguise: nothing was left out, the data could not be read.

## Errors in MCP

A tool call that fails returns a tool result marked as an error, with the same envelope as its text. A missing or invalid key is refused before the handshake, with `401` or `402` as an HTTP answer.
