Skip to main content

Rate limits

Requests are counted per key, per minute. The budget comes from the workspace's plan.

PlanRequests / minuteAPI keys
Starter602
Growth30010
Enterprise1000Unlimited
Custom1000 unless agreed otherwiseUnlimited

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:

HeaderMeaning
RateLimit-LimitRequests allowed in the window.
RateLimit-RemainingRequests left in it.
RateLimit-ResetSeconds 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.