The limit
Each key can make 60 requests a minute, whatever they ask for. The limit applies to each key separately, so two apps with their own keys don’t slow each other down. Credits, by contrast, are shared by all of an account’s keys (Credits).
Rate limit headers
Every response to a request with a key includes these headers:
| Header | Value |
|---|---|
X-RateLimit-Limit | The requests a key can make in one window: 60 |
X-RateLimit-Remaining | The requests left in the current window |
X-RateLimit-Reset | When the current window ends, in Unix seconds |
Exceeding the limit
A request over the limit returns 429 with the code rate_limited and a Retry-After header that gives the seconds to wait. The request costs nothing.
HTTP/2 429
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1762866060
Retry-After: 12
{"error": {"code": "rate_limited", …}}Note
Several limits return 429, so branch on the error’s code, not only its status. quota_exceeded, key_limit_reached and scan_limit_reached mean a monthly or daily limit is used up: retrying fails the same way until the limit resets.
When to retry
| Status | Code | Retry? |
|---|---|---|
| 409 | idempotency_key_in_use | Yes, with backoff |
| 429 | rate_limited | Yes, after Retry-After |
| 429 | quota_exceeded | No |
| 429 | key_limit_reached | No |
| 429 | scan_limit_reached | No |
| 500 | internal_error | Yes, with backoff |
| 503 | capacity | Yes, after Retry-After |
| 4xx | Any other | No. Change the request first; the message says what to change |
- Back off between attempts: wait 1, 2, 4 and 8 seconds, with a little random jitter, for up to five attempts. When the response has a
Retry-Afterheader, wait that long instead. - Polling a scan is free and doesn’t use credits. Read its
links.selfas often asRetry-Afterasks, about every 3 seconds. - Send the same
Idempotency-Keywith every attempt of a POST request, so a retry never starts or charges for anything twice (Idempotency).
The following function retries a request this way:
const RETRYABLE = new Set([
'idempotency_key_in_use',
'rate_limited',
'capacity',
'internal_error',
]);
async function callApi(path, init = {}, tries = 5) {
// Send the same key with every try of a POST, so a retry never starts
// or charges for a second scan.
const idempotency =
init.method === 'POST' ? { 'Idempotency-Key': crypto.randomUUID() } : {};
for (let attempt = 1; ; attempt += 1) {
const response = await fetch(`https://api.checkanysite.com/v1${path}`, {
...init,
headers: {
Authorization: `Bearer ${process.env.CHECKANYSITE_API_KEY}`,
...idempotency,
...init.headers,
},
});
if (response.ok) return response.json();
const { error } = await response.json();
if (!RETRYABLE.has(error.code) || attempt === tries) {
throw new Error(`${error.code}: ${error.message} (${error.requestId})`);
}
// Wait as long as Retry-After asks, or back off: 1, 2, 4… seconds.
const retryAfter = Number(response.headers.get('retry-after'));
const seconds = retryAfter || 2 ** (attempt - 1);
const delay = seconds * 1000 + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
const site = await callApi('/sites/wikipedia.org?checks=ai');The Python SDK retries like this automatically.