Core concepts
Rate limits
Per-key limits, the headers that report them, and how to back off.
Limits are counted per API key in a fixed window. They are generous enough that a well-behaved integration never sees one, and low enough that a runaway loop cannot take the service down for everyone else.
Every key gets 600 requests per 15-minute window, counted per key rather than per account — two integrations on one account do not spend each other's allowance, and one running hot does not throttle the other. | Enterprise | Agreed per contract | Agreed per contract |
Headers
Every response carries the current state of your window, whether or not you are near it.
HTTP/1.1 200 OK
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 597
X-RateLimit-Reset: 1786100460These come back on every response, not only on a 429 — you cannot pace against a budget you are only told about once you have already overrun it.
When the window is spent the API returns 429 with Retry-After in seconds.
A 503 means the limiter itself is down
If rate limiting is unavailable, this API answers 503 with Retry-After: 5 rather than letting the request through. An unmetered public endpoint is worse than a brief outage, and your client is a program that can wait five seconds.
/v1/meetings{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry in 12 seconds."
}
}Backing off
Read Retry-After and wait exactly that long. Retrying sooner counts against the next window and pushes the recovery further out.
async function call(url: string, attempt = 0): Promise<Response> {
const response = await fetch(url, { headers });
if (response.status !== 429 || attempt >= 5) return response;
const wait = Number(response.headers.get("Retry-After") ?? 1);
const jitter = Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, wait * 1000 + jitter));
return call(url, attempt + 1);
}Stop polling
Most 429s come from polling for a meeting that is still processing. A webhook tells you the moment it is ready, and costs no requests at all.