# Rate Limits

Sliding-window rate limits and how to read the X-RateLimit headers and 429 responses.

The API applies sliding-window rate limits before any authentication-aware routes. Every response carries the current usage, and exceeding the limit returns `429 Too Many Requests` with a `Retry-After` header.

## Policies

| Caller                                                     | Limit               |
| ---------------------------------------------------------- | ------------------- |
| Authenticated actor (distributor / partner / organization) | 5 requests / second |
| Unauthenticated (per IP)                                   | 1 request / second  |

Rate-limit keys are derived from the JWT or API key actor for authenticated requests, and from the client IP otherwise.

### Excluded paths

These paths are not rate-limited so monitoring and documentation always work:

* `GET /healthz`
* `GET /readiness`
* All paths under `/docs/` (OpenAPI spec and HTML docs)

## Response headers

Every API response includes:

| Header                  | Meaning                                          |
| ----------------------- | ------------------------------------------------ |
| `X-RateLimit-Limit`     | Maximum requests allowed in the window.          |
| `X-RateLimit-Remaining` | Requests remaining in the current window.        |
| `X-RateLimit-Reset`     | Unix timestamp (seconds) when the window resets. |

On `429 Too Many Requests` responses you also get:

| Header        | Meaning                                    |
| ------------- | ------------------------------------------ |
| `Retry-After` | Number of seconds to wait before retrying. |

## Example: under the limit

```http
GET https://api.benjipays.com/v2/whoami
Authorization: Bearer YOUR_ACCESS_TOKEN
```

```http
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 4
X-RateLimit-Reset: 1704067201
```

## Example: rate limit exceeded

```http
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1704067202
Retry-After: 1
```

Body:

```json
{
  "type": "https://api.benjipays.com/problems/too_many_requests",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Please retry after the specified time.",
  "instance": "/v2/whoami",
  "retry_after_seconds": 1
}
```

## Parsing the headers

**Node.js**

```javascript
const axios = require('axios');

async function makeRequest() {
  try {
    const response = await axios.get('https://api.benjipays.com/v2/whoami', {
      headers: { Authorization: 'Bearer YOUR_ACCESS_TOKEN' }
    });
    const limit = parseInt(response.headers['x-ratelimit-limit'], 10);
    const remaining = parseInt(response.headers['x-ratelimit-remaining'], 10);
    const reset = parseInt(response.headers['x-ratelimit-reset'], 10);
    console.log(`Rate limit: ${remaining}/${limit} remaining`);
    console.log(`Resets at: ${new Date(reset * 1000).toISOString()}`);
    return response.data;
  } catch (err) {
    if (err.response?.status === 429) {
      const retryAfter = parseInt(err.response.headers['retry-after'], 10);
      console.log(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
    }
    throw err;
  }
}
```

**Python**

```python
import requests, time

def make_request():
    r = requests.get(
        'https://api.benjipays.com/v2/whoami',
        headers={'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}
    )
    if r.status_code == 429:
        time.sleep(int(r.headers.get('Retry-After', '1')))
        return  # caller retries
    r.raise_for_status()
    return r.json()
```

## Best practices

* Inspect `X-RateLimit-Remaining` on every response and back off proactively.
* On `429`, respect `Retry-After` exactly — don't retry sooner.
* Use exponential backoff if you see repeated `429`s after the suggested wait.
* Cache responses where the data tolerance allows it.
* Don't try to evade limits with multiple IPs or accounts — limits are per actor on authenticated traffic.
* HTTP headers are case-insensitive. Parse them as such.