Rate Limits¶
The User API enforces per-credential rate limits to keep the platform stable and fair. Limits are applied per authenticated principal (the identity behind your token) — not per IP address — and scale with your workspace's plan.
Limits by plan¶
Every request against a /v1 endpoint counts toward a rolling 60-second window. When you exceed your limit, further requests in that window are rejected with 429 Too Many Requests until the window resets.
| Plan | Requests per minute |
|---|---|
| Free | 10 |
| Pro | 100 |
| Elite | 500 |
| Enterprise | 1,000 |
Limits are per principal
The limit is keyed on the identity that owns the credential, so all tokens acting as the same principal share one budget. Using multiple Personal Access Tokens does not multiply your limit.
Rate-limit headers¶
Every response includes headers describing your current budget, so you can throttle proactively instead of waiting for a 429:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the current window (your plan limit). |
X-RateLimit-Remaining | Requests remaining in the current window. |
X-RateLimit-Reset | Unix timestamp (seconds since epoch) when the window resets. |
When a request is throttled, the 429 response additionally includes:
| Header | Description |
|---|---|
Retry-After | Seconds to wait before retrying. This is the actual time to reset, not the full window, so honoring it waits only as long as necessary (minimum 1). |
The 429 response¶
A throttled request returns HTTP 429 with this body:
{
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Please try again later.",
"request_id": "abc123xyz"
}
Match on the machine-readable code (rate_limit_exceeded), not the human-readable message.
Distinguish from the events stream cap
The Server-Sent Events endpoint enforces a separate concurrent connection cap that also returns 429, but with code = too_many_requests. See User Events (SSE).
Handling throttling¶
The recommended pattern is to respect Retry-After and back off exponentially on repeated 429s:
import time
import requests
def request_with_backoff(session, method, url, **kwargs):
for attempt in range(5):
resp = session.request(method, url, **kwargs)
if resp.status_code != 429:
return resp
# Prefer the server's hint; fall back to exponential backoff.
retry_after = int(resp.headers.get("Retry-After", 2 ** attempt))
time.sleep(retry_after)
return resp # last response after exhausting retries
async function requestWithBackoff(url: string, init: RequestInit): Promise<Response> {
for (let attempt = 0; attempt < 5; attempt++) {
const resp = await fetch(url, init);
if (resp.status !== 429) return resp;
const retryAfter = Number(resp.headers.get("Retry-After") ?? 2 ** attempt);
await new Promise((r) => setTimeout(r, retryAfter * 1000));
}
return fetch(url, init);
}
Best practices¶
- Read the headers. Watch
X-RateLimit-Remainingand slow down before you hit zero. - Honor
Retry-After. It tells you exactly when the window resets. - Batch and paginate efficiently. Request larger pages (up to the pagination maximum) instead of many small requests.
- Prefer webhooks and events over polling. Workspace Webhooks and User Events (SSE) push changes to you, eliminating poll traffic entirely.
Next steps¶
- Errors — the standard error envelope and common status codes
- User Events (SSE) — real-time updates without polling
- Workspace Webhooks — push notifications to your own endpoint