Rate limits
Each API key has a maximum rate, counted over a fixed one-second window. Counting is shared across all instances of the service: the limit is the same whichever server answers.
The applied rate is the key's own rate if it has one, otherwise the workspace plan's (Pro: 25 requests per second, Business: 50). GET /v1/workspace returns it in limits.requests_per_second.
Headers#
Every authenticated response carries the state of the current window:
RateLimit-Limit: 25
RateLimit-Remaining: 19
RateLimit-Reset: 1
RateLimit-Policy: 25;w=1| Header | Meaning |
|---|---|
RateLimit-Limit |
Requests allowed per window. |
RateLimit-Remaining |
Requests left in the current window. |
RateLimit-Reset |
Seconds until the next window (always 1). |
RateLimit-Policy |
Applied policy: <limit>;w=<window in seconds>. |
Going over the limit#
Past the limit, the API answers 429 with Retry-After: 1:
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded: 25 requests per second per API key.",
"details": { "limit": 25, "window_seconds": 1 }
}
}Wait for the Retry-After duration, then retry. Do not tell rate_limited (rate, wait a moment) from daily_quota_exceeded (daily quota, wait until midnight UTC) by status alone: read the code.
Retrying properly#
async function call(url, init = {}, attempt = 0) {
const res = await fetch(url, {
...init,
headers: { Authorization: `Bearer ${process.env.AICSCAN_API_KEY}`, ...init.headers },
});
if (res.status === 429 && attempt < 5) {
const body = await res.json();
if (body.error.code === 'daily_quota_exceeded') throw new Error('daily quota exceeded');
const wait = Number(res.headers.get('Retry-After') ?? 1) * 1000;
await new Promise((r) => setTimeout(r, wait + Math.random() * 250));
return call(url, init, attempt + 1);
}
return res;
}import os, random, time
import requests
def call(method, url, attempt=0, **kwargs):
headers = {"Authorization": f"Bearer {os.environ['AICSCAN_API_KEY']}", **kwargs.pop("headers", {})}
resp = requests.request(method, url, headers=headers, timeout=10, **kwargs)
if resp.status_code == 429 and attempt < 5:
if resp.json()["error"]["code"] == "daily_quota_exceeded":
raise RuntimeError("daily quota exceeded")
time.sleep(int(resp.headers.get("Retry-After", "1")) + random.random() / 4)
return call(method, url, attempt + 1, headers=headers, **kwargs)
return respFor a write (POST), send the same Idempotency-Key on every attempt: a retry never creates a duplicate.
Tips#
- Rather than polling
GET /v1/matches, receive alerts by webhook or on the real-time stream. - To walk a long history, use
limit=100: a page of 100 items counts as a single call. - Spread bulk jobs: a few requests per second are enough to go through thousands of items.