Rate Limits
BillerAPI enforces rate limits to ensure fair usage and platform stability. A default limit covers the API data plane, with a few sensitive auth and security endpoints carrying tighter per-route limits. All limits are applied per API key, so your traffic is never affected by other developers.
The limits
| Scope | Limit | Window |
|---|---|---|
| Default (all data-plane endpoints, per API key) | 100 requests | Per 60 seconds |
| Sensitive auth/security endpoints (sign-in, password reset, email verification, sign-up, etc.) | 5–10 requests | Per 60 seconds |
Each window is a fixed 60-second window that resets once it elapses. The default 100/60s limit applies to the API data plane and is not split by read vs. write. A small set of sensitive authentication and security endpoints (such as sign-in, password reset, and email verification) carry tighter per-route limits to protect your account. The limits are the same in sandbox and production.
Requests are counted per API key (your client ID). Requests that arrive without credentials are counted per source IP address instead. If you need a higher limit, contact us at support@billerapi.com.
Rate limit headers
Every response — not just 429s — carries your current budget so you can throttle yourself before you ever hit the limit:
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Maximum requests allowed in the window (currently 100). |
| X-RateLimit-Remaining | Requests you have left in the current window. |
| X-RateLimit-Reset | Seconds until the current window resets and your budget refills. |
| Retry-After | Only on a 429 — seconds to wait before retrying. |
A successful response looks like this:
When you exceed the limit
The API returns 429 Too Many Requests with the RATE_LIMITED error code. The seconds to wait before retrying are surfaced two ways: the Retry-After response header and a numeric retry_after field in the JSON error body. Prefer the body field; fall back to the header.
Quote the request_id in any support request. See Error Handling for the full error envelope.
Backoff strategy
When you get a 429, wait the number of seconds in retry_after (or the Retry-After header) before retrying. If neither is present, fall back to exponential backoff with jitter. Never retry immediately.
Respect retry_after, with exponential-backoff fallback
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fetch(url, options);
if (response.status !== 429) {
return response;
}
if (attempt === maxRetries) {
throw new Error('Max retries exceeded');
}
// Prefer the retry_after body field, then the Retry-After header,
// then exponential backoff.
let delaySeconds;
try {
const body = await response.clone().json();
delaySeconds = typeof body.retry_after === 'number' ? body.retry_after : undefined;
} catch {
delaySeconds = undefined;
}
if (delaySeconds === undefined) {
const header = response.headers.get('Retry-After');
delaySeconds = header ? parseInt(header, 10) : Math.min(Math.pow(2, attempt), 30);
}
// Add jitter to prevent thundering herd.
const jitter = Math.random() * 1000;
await new Promise(resolve => setTimeout(resolve, delaySeconds * 1000 + jitter));
}
}Best practices
- 1.Watch the headers — read
X-RateLimit-Remainingon every response and slow down as it approaches zero, rather than waiting for a 429. - 2.Use webhooks instead of polling — subscribe to events like
bill.createdinstead of polling for new bills. - 3.Cache and batch — biller metadata and account details change infrequently; cache them, and fetch multiple records in a single paginated request instead of one-by-one.
- 4.Always back off — never retry immediately after a 429. Wait at least the
retry_afterduration.
Related
- Error Handling — error response format and codes
- Authentication — API key and bearer token auth
- Webhooks — event-driven alternative to polling