Limits are applied per API key.
Method | Limit |
|---|---|
| 1000 / minute |
| 60 / minute |
Burst | 2× the above, over a 10-second window |
If you need higher limits, contact us with your workspace id and the use case.
Response headers
Every response - success or error - carries the current state:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1746489660
X-RateLimit-Limit- the cap for the method class on this key.X-RateLimit-Remaining- how many calls you have left in the current window.X-RateLimit-Reset- unix seconds when the window resets.
On 429, we additionally send Retry-After: <seconds>.
When you exceed the limit
HTTP/1.1 429 Too Many Requests
Retry-After: 18
Content-Type: application/json
{
"error": {
"code": "rate_limited",
"message": "Rate limit exceeded.",
"details": { "limit": 60, "window": "1m", "retry_after_seconds": 18 },
"request_id": "req_..."
}
}
Pacing strategy
The cheap and correct approach: pre-pace using X-RateLimit-Remaining rather than waiting for the 429.
const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
const remaining = Number(res.headers.get("X-RateLimit-Remaining") ?? "0");
const reset = Number(res.headers.get("X-RateLimit-Reset") ?? "0");
if (remaining < 5) {
const wait = Math.max(0, reset * 1000 - Date.now());
await new Promise((r) => setTimeout(r, wait));
}
Retrying after a 429
Honour Retry-After literally - don't add jitter that beats it:
if (res.status === 429) {
const retryAfter = Number(res.headers.get("Retry-After") ?? "1");
await new Promise((r) => setTimeout(r, retryAfter * 1000));
// retry the same request
}
If you have multiple workers sharing one key, do not retry from every worker at the same instant. Add a small random jitter on top of Retry-After (e.g. 0–500ms) to spread the thundering herd.
Best practices
Use webhooks instead of polling list endpoints. See Webhooks.
Use the cursor's
has_moreflag, notwhile (true), when paginating.Batch across ids where an endpoint supports it.
POST /threads/bulk-updateandPOST /threads/bulk-deleteeach take up to 100 ids and cost one write, so a 500-thread cleanup spends five writes instead of 500.Batch writes at your end where the data model allows (e.g. one
PATCH /companies/{id}with five fields instead of five PATCHes).Share keys at the integration level, not per user. One key per "thing that calls us" is the sweet spot.
