Errors & rate limits
One error envelope across every endpoint. Per-minute rate limits scaled by plan (100/min on Reads, 300/min on Reads + Webhooks) — surfaced via standard headers on every response.
Error envelope
The same shape on every endpoint, on every status code from 400 upward.
{
"error": {
"code": "invalid_query",
"message": "Unknown field 'percent_change'. Did you mean 'day_change_pct'?",
"request_id": "req_01J9PZA1F2C9V3XQ"
}
}Always include the request_id when you contact support. It points us at the exact log entry for your request.
Error codes
Stable, machine-readable codes. The HTTP status is set accordingly.
| Code | Status | When you see it |
|---|---|---|
invalid_request | 400 | The request was malformed (bad JSON, missing required parameter, invalid value). |
invalid_query | 400 | The query string failed to parse, references an unknown field, or uses a SQL feature we don’t accept. |
unknown_signal | 404 | A signal name in the path does not exist. See the schema page for valid names. |
not_found | 404 | The requested resource (ticker, webhook) was not found. |
unauthorized | 401 | The Authorization header is missing or the API key is invalid. |
forbidden | 403 | The API key is valid but does not include the requested capability (e.g. webhooks on the Reads tier). |
rate_limited | 429 | You exceeded your plan's per-minute request limit. The `Retry-After` header indicates seconds to wait until the next minute boundary (always under 60s). |
internal_error | 500 | Something went wrong on our side. Safe to retry with exponential backoff. Include the `request_id` if you contact support. |
Rate limit
Per-key, per-minute window. The cap depends on your plan.
| Plan | Per-minute limit | Sustained equivalent |
|---|---|---|
| Reads | 100 / min | ~1.7 req/sec |
| Reads + Webhooks | 300 / min | ~5 req/sec |
| Enterprise | custom | — |
The window is a rolling-minute bucket: brief bursts above the sustained rate are absorbed as long as you stay under your per-minute cap. When you cross the limit you get 429 rate_limited with a Retry-After header pointing at the next minute boundary (always under 60s).
Every successful response — and every 429 — includes rate-limit headers. Use them to back off proactively rather than waiting to be limited.
| Header | Description |
|---|---|
X-RateLimit-Limit | Per-minute request limit for the API key (depends on plan: 100 on Reads, 300 on Reads + Webhooks). |
X-RateLimit-Remaining | Requests remaining in the current minute window. |
X-RateLimit-Reset | Seconds remaining until the per-minute window resets. |
Retry-After | Seconds to wait before retrying. Present only on 429 responses. |
No daily cap. The per-minute window is what catches runaway clients; daily quotas mostly punish honest traffic without solving abuse. See authentication for the full plan comparison.
Backoff guidance
What to do when you get a 429.
- Read the
Retry-Afterheader on the 429 response. Wait at least that many seconds before retrying. - Use exponential backoff for transient
5xxerrors, starting at 1 second and doubling. Cap at 60 seconds. - Use the
X-RateLimit-Remainingheader to throttle yourself proactively rather than racing into the cap. - Treat
internal_error(500) as retryable. Treatinvalid_request(400),invalid_query(400), andunauthorized(401) as fatal — retrying won't help.