Errors & rate limits

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.

Missing required parameter · GET /api/sd/oi · HTTP 400
{
  "success": false,
  "error": "coin required",
  "code": "INVALID_PARAM",
  "request_id": "cb-mwefrmsw-d044286571"
}
Unknown endpoint · HTTP 404
{
  "success": false,
  "code": "NOT_FOUND",
  "error": "unknown endpoint: GET /api/this-endpoint-does-not-exist",
  "request_id": "cb-mwefrmsw-4573d655b3"
}

Error codes

CodeHTTPMeaningWhat to do
INVALID_PARAM400A 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.
UNAUTHORIZED401The `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.
FORBIDDEN403The route is not part of the public API (internal or account-only).Do not retry.
NOT_FOUND404Unknown route, or the requested coin / exchange / fund does not exist.Do not retry; check the path and identifiers against the catalog.
RATE_LIMITED429More requests than the per-minute limit. `Retry-After` gives the seconds to wait.Wait for `Retry-After`, then retry with backoff.
INTERNAL500Unexpected 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_FAILED503The 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.
UNAVAILABLE501 / 503The 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_FAILED200Some 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.
Older endpoints may answer HTTP 200 with success: false and code REQUEST_FAILED for a missing item. Check success on every response.

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

NameDescription
X-Request-IdIdentifier of this request. Quote it in bug reports.
X-RateLimit-LimitRequests allowed in the current one-minute window.
X-RateLimit-RemainingRequests left in the current window.
X-RateLimit-ResetUnix time in seconds when the window resets.
Retry-AfterSent with HTTP 429: seconds to wait before retrying.
X-CacheHIT, 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

JavaScript
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.