BillKit/DocsConsole
Build & operate

Rate limits

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

Updated Sep 24, 2026·OpenAPI v1.1.0

The limits

Two token buckets per account, one for reads and one for writes. The route’s HTTP method decides which: POST, PATCH, PUT and DELETE are writes and everything else is a read. A POST that only reads, such as /v1/coupons/validate, still spends a write token. The buckets are per account and mode, not per key: live and test each have their own pair, so a test-mode load test cannot rate limit your live traffic, but every key in one mode shares that mode’s budget.

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. Each API server keeps its own buckets, so you may sometimes get more than this. Design to the table, not to what you observe.

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. 60 on any response to a request that did not authenticate, a 401 included, because those are metered by the pre-authentication limit below.
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, the seconds until one token is available. Other retryable answers carry it too, such as a 503; see Errors.

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 wait out a Retry-After of up to 30 seconds and retry; a longer one is raised to your code.

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. A request that does not authenticate keeps its token, and its response, whether a 401 or a 429, carries this bucket’s headers rather than your key’s. 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 and spends one read token. It needs the data:export scope. See Events and audit log.