Guides
Rate limits
Generous by design: a real integration, even a sync job paging through every contact, should never meet them. They exist to stop a runaway loop.
Per key
Each key has its own allowance, so one busy integration never slows another down. Allowances refill continuously.
| REST API | Bursts of 300 requests, refilling at 600 a minute |
| Agent inbox API | Bursts of 120 requests, refilling at 120 a minute, per agent key |
Over the allowance, a request is answered 429 rate_limited with a retry-after header: the whole number of seconds to wait.
HTTP/1.1 429 Too Many Requests
retry-after: 2
content-type: application/json
{
"error": "rate_limited",
"message": "Too many requests for this key. Try again in 2 seconds."
}Account limits on sending
Sending has limits of its own, set by the account's plan and checked however many keys you use. They protect your sending reputation as much as ours.
| Transactional email, per hour | Trial 100, Starter 2,000, Business 10,000 |
| Transactional email to one address | 20 a day |
| Campaigns and automations | One shared hourly allowance by plan |
| Test copies of a campaign | 30 an hour, to members of the account only |
| An AI agent | The daily limit you set on the agent |
An account limit is answered 429 too_many, and the message says which limit. A transactional message that was refused does not count towards either transactional limit.
Retrying
Wait for the seconds in retry-after, then try again; do not retry in a tight loop. For a send, keep the same idempotencyKey on every attempt so the message goes once.
async function call(url, init, attempt = 0) {
const res = await fetch(url, init);
if (res.status === 429 && attempt < 5) {
const seconds = Number(res.headers.get('retry-after') ?? 1);
await new Promise((r) => setTimeout(r, seconds * 1000));
return call(url, init, attempt + 1);
}
return res;
}Need more