Rate limits
Two independent 120-per-minute limits, one per IP and one per workspace, the stricter caps on exports, crawls and topics, and a backoff loop that behaves.
v1 enforces two rate limits. They are independent, and either can reject a request.
| Limit | Window | Scope |
|---|---|---|
| 120 requests | per minute | Per IP, checked before the key is looked up |
| 120 requests | per minute | Per workspace, checked once the key resolves |
The per-IP limit runs first, deliberately: it bounds floods of missing or invalid keys, which would otherwise reach a database lookup unthrottled. The per-workspace limit is what bounds one valid key sprayed across many machines.
So a single client from one address is effectively capped at 120 requests per minute; five servers sharing one key are still capped at 120 per minute in total.
Stricter per-endpoint limits
A few endpoints carry their own limit on top of the global two, because each request is expensive:
| Endpoint | Limit | Scope of the limit |
|---|---|---|
GET /conversations/export (CSV) | 10 / minute | per workspace |
GET /leads/export (CSV) | 10 / minute | per workspace |
POST …/sources/crawl (website crawl) | 10 / minute | per workspace |
POST /analytics/topics (LLM clustering) | 10 / minute | per workspace |
PUT/POST /reseller/custom-domain | 30 / minute | per IP |
POST /reseller/mail-config/test | 5 / hour | per organization |
POST /members/invite | 20 / hour | per workspace |
All of them return the same 429 as the global limits.
The response
{
"success": false,
"error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded." }
}HTTP status 429.
Backing off
Retry with exponential backoff and jitter, and cap the number of attempts.
async function getWithRetry(url, headers, attempts = 5) {
for (let attempt = 0; attempt < attempts; attempt += 1) {
const res = await fetch(url, { headers });
const body = await res.json();
if (body.success) return body.data;
if (body.error.code !== "RATE_LIMITED") {
throw new Error(`${body.error.code}: ${body.error.message}`);
}
// 1s, 2s, 4s, 8s … plus jitter, so parallel workers do not resynchronise.
const wait = 2 ** attempt * 1000 + Math.random() * 250;
await new Promise((resolve) => setTimeout(resolve, wait));
}
throw new Error("Still rate limited after retries.");
}Two things to avoid:
- Retrying immediately. The window is a minute; an instant retry just spends another request.
- Retrying anything else.
UNAUTHORIZEDandFORBIDDENwill not resolve themselves. See Errors.
Staying under the limit
- Use the maximum
limitof 200 when paging, so a given dataset costs fewer requests. See Pagination. - Sync on a schedule rather than polling in a loop. Nothing in v1 changes fast enough to reward a tight poll.
- For "tell me when something happens", use webhooks instead of polling — they push, and they cost you no requests.
Where to go next
Last updated