Rate limits
Each API key gets 120 requests per 60-second window. The limit is per key, so splitting traffic across keys splits the budget.
The limit
| Budget | 120 requests per 60 seconds. |
|---|---|
| Counted against | The API key, not the organization or the IP address. |
| Window | Fixed 60-second buckets, not a rolling average. The counter resets at the boundary in X-RateLimit-Reset. |
| Scope | All eight public endpoints share one counter. Reads and writes are not budgeted separately. |
Response headers
Every response carries your current position in the window, so you do not have to guess.
Response headers
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117
X-RateLimit-Reset: 1771406400 X-RateLimit-Limit | Requests allowed in a window. |
|---|---|
X-RateLimit-Remaining | Requests left in the current window. |
X-RateLimit-Reset | Unix timestamp when the window resets and the counter returns to zero. |
Retry-After | Seconds to wait. Sent only on HTTP 429. |
When you exceed the limit
You get HTTP 429 with RATE_LIMIT_EXCEEDED and a Retry-After header. Honour the header rather than retrying on a fixed timer.
429 Too Many Requests
HTTP/1.1 429 Too Many Requests
Retry-After: 18
X-RateLimit-Limit: 120
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1771406400
{
"detail": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Try again later.",
"suggestion": "Maximum 120 requests per 60s window. Retry after 18s."
}
} Python
import time
def with_retry(send, attempts=5):
"""Retry on 429 using Retry-After, with an exponential fallback."""
for attempt in range(attempts):
response = send()
if response.status_code != 429:
return response
wait = response.headers.get("Retry-After")
time.sleep(int(wait) if wait else 2 ** attempt)
raise RuntimeError("rate limited after %d attempts" % attempts) Node.js
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
/** Retry on 429 using Retry-After, with an exponential fallback. */
export async function withRetry(send, attempts = 5) {
for (let attempt = 0; attempt < attempts; attempt += 1) {
const response = await send();
if (response.status !== 429) return response;
const wait = response.headers.get("Retry-After");
await sleep(wait ? Number(wait) * 1000 : 2 ** attempt * 1000);
}
throw new Error(`rate limited after ${attempts} attempts`);
} Staying under the limit
- Cache what does not move. Workspace ids and gateway credentials are stable between plan changes; fetch them once and reuse them.
- Prefer one wide call to many narrow ones.
GET /proxiesreturns every assignment in the organization, so you rarely need per-workspace calls in a loop. - Poll on a timer, not in a tight loop. When you are waiting for a workspace to finish provisioning, a few seconds between polls is plenty.
- Give each service its own key so a noisy batch job cannot starve your user-facing traffic.
Need a higher limit?
Tell us what you are building and the shape of the traffic. Limits are configurable and we would rather raise yours than have you work around it.