Errors
The envelope
Every failure — on any endpoint — returns the same JSON envelope with the failure's HTTP status:
{
"error": {
"code": "…",
"message": "…"
}
}
codeis a stable machine-readable string (quota_exceeded,contact_exists, …) — switch on this.messageis a human-readable explanation in English. It is not contractual and may evolve.
When several things are wrong at once, only the last detected error is reported — fix and re-submit to see the next one.
Error codes
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Malformed JSON, missing field, invalid cursor/limit |
| 400 | invalid_from | From address cannot be parsed |
| 400 | batch_too_large | >100 emails/messages per batch; >1000 contacts per bulk upsert |
| 400 | invalid_phone | Non E.164 phone in WhatsApp item |
| 400 | invalid_params | Missing/unknown template parameter slot |
| 400 | template_disabled / template_unsupported | WhatsApp template not sendable |
| 401 | unauthorized | Missing/invalid/revoked key, or session token where a key is required |
| 402 | quota_exceeded | Send or contact allowance reached |
| 429 | rate_limited | Send rate limit exceeded (free: 1 email/s, paid: 10/s, burst 5×) — retry after the Retry-After response header (seconds); batch items carry this code per item without the header |
| 403 | insufficient_scope | Key lacks the route's scope |
| 403 | domain_not_allowed | Key bound to a different domain than the From address resolves to |
| 404 | contact_not_found | No contact with that email |
| 404 | not_found | e.g. unknown WhatsApp template_id |
| 409 | contact_exists | Duplicate create |
| 409 | contact_suppressed | bounced/complained contact cannot be resubscribed |
| 422 | from_domain_not_verified | From domain matches no verified team domain |
| 500 | internal | Unexpected server error |
Per-item errors in batch and bulk responses
Batch and bulk endpoints never fail wholesale for one bad item. Each item in the response is independently either an acceptance or an error, using the same code/message pair:
{
"index": 2,
"error": {
"code": "from_domain_not_verified",
"message": "no verified domain matches the from address"
}
}
index is the item's position in the request array (0-based). See Sending batches for the full decode.
Worked examples
401 unauthorized — no key, an invalid key, or a session token where an API key is required:
{
"error": {
"code": "unauthorized",
"message": "missing or invalid api key"
}
}
403 insufficient_scope — a valid key without the route's scope:
{
"error": {
"code": "insufficient_scope",
"message": "api key lacks the emails:send scope"
}
}
409 contact_exists — creating a contact whose email already exists:
{
"error": {
"code": "contact_exists",
"message": "contact already exists"
}
}
400 batch_too_large — more than 100 emails in one batch call:
{
"error": {
"code": "batch_too_large",
"message": "batch is limited to 100 emails"
}
}
429 rate_limited — sending faster than the plan's rate (1 email/s on the free plan, 10/s on paid plans, with a 5× burst). The response carries a Retry-After header with the seconds to wait. A real response:
{
"error": {
"code": "rate_limited",
"message": "send rate limit exceeded; retry after 1 seconds"
}
}
In a batch call 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). See Quotas and limits.
code, never on message. code values are stable API surface; message strings are diagnostics that can change at any time.