Rate limits
Requests are counted per key, per minute. The budget comes from the workspace's plan.
| Plan | Requests / minute | API keys |
|---|---|---|
| Starter | 60 | 2 |
| Growth | 300 | 10 |
| Enterprise | 1000 | Unlimited |
| Custom | 1000 unless agreed otherwise | Unlimited |
Per key, not per workspace — so a misbehaving integration exhausts its own budget rather than starving the others. Splitting your integrations across separate keys is worth doing for that reason alone.
The budget is one number for the key, enforced across every server that
handles your requests: the count is shared, so which instance answers a call
never changes what you are allowed. It refills continuously — a request's
worth of budget comes back every 60 / limit seconds — rather than resetting
on a clock boundary, which is why RateLimit-Remaining can climb between
calls.
Reading the headers
Every response carries the current state, so you never have to guess:
| Header | Meaning |
|---|---|
RateLimit-Limit | Requests allowed in the window. |
RateLimit-Remaining | Requests left in it. |
RateLimit-Reset | Seconds until the window resets. |
Past the limit you get 429 with code: "rate_limit_exceeded" and a
Retry-After header in seconds. Honour it. Retrying immediately just burns
the next window too.
async function call(path, init = {}) {
const res = await fetch(`${BASE}${path}`, init);
if (res.status === 429) {
const wait = Number(res.headers.get('retry-after') ?? 60);
await sleep(wait * 1000);
return call(path, init);
}
// Slow down before you are refused, not after.
if (Number(res.headers.get('ratelimit-remaining') ?? 99) < 5) {
await sleep(1000);
}
return res;
}
Staying inside the budget
Page with limit=100. The maximum page size is the same one request as the
default of 25, and it does four times the work.
Sync incrementally. updated_since turns a nightly full refetch into a
handful of calls. See Pagination.
Subscribe instead of polling. A webhook tells you the moment something changes and costs you no requests at all. Polling every minute to catch a change that happens twice a day spends your entire budget learning nothing.
Do not parallelise a sync to go faster. Ten concurrent workers on a Starter key exhaust sixty requests in a few seconds and then all wait together. One sequential loop that respects the headers finishes sooner.
If the budget is genuinely too small
A per-key override can be set for a specific integration without changing the whole plan. If a legitimate workload does not fit — a first import of many years of attendance, say — talk to us rather than engineering around it; a one-off backfill is a different problem from a steady-state limit.