Skip to content

Rate limits

Limits are per API client, not per IP — one integration’s traffic never starves another’s.

Budget Applies to
300 / min reads (GET, HEAD)
120 / min writes (POST, PUT, PATCH, DELETE)
60 / min the binary upload and download endpoints, on top of the above

The upload budget is a token bucket with a burst of 20, and it stacks: an upload consumes both it and the write budget, and both must clear. It sits below the write budget deliberately, so it is the one that binds first on an upload.

Every response carries the current state, in IETF draft-7 format:

RateLimit-Limit: 300
RateLimit-Remaining: 297
RateLimit-Reset: 42

Reset is seconds until the window refills. Postman and Insomnia surface these natively.

On exceed: 429 TOO_MANY_REQUESTS with a Retry-After header. Honour it. Retrying earlier consumes budget you do not have and extends the block.

For a migration or a nightly sync, drive the loop from the headers rather than from a fixed sleep:

r = session.post(url, headers=auth, data=payload)
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "5")))
continue # re-send the same request
remaining = int(r.headers.get("RateLimit-Remaining", "1000"))
if remaining < 20: # ease off before the wall
time.sleep(int(r.headers.get("RateLimit-Reset", "5")))

A fixed sleep is either slower than it needs to be or still hits the wall, because it cannot know what else is spending the same budget.

At 120 uploads a minute a corpus of 40,000 documents moves in about six hours — a working day, unattended. If you need it faster, ask: the upload budget is raisable per API client, and a one-off migration is exactly what that exists for. It is a support action, not a code change.

The GET /v1/openapi.json contract document is unauthenticated and outside these budgets — fetch it in CI to diff the contract, without spending a key’s allowance.