This guide explains how rate limiting works on the Billabex Public API
(/api/public/v1/*) and how your client should behave to integrate
reliably and predictably.
Overview
Rate limiting is enforced per OAuth Bearer access token.
This model is designed to provide clear and deterministic behavior for integrations:
- One token = one shared quota across all public endpoints
- Tokens are isolated from each other
- Sliding window enforcement smooths short traffic bursts
If multiple systems or threads use the same token, they also share the same rate limit.
Rate-Limiting Model
| Aspect | Behavior |
|---|---|
| Scope | Per OAuth access token |
| Window type | Sliding window |
| Window duration | 60 seconds |
| Limit | 300 requests per window |
| Applies to | All OAuth-protected public endpoints |
| Exceeded behavior | 429 Too Many Requests + Retry-After |
Why Per-Token Limiting?
This design choice ensures:
- Fairness between independent integrations
- Isolation between tokens
- Predictable capacity planning on the client side
What this means for you
- Treat each access token as a shared, limited resource
- Centralize outgoing requests per token
- Avoid uncontrolled concurrency using the same token
Where Rate Limiting Applies
This model applies to all OAuth-protected endpoints under:
/api/public/v1/*
Examples:
GET /api/public/v1/accountsGET /api/public/v1/invoicesGET /api/public/v1/organizations
Token Endpoint Rate Limiting
In addition to the per-token rate limiting on API endpoints, the token endpoint (POST /api/oauth/token) has a dedicated rate limit to prevent abuse.
| Aspect | Behavior |
|---|---|
| Scope | Per IP address |
| Limit | 10 requests per 60 seconds |
| Applies to | /api/oauth/token (all grants) |
This limit applies to:
- Authorization code exchange
- Refresh token grants
This prevents malicious actors from generating excessive tokens to bypass per-token rate limits.
Its responses carry the same headers as the ones below, under the "auth" policy name instead of "public-api": it is a separate quota, counted per IP rather than per token.
Rate Limit Response Headers
Every rate-limited response includes the current
IETF RateLimit draft headers
and the legacy X-RateLimit-* headers for compatibility.
| Header | Description |
|---|---|
RateLimit-Policy |
Named quota policy, total quota, and window in seconds |
RateLimit |
Remaining quota and effective window in seconds |
X-RateLimit-Limit |
Legacy maximum requests allowed per window |
X-RateLimit-Remaining |
Legacy requests remaining in the current window |
X-RateLimit-Reset |
Legacy Unix timestamp in seconds when the window resets |
Example Response
HTTP/1.1 200 OK
RateLimit-Policy: "public-api";q=300;w=60
RateLimit: "public-api";r=258;t=12
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 258
X-RateLimit-Reset: 1738859285
Reading the Policy Before You Have a Token
RateLimit-Policy is static and public: it states the quota, not how much of it you have left.
It is therefore returned on every response from /api/public/v1/* and /mcp, including the
401 you get before authenticating. You can size your throttling from a single unauthenticated
request:
GET /api/public/v1/accounts HTTP/1.1
HTTP/1.1 401 Unauthorized
RateLimit-Policy: "public-api";q=300;w=60
WWW-Authenticate: Bearer resource_metadata="[baseURL]/.well-known/oauth-protected-resource"
RateLimit, which carries your remaining quota, only appears once the request is authenticated:
there is no per-caller counter before that.
When the Limit Is Exceeded (429)
When the quota is exhausted, the API responds with:
- HTTP status
429 Too Many Requests - A
Retry-Afterheader indicating when it is safe to retry
Example 429 Response
HTTP/1.1 429 Too Many Requests
RateLimit-Policy: "public-api";q=300;w=60
RateLimit: "public-api";r=0;t=12
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1738859285
Retry-After: 12
Content-Type: application/json
{
"message": "Too many requests",
"code": "HTTP_429",
"frontMessage": "Too many requests",
"timestamp": "2026-08-24T18:09:55.818Z",
"path": "/api/public/v1/invoices",
"type": "http",
"method": "GET",
"resolution": "Wait for the Retry-After delay before retrying."
}
Important
- Always respect
Retry-After - Do not retry immediately or use hardcoded delays
- Repeated violations may lead to additional protections
Recommended Client Behavior
To work reliably with the API, your client should:
- Centralize all outgoing requests per access token
- Monitor
RateLimitand useX-RateLimit-Remainingas a legacy fallback - On
429, wait forRetry-Afterbefore retrying - Cap retries and surface errors when limits are repeatedly hit
Reference Retry Pattern (JavaScript)
async function fetchWithTokenRateLimit(url, options = {}, maxRetries = 3) {
let attempt = 0;
while (attempt <= maxRetries) {
const response = await fetch(url, options);
// Success or non-rate-limit error
if (response.status !== 429) {
return response;
}
const retryAfterSeconds = Number(
response.headers.get('Retry-After') || '1',
);
// Safety floor to avoid immediate retries
const waitMs = Math.max(1000, retryAfterSeconds * 1000);
if (attempt === maxRetries) {
return response;
}
await new Promise((resolve) => setTimeout(resolve, waitMs));
attempt += 1;
}
}
Best Practices
- Prefer pagination and batching to reduce request count
- Use a per-token request queue or throttler
- Keep concurrency controlled, especially for write-heavy flows
- Avoid large parallel bursts using the same token
- If you rotate tokens, remember that quotas are tracked per token
Next Steps
- Getting Started: End-to-end integration flow
- OAuth Authentication: Token lifecycle and OAuth flow
- JSON Errors: Stable error codes and retry decisions
- Pagination: Reducing request volume
- API Reference: Endpoint details and examples
Support
Questions about OAuth token rate limiting?
Contact us via the website contact form.