Errors and rate limits
Every endpoint reports failures in the same shape, with a machine-readable code and a request ID.
Error response
A failed call returns success: false. code is stable and meant for programs, error is a short English message meant for people, and request_id identifies the call in our logs. param names the offending parameter when the problem is an input.
Real examples
These are live responses, fetched from the API when this page was rendered.
{
"success": false,
"error": "coin required",
"code": "INVALID_PARAM",
"request_id": "cb-mwefrmsw-d044286571"
}{
"success": false,
"code": "NOT_FOUND",
"error": "unknown endpoint: GET /api/this-endpoint-does-not-exist",
"request_id": "cb-mwefrmsw-4573d655b3"
}Error codes
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| INVALID_PARAM | 400 | A required parameter is missing or a value is not accepted. `param` names the parameter when it is known. | Do not retry unchanged; fix the request. |
| UNAUTHORIZED | 401 | The `CB-API-KEY` header was sent but the key is unknown or revoked. Public data needs no key — drop the header or use a valid key. | Do not retry unchanged. |
| FORBIDDEN | 403 | The route is not part of the public API (internal or account-only). | Do not retry. |
| NOT_FOUND | 404 | Unknown route, or the requested coin / exchange / fund does not exist. | Do not retry; check the path and identifiers against the catalog. |
| RATE_LIMITED | 429 | More requests than the per-minute limit. `Retry-After` gives the seconds to wait. | Wait for `Retry-After`, then retry with backoff. |
| INTERNAL | 500 | Unexpected server error. The message is generic by design; quote `request_id` when reporting it. | Retry once after a few seconds; report it with the request id if it persists. |
| UPSTREAM_FAILED | 503 | The data source behind this endpoint did not answer in time. No stale or substitute numbers are returned in its place. | Retry with backoff; check the status page. |
| UNAVAILABLE | 501 / 503 | The dataset is temporarily not ready (cache warming) or the endpoint has no lawful public source yet. | Retry later; the catalog marks endpoints that are unavailable by design. |
| REQUEST_FAILED | 200 | Some older endpoints answer HTTP 200 with `success: false`. Always check `success`, not only the HTTP status. | Depends on the message; treat as a failed request. |
Request ID
Each response carries an X-Request-Id header. Failed responses repeat it in the body as request_id. Responses served from the short edge cache repeat the ID of the request that produced them, so identical IDs on consecutive calls mean you received a cached copy.
Rate limits
Limits apply per minute, per IP address for anonymous calls and per key when a valid CB-API-KEY header is sent. The limit is 1200 requests per minute. Icon requests under /api/coin-icon are not counted.
Rate limit headers
| Name | Description |
|---|---|
| X-Request-Id | Identifier of this request. Quote it in bug reports. |
| X-RateLimit-Limit | Requests allowed in the current one-minute window. |
| X-RateLimit-Remaining | Requests left in the current window. |
| X-RateLimit-Reset | Unix time in seconds when the window resets. |
| Retry-After | Sent with HTTP 429: seconds to wait before retrying. |
| X-Cache | HIT, STALE or MISS for the server-side response cache. |
When you exceed the limit
The API answers HTTP 429 with code RATE_LIMITED. Wait for the number of seconds given in Retry-After, then retry. Polling faster than an endpoint's cache TTL returns the same cached response, so match your polling interval to the TTL shown on each reference page.
Retry with backoff
async function getJson(url, tries = 4) {
for (let i = 0; i < tries; i++) {
const res = await fetch(url);
if (res.status === 429 || res.status >= 500) {
const wait = Number(res.headers.get("Retry-After")) || 2 ** i;
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
const body = await res.json();
if (body.success === false) {
throw new Error(body.code + ": " + body.error + " (" + body.request_id + ")");
}
return body;
}
throw new Error("gave up after " + tries + " attempts");
}No data is not an error
When the parameters are valid but there is no lawful public source for the data, the call succeeds with HTTP 200: the value fields are null, meta.quality is UNAVAILABLE and coverage.unavailable lists the reason. Render it as a dash. Do not convert it to 0.
Stale data
If a collector falls behind, endpoints keep serving the last stored values with meta.is_stale: true until they pass the usable window, after which they are dropped from aggregates and listed in coverage.stale_excluded.