the buckets, the headers, and how to behave
Rate limits
Every response carries the current state of the bucket it charged:
http
RateLimit-Limit: 600
RateLimit-Remaining: 587
RateLimit-Reset: 1789459620On a refusal you also get Retry-After, in seconds, and a rate_limited problem body.
The headers are on EVERY response, not just refusals — a client that can see its remaining budget can slow down before it is cut off. One that only learns at the 429 has no way to behave well.
The buckets
| bucket | limit |
|---|---|
| per credential, all requests | 600 / 5 min |
| per credential, writes | 120 / 5 min |
POST /oauth/token |
60 / min / IP |
POST /oauth/register |
10 / hour / IP |
| approval code lookup | 10 / min / user, 30 / min / IP |
POST /keys |
10 / day / user |
POST /webhooks |
20 / hour / user |
| per IP, at the edge | 20 req/s, burst 100 |
Windows are fixed, not sliding. A burst that straddles a boundary can spend two windows' worth; that is a deliberate trade for counters that survive a restart.
Behaving well
- Read
RateLimit-Remainingand slow down. Do not wait for the 429. - On 429, sleep
Retry-After. Not a guess, not immediately. - Back off exponentially on 5xx, with jitter, so a restart does not gather every client into one thundering retry.
- Use
updated_sincerather than re-reading everything. A full re-read on a timer is the usual reason an integration hits the limit at all. - Prefer a webhook to a tight poll — but keep a slow reconciliation poll, because inbound delivery is best-effort.
If you need more
The limits are tunable and currently generous relative to what real integrations use. If you are hitting one legitimately, say what you are building.