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
/v1counts once: successes,304 Not Modifiedanswers, 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
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests the key may make in the current window. |
X-RateLimit-Remaining | Requests left in the current window, never below 0. |
X-RateLimit-Reset | Unix time, in whole seconds, when the current window ends and the count starts again. |
Retry-After | On a 429 only: whole seconds to wait, at least 1. |
HTTP/2 200
content-type: application/json; charset=utf-8
x-ratelimit-limit: 60
x-ratelimit-remaining: 42
x-ratelimit-reset: 1791727260The 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.
{
"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/latestand/v1/assets/infotake up to 100 asset IDs in one request;/v1/activity/markets/latesttakes 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_atwith 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-Remainingruns low, wait untilX-RateLimit-Resetrather 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.