Rate limits
Limits are per account, split between reads and writes, and reported on every response.
The limits
Two token buckets per account, one for reads and one for writes. The route’s HTTP method decides
which: GET and HEAD are reads, POST, PATCH, PUT and DELETE are writes. The buckets are
per account, not per mode, so a test-mode load test spends the same budget your live traffic does.
| Bucket | Sustained rate | Burst |
|---|---|---|
| Reads | 500 per minute | 100 requests |
| Writes | 100 per minute | 30 requests |
The burst column is the bucket size, so it is what you can spend at once from full. It refills continuously at the sustained rate rather than resetting on a schedule, which means a client that paces itself under the sustained rate never runs out no matter how long it runs.
Headers
Every response that passed the limiter carries three headers, and a 429 adds a fourth.
| Header | Value |
|---|---|
| X-RateLimit-Limit | The sustained per-minute rate for the bucket that served this request. |
| X-RateLimit-Remaining | Tokens left in that bucket right now. |
| X-RateLimit-Reset | Unix seconds at which the bucket will be full again. |
| Retry-After | On a 429 only. Seconds until one token is available. |
All four are exposed to browsers through CORS, so a client-side integration can read them.
A 429 uses the standard error envelope with code: "rate_limited" and
type: "rate_limit_error".
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Too many requests. Retry after 3 seconds.",
"request_id": "9f4c2b1ad7e84c0fa1b3e6d590c72b48"
}
} Backing off
Honour Retry-After, then add jitter. A fleet of workers that all retry after exactly three
seconds arrives together and gets rate limited together. The official SDKs retry with jittered
exponential backoff already.
Pair backoff with an Idempotency-Key. A retried write that carries
one cannot duplicate work, which turns “did that go through?” from a question into a non-issue.
The pre-authentication limit
Before your key is looked up, a separate bucket meters unauthenticated attempts per client address and route: 60 per minute, burst 20. It exists so that guessing at keys costs the attacker budget instead of costing the database work.
A request that authenticates successfully refunds the token it took, so legitimate traffic never
touches this limit. If you are seeing 429 on your very first call of the day, the cause is
almost always a wrong key rather than volume.
Staying under
Prefer one list call over a loop of retrieves. GET /v1/subscriptions takes limit up to 100
along with customer_id and status filters, which usually replaces a page of individual
lookups.
Read state from webhooks rather than polling. Polling GET /v1/subscriptions on a timer is the
most common way an integration ends up rate limited, and it is also slower than the event that
would have told you.
For a whole-account snapshot, GET /v1/tenant/export streams everything in one request. It needs
the data:export scope.
BillKit