Quotas and limits
Send quota
Every plan includes a monthly email allowance, enforced per team. When the allowance is reached the API answers 402 Payment Required with quota_exceeded. Example response:
{
"error": {
"code": "quota_exceeded",
"message": "monthly send quota exceeded"
}
}
How the window is measured depends on the plan:
- Free plans — a calendar month: the allowance resets on the first of each month.
- Paid plans — the current Stripe billing period: it resets on each invoice.
Two semantics matter when you send in bulk:
- Whole-batch up-front check.
POST /v1/emails/batchis validated against the quota before anything is stored: a batch that would not fit is rejected wholesale, so a 402 never leaves a half-sent batch. - Idempotent replays are exempt. Re-sending with a previously stored
idempotency_keyreturns the original row without consuming quota, so retrying after a timeout never double-counts.
Contact quota
Contact storage is also allowance-based. Bulk upserts enforce it per item: contacts that do not fit come back in the item results with 402 quota_exceeded, while the items that fit are stored normally — a contact-quota failure never aborts the whole bulk call.
Limits
| Limit | Value |
|---|---|
| Emails per batch call | 100 |
| Custom headers per email | 10 (names ≤ 126 chars, values 1–995 printable-ASCII chars) |
| WhatsApp messages per batch call | 100 |
| Recipients per WhatsApp item | 50 |
| Contacts per bulk upsert | 1000 |
| Contacts per list page | 100 (default 20) |
| Send rate | 1 email/s (free), 10/s (paid), burst 5×; 429 rate_limited + Retry-After |
Rate limiting
On top of the monthly quota, send requests are rate-limited per team: 1 email/s on the free plan, 10/s on paid plans (with a burst of 5× the per-second rate). Exceeding the limit answers 429 Too Many Requests with rate_limited and a Retry-After header (seconds to wait):
{
"error": {
"code": "rate_limited",
"message": "send rate limit exceeded; retry after 1 seconds"
}
}
In POST /v1/emails/batch the limit is applied per item: once the bucket is empty, the remaining items fail with rate_limited in their per-item result (no Retry-After header there). Replays with a stored idempotency_key do not consume rate. The monthly volume quota still applies on top.
Retry-After on a 429: sleep (or re-queue) for the given number of seconds rather than hammering the API — and keep the same idempotency_keys when you retry so a timed-out first attempt never double-sends.