Best Practices5 min

Rate Limiting

Understand the OAuth token rate-limiting model, why it is designed this way, and what your integration should implement.

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/accounts
  • GET /api/public/v1/invoices
  • GET /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-After header 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

To work reliably with the API, your client should:

  1. Centralize all outgoing requests per access token
  2. Monitor RateLimit and use X-RateLimit-Remaining as a legacy fallback
  3. On 429, wait for Retry-After before retrying
  4. 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

Support

Questions about OAuth token rate limiting?
Contact us via the website contact form.