Skip to content
Docs
Get API Access
Browse the Documentation

Rate Limits

A fixed one-minute window per key, reported on every response. Read the headers, handle 429 and back off correctly.

How the Window Works

  • Each key has a budget of requests per minute set by its plan: 60 on Developer and 300 on Pro. See Plans and limits.
  • The window is a fixed minute aligned to the clock (from :00 to :00 of the next minute, UTC), not a rolling sixty seconds. The count starts again at every minute boundary.
  • The count is per key and shared by every API server, so spreading requests across machines or regions does not raise it.
  • Every authenticated request to /v1 counts once: successes, 304 Not Modified answers, validation failures, 404s and the refused requests themselves. Opening a WebSocket connection counts too. Requests refused for a missing, unknown or inactive key (401, 403 forbidden) are not counted.

Headers on Every Response

HeaderMeaning
X-RateLimit-LimitRequests the key may make in the current window.
X-RateLimit-RemainingRequests left in the current window, never below 0.
X-RateLimit-ResetUnix time, in whole seconds, when the current window ends and the count starts again.
Retry-AfterOn a 429 only: whole seconds to wait, at least 1.
HTTP
HTTP/2 200
content-type: application/json; charset=utf-8
x-ratelimit-limit: 60
x-ratelimit-remaining: 42
x-ratelimit-reset: 1791727260

The headers are exposed to browsers through CORS, but the key should not be in a browser in the first place; read them on your server.

When You Go Over: 429

A request beyond the budget is answered 429 with a Retry-After header and the exact wait in error.retry_after_ms. The error is marked retryable.

JSON
{
  "status": {
    "timestamp": "2026-10-11T14:00:41.512Z",
    "elapsed_ms": 1,
    "error_code": 1005,
    "error_message": "The request rate limit was exceeded",
    "request_id": "req_7Hk2pQ9xLm"
  },
  "error": {
    "code": "rate_limited",
    "message": "The request rate limit was exceeded",
    "retryable": true,
    "retry_after_ms": 18488
  }
}

Retry with Backoff

Retry only what the API marks retryable: 429 rate_limited and 503 source_unavailable. Wait as long as the API says when it says (the retry_after_ms field, or the header), and otherwise back off exponentially with jitter. Cap the attempts, and never retry a 4xx other than 429: it will fail the same way.

const API = "https://api.cantoncap.cc";
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

export async function cantoncap(path, params = {}, attempts = 5) {
  const url = new URL(path, API);
  for (const [key, value] of Object.entries(params)) url.searchParams.set(key, String(value));

  for (let attempt = 1; ; attempt++) {
    const response = await fetch(url, {
      headers: { "X-API-Key": process.env.CANTONCAP_API_KEY },
    });
    if (response.status === 304) return null;
    const body = await response.json();
    if (response.ok) return body;

    const { error } = body;
    if (!error?.retryable || attempt >= attempts) {
      throw new Error(`${response.status} ${error?.code}: ${error?.message} (${body.status?.request_id})`);
    }
    // Use the API's own wait when it gives one, else exponential backoff with full jitter.
    const header = Number(response.headers.get("retry-after")) * 1000;
    const wait = error.retry_after_ms ?? (header || Math.random() * Math.min(30_000, 500 * 2 ** attempt));
    await sleep(wait);
  }
}

const { data } = await cantoncap("/v1/global/metrics/latest", { convert: "USD" });

Use the Budget Well

  • Batch. /v1/simple/price, /v1/assets/quotes/latest and /v1/assets/info take up to 100 asset IDs in one request; /v1/activity/markets/latest takes up to 100 market IDs.
  • Poll at the data's pace. Reference prices are recalculated about once a minute; polling a quote every second spends sixty requests on one value. Compare last_updated_at with the last value you stored.
  • Send the ETag back. Metadata and latest endpoints answer an unchanged representation with an empty 304. It still counts against the window, but it saves the transfer and the parsing. See Caching and ETags.
  • Cache on your side. One server-side cache in front of the API, shared by your users, keeps your request rate independent of your traffic.
  • Stream what changes often. One WebSocket connection replaces a polling loop for tickers, asset quotes, candles and source status. See Streaming.
  • Pace yourself. When X-RateLimit-Remaining runs low, wait until X-RateLimit-Reset rather than spending the rest of the minute on 429s.
Other Bounds
Page size and history depth are not rate limits: going past them answers 403 quota_exceeded, which is not retryable. Change the request instead. See Plans and limits.
Esc
navigateopenEscclose