Rate Limits
Rate limits keep the API responsive for every organization and stop a runaway script from affecting your colleagues' integrations. They are applied in layers, and a request must fit within every layer.
The limits
| Layer | Limit | Applies to |
|---|---|---|
| Per token, burst | 20 requests per second | Each token |
| Per token, sustained | 120 requests per minute | Each token |
| Per organization | 600 requests per minute | All of an organization's tokens together |
| Per address | 600 requests per minute | Every request from one IP address, before the token is checked, including the documentation endpoints. IPv6 addresses are counted per /64 network. |
| Network edge | 20 requests per second per IP address, with a short burst allowance | Applied before the request reaches the API |
The windows are sliding: a limit of 120 per minute means at most 120 requests in any 60-second span, not 120 per calendar minute. A request that is refused does not count against your quota.
Because the organization limit is shared, several busy integrations in one organization can reach it together even when each stays under its own token limit. Spread heavy jobs out, or schedule them at different times.
Most requests count as one. Downloading a document file (GET /documents/{id}/content) counts as 10 against the token and organization limits.
Rate limit headers
Authenticated responses report the window you are closest to exhausting:
| Header | Meaning |
|---|---|
RateLimit-Limit | The request limit of that window |
RateLimit-Remaining | Requests left in that window |
RateLimit-Reset | Seconds until a slot in that window frees up |
Retry-After | Sent with 429 only: seconds to wait before retrying |
HTTP/1.1 429 Too Many Requests
RateLimit-Limit: 120
RateLimit-Remaining: 0
RateLimit-Reset: 14
Retry-After: 14
Content-Type: application/json; charset=utf-8
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests. Retry after the time given in Retry-After."
},
"meta": { "request_id": "..." }
}
Failed-authentication lockout
To stop token guessing, an IP address (for IPv6, a /64 network) that sends more than 20 requests with an invalid token within 10 minutes is blocked for 15 minutes. During the block, any request from that address with a missing, malformed or invalid token receives 429 RATE_LIMITED with Retry-After set to the seconds remaining. Requests with a valid token are never blocked by it, so an integration platform, office network or CI service that shares one outbound address with someone else's misconfigured script keeps working. Only missing, malformed, unknown, revoked and expired tokens count towards the block. A real token refused for another reason, for example because its owner was deactivated or the API is switched off, does not.
The usual cause is a script that keeps running with a revoked or expired token. Stop on the first 401 AUTH_INVALID rather than retrying it.
Handling limits well
- Honour
Retry-After. On a429, wait at least that many seconds before the next request. - Pace yourself with
RateLimit-Remaining. When it gets low, slow down rather than waiting for a429. - Back off exponentially on repeated failures, with random jitter, so many workers do not retry at the same instant. For example, wait 1, 2, 4, 8 seconds and so on, up to a minute, adding a random fraction each time.
- Use the largest page size (
limit=100) for bulk reads, so you make fewer requests. - Do not poll more often than you need. For changes you need to hear about quickly, use webhooks instead of polling.
- Treat any
429the same way. A429from the network edge may not include the JSON body or the RateLimit headers; if there is noRetry-After, back off exponentially.
async function callApi(url, init, attempt = 0) {
const res = await fetch(url, init)
if (res.status !== 429 && res.status < 500) return res
if (attempt >= 6) return res
const retryAfter = Number(res.headers.get('retry-after'))
const backoff = Math.min(60, 2 ** attempt) + Math.random()
const waitSeconds = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : backoff
await new Promise((resolve) => setTimeout(resolve, waitSeconds * 1000))
return callApi(url, init, attempt + 1)
}
Retry a write only with an idempotency key, so a request that did succeed is not applied twice.