# Error handling

## Live today

| Status | When | Body |
|--------|------|------|
| **403** | Missing/invalid HMAC or session on `/rest/*` | `Forbidden` (text, Express `sendStatus`) |
| **403** | Request outside your account's coverage grant (see the [coverage guide](./coverage.md)) | JSON `{"error":"coverage_not_entitled"}` |
| **400** | Invalid odds query: empty `eventTime` or `eventName` | `Bad Request` (text) |
| **404** | `/rest/match/*` found no match, or `/rest/account` has no record for the identifier | JSON `{"error":"Not found"}` / `{"error":"account_not_found"}` |
| **429** | Capped account already over this hour's data-point limit | JSON `{"error":"throttled","data_points_this_hour":n,"data_points_limit":n,"hour_resets_at":iso,"retry_after_seconds":n}` |
| **200** | `/authenticate` always — check `authenticated` boolean | JSON `{ authenticated, admin }` |
| **200** | Successful catalog reads (including empty arrays) | JSON per OpenAPI schemas |
| **500** | Unexpected server failure on some odds routes | Implementation-defined |

Clients should treat a **403** on `/rest` as “re-auth / refresh hash” — unless the body is
`coverage_not_entitled`, which means the request is outside the account's coverage grant rather than
a credentials problem (see the [coverage guide](./coverage.md)). A **429** body carries the current
usage. Do not assume one enveloped error shape across the API: the odds routes answer with plain-text
status bodies (`Forbidden`, `Bad Request`), `/rest/match/*` returns `{"error":"..."}` on
**400**/**404**, and `/rest/account` returns `{"error":"account_not_found"}` on **404**.

### Metering headers

Successful responses on the metered routes (`GET /rest/odds` and `/rest/match/*`) carry the caller's
current usage:

| Header | Value |
|--------|-------|
| `X-Data-Points-This-Hour` | Data points charged in the current hour |
| `X-Data-Points-Limit` | Hourly limit, or `unlimited` |
| `X-Hour-Resets-At` | ISO timestamp when the current hour bucket resets |

`GET /rest/account` reports the same limit (and the account's coverage grant) for introspection; it
is not metered. A **429** carries the three headers plus the JSON body above.

### Auth timing

If the hash’s embedded Unix time is outside the acceptance window, auth fails → **403** on `/rest` (or `authenticated: false` on `/authenticate`). Clock skew and reused hashes outside the grace path will look the same to the client.

## Planned (not live yet)

The following codes and headers are planned. The API **does not emit them yet**, so SDKs and clients
must not require them for green paths.

| Code / signal | Intent (future) | Status |
|---------------|-----------------|--------|
| `feed_down` | Feed or upstream availability outage signal | **Planned — not live** |
| `catalog_dropped` | Coverage-driven trimming of a catalog response body | **Planned — not live** |
| `payment_required` | Billing / account payment gate | **Planned — not live** |

Coverage enforcement and per-hour throttling are **live** (the **403** `coverage_not_entitled` and
**429** `throttled` rows above); the codes in this table are not. When they ship, OpenAPI response
schemas, these guides, and the SDKs will be updated.

## Related

- [Quickstart](./quickstart.md)
- [Coverage model](./coverage.md)
