Public Data API
Errors and limits
Every stable error code and what to do about it; then authentication, the stored-data rule, what is public, and the rate limits a key changes.
Errors#
Every failure carries a stable code. Branch on that, not on the status: a code is part of the contract and adding one is an ordinary change, while changing one is a breaking change to whatever is already branching on it.
| Code | Status | Meaning |
|---|---|---|
API_V1_BAD_PARAMETER | 400 | A path or query parameter is not the shape the endpoint accepts. details names the parameter, what arrived and what is accepted |
API_V1_BAD_CURSOR | 400 | A cursor did not decode, or was minted for a different query. details carries both query fingerprints |
API_V1_UNSUPPORTED_FORMAT | 400 | ?format= asked for something other than json or csv |
API_V1_NOT_FOUND | 404 | The subject of a single-resource route is not in the index. The message names the factories in scope and points at /api/v1/health |
API_V1_DATA_NOT_STORED | 404 | The request asks for data we have not stored — today, a from/to range on /stats/daily or /fees entirely before the first stored day or after the newest indexed block’s day. Never fetched on demand. details: requestedFrom, requestedTo, storedFirstDay, storedThroughDay, asOfBlock, table. Retry with a range that overlaps the stored days |
API_V1_MODERATION_UNREADABLE | 503 | The list of launches hidden from listings could not be read, so /launches refuses rather than serve an unfiltered list. Retry shortly |
API_V1_RATE_LIMITED | 429 | The tier’s window is full. The exact wait is in the message and the details |
API_V1_READER_SHAPE_UNEXPECTED | 500 | A query ran and what came back is not the shape the view declares — for instance a quantity that arrived as a float. Refused rather than published |
API_V1_READER_UNREACHABLE | 503, or 500 | The read-only connection could not be opened or a query failed (503); an unclassified failure wrapped at the endpoint boundary answers 500, with the original message in details.underlying |
API_V1_READER_URL_MISSING | 503 | The reader connection variable is not set in this environment |
API_V1_READER_URL_IS_WRITER | 503 | The reader is unset while the writer is set. The API refuses to fall back onto the writer rather than working today and breaking the day a replica is introduced |
API_V1_READER_DRIVER_MISSING | 503 | The Postgres driver is not installed. The message names the package and the command |
API_V1_FRESHNESS_CHECKPOINT_MISSING | 503 | The indexer has written no checkpoint, so no figure can carry an asOfBlock — and no figure is published without one |
A failure is sent with Cache-Control: no-store. A cached 503 outlives the fault that caused it, so an RPC that recovers in two seconds would otherwise keep serving “unreachable” for the rest of the window.
Authentication#
| Question | Answer |
|---|---|
| Which endpoints need credentials? | None. Every endpoint under /api/v1 is public and read-only. |
| Which accept an optional credential? | All of them, as an X-API-Key request header. It changes the rate limit and nothing else — not the data, not the fields, not which endpoints answer. |
| How is a key obtained? | By asking. Keys are issued by hand and held in this deployment’s configuration — there is no self-service portal. A key that was not issued for the environment you are calling is not recognised there. |
| What happens to an unrecognised key? | The request is served, and rate-limited as anonymous. The response says so: x-thehood-api-key-status: unrecognised. Nothing is rejected and no data changes. |
| Are cookies or bearer tokens accepted? | No. CORS advertises access-control-allow-origin: * with no allow-credentials, and the only request headers accepted on a preflight are content-type, if-none-match and x-api-key. |
| Is any endpoint write-capable? | No. Every route exports GET and OPTIONS only. |
Stored data only#
| The API | Rule |
|---|---|
| Serves | Only data already stored in The Hood’s own database — the indexer’s views, read over a read-only connection |
| Never calls | A provider on your behalf — no RPC node, price feed, explorer or social API is reached by any /api/v1 request. Chain head is never read |
| Never fetches on a miss | Data we have not stored is refused with API_V1_DATA_NOT_STORED, naming what is stored. It is not fetched, back-filled or proxied |
| Never exposes | Our provider keys, endpoints or their error text — in a body, a header or an error |
X-API-Key | A key for this API’s rate limit. It unlocks no provider and no extra data |
What is public, and what is not#
Rate limits and keys#
| Tier | Identified by | Requests per minute |
|---|---|---|
anonymous | The first entry of X-Forwarded-For, else X-Real-IP, else one shared bucket | 60 |
free | An X-API-Key header carrying a key issued for this deployment. An unrecognised value is counted as anonymous, in the caller’s own IP bucket. | 600 |
pro, business | An issued key configured onto that tier | 600 |
The counter lives in memory per function instance, so the effective limit is the figure above multiplied by however many instances are serving, and it resets when an instance is recycled. That is stated rather than hidden: a limit that is quietly ten times what it claims is a limit nobody can reason about. It still does the job it exists for, which is stopping one client opening a hundred concurrent connections against the reader from a single instance.
| Header | Value |
|---|---|
x-ratelimit-limit | The tier’s limit |
x-ratelimit-remaining | Requests left in this window, floored at 0 |
x-ratelimit-reset | Unix seconds at which the window ends |
x-thehood-api-tier | anonymous or the keyed tier |
x-thehood-api-key-status | absent (no header sent), recognised, or unrecognised (sent, not honoured, counted as anonymous) |
A refused request is a 429 with code API_V1_RATE_LIMITED, and its details carry path, tier, limit, remaining, resetAt, resetAtIso, retryAfterSeconds — the exact wait, in the body — plus apiKeyStatus, apiKeyLabel and apiKeyFingerprint, the first 8 hex characters of the key’s SHA-256. The key itself is never echoed, in the body or anywhere else; hash your own copy to confirm which one was used.