---
title: "Rate Limiting"
description: "Understand the OAuth token rate-limiting model, why it is designed this way, and what your integration should implement."
canonical: https://developer.billabex.com/en/guides/rate-limiting/
lang: en
alternate: https://developer.billabex.com/fr/guides/limites-de-debit/
last-updated: 2026-09-17
---

# Rate Limiting

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

Source: https://developer.billabex.com/en/guides/rate-limiting/
Language: English (en)
French version: https://developer.billabex.com/fr/guides/limites-de-debit/

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](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-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
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:

```http
GET /api/public/v1/accounts HTTP/1.1

HTTP/1.1 401 Unauthorized
RateLimit-Policy: "public-api-anonymous";q=60;w=60, "public-api";q=300;w=60
RateLimit: "public-api-anonymous";r=59;t=47
WWW-Authenticate: Bearer resource_metadata="https://next.billabex.com/.well-known/oauth-protected-resource"
```

A call to `/api/public/v1/*` that sends no `Authorization: Bearer` header is counted against the
[anonymous quota](#the-anonymous-quota), so its `401` carries a live `RateLimit` for that quota.
`RateLimit-Policy` lists the token quota next to it: that is the budget you get once authenticated,
and `RateLimit` reports it from your first authenticated call.

A call that sends a token, even an expired or revoked one, is not counted anonymously. It keeps its
plain `401`, so your client refreshes the token instead of backing off. The `401` of `POST /mcp`,
which starts the OAuth flow of MCP clients, carries `RateLimit-Policy` only.

## The Anonymous Quota

Two endpoints answer without any credential, and are counted **per IP address** rather than per
token:

| Endpoint                  | What it is                                             |
| ------------------------- | ------------------------------------------------------ |
| `/api/public/v1/status`   | The discovery document of the public API                |
| `/mcp/tools`              | The catalogue of MCP tools, with their JSON Schemas     |

| Aspect            | Behavior                                |
| ----------------- | --------------------------------------- |
| Scope             | Per IP address                          |
| Policy name       | `public-api-anonymous`                  |
| Window duration   | 60 seconds                              |
| Limit             | 60 requests per window                  |
| Exceeded behavior | `429 Too Many Requests` + `Retry-After` |

The same quota also counts every call to `/api/public/v1/*` made **without a Bearer token**. Past
60 such calls a minute from one IP address, the API answers `429 Too Many Requests` with
`Retry-After` before it looks at authentication. Authenticated calls never draw from it.

These calls carry a policy name of their own on purpose. Sharing `"public-api"` would tell you that the
figure you read before authenticating is the budget you get afterwards, and it is not: one is
counted per IP, the other per token.

Because they need no credential, they are the shortest way to see the headers live, `RateLimit`
included:

```http
GET /api/public/v1/status HTTP/1.1

HTTP/1.1 200 OK
RateLimit-Policy: "public-api-anonymous";q=60;w=60
RateLimit: "public-api-anonymous";r=59;t=47
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1738859285
```

The body of that response repeats both quotas as data, so a client can read them without parsing
headers at all.

## 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
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:

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)

```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](/en/guides/getting-started/)**: End-to-end integration flow
- **[OAuth Authentication](/en/guides/oauth/)**: Token lifecycle and OAuth flow
- **[JSON Errors](/en/guides/errors/)**: Stable error codes and retry decisions
- **[Pagination](/en/guides/pagination/)**: Reducing request volume
- **[API Reference](/en/api-reference/)**: Endpoint details and examples

## Support

Questions about OAuth token rate limiting?  
Contact us via the [website contact form](https://billabex.com).

---

Billabex developer portal. OpenAPI specification: https://developer.billabex.com/openapi.json.
Agent instructions: https://developer.billabex.com/llms.txt. Complete documentation: https://developer.billabex.com/llms-full.txt.
