BillKit/Docs Console
Build & operate

Rate limits

Limits are per account, split between reads and writes, and reported on every response.

Updated Sep 13, 2026 · API version 2026-09-13

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.

BucketSustained rateBurst
Reads500 per minute100 requests
Writes100 per minute30 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.

HeaderValue
X-RateLimit-LimitThe sustained per-minute rate for the bucket that served this request.
X-RateLimit-RemainingTokens left in that bucket right now.
X-RateLimit-ResetUnix seconds at which the bucket will be full again.
Retry-AfterOn 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".

429 Too Many Requests
{
"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.