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 /healthzGET /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
GET https://api.benjipays.com/v2/whoami
Authorization: Bearer YOUR_ACCESS_TOKENHTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 4
X-RateLimit-Reset: 1704067201Example: rate limit exceeded
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: 1Body:
{
"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
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
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-Remainingon every response and back off proactively. - On
429, respectRetry-Afterexactly — don't retry sooner. - Use exponential backoff if you see repeated
429s 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.
