Errors
Every error is JSON with the same shape, whatever went wrong:
{ "error": "invalid_request", "message": "a sentence you can act on" }error is a stable machine-readable code — branch on it. message is for humans and may be reworded at any time, so never match on its text. Some errors carry extra fields, named below.
Authentication
| Status | error | Meaning |
|---|---|---|
| 401 | missing_api_key | no bearer token on the request |
| 401 | invalid_api_key | key not recognised, or wrong secret |
| 401 | api_key_revoked | the key has been revoked |
| 403 | insufficient_scope | the key lacks the scope this route needs |
| 503 | auth_unavailable | we could not check the key right now. Retry. |
Request
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_request | malformed JSON, or a field failed validation. issues[] names the fields. |
| 404 | not_found | no such route, or no such object for this key |
| 405 | method_not_allowed | the route exists but not for that HTTP method |
| 413 | payload_too_large | the request body is over the limit. The message gives the actual and permitted byte counts. |
| 429 | rate_limited | over your per-minute limit. Back off and retry. |
Sending
| Status | error | Meaning |
|---|---|---|
| 400 | recipient_refused | the address is near-certain to bounce. findings[] gives risk, detail and often a suggestion. Refused before anything is charged. |
| 400 | unsubscribe_required | a marketing send without unsubscribe configured |
| 402 | billing_paused | sending is paused for a billing reason |
| 403 | from_domain_not_allowed | this key may not send from that domain |
| 409 | send_in_progress | an identical send is already in flight. It will not be sent twice. |
| 409 | idempotency_conflict | this idempotencyKey is already in use by a different in-flight message. Use a new key. |
| 413 | attachments_too_large | attachments exceed the limit. The message gives the byte counts. |
| 429 | account_pool_exhausted | the sending pool your account draws from has used its daily allocation. Retry after the UTC day rolls over. |
| 429 | account_quota_fair_share | the shared sending platform is near its daily ceiling and your account has already sent more than its share today. The remaining capacity is reserved so every sender gets some. Retry after the UTC day rolls over. |
| 503 | account_paused | sending is paused on this account |
| 503 | send_unavailable | could not accept the send right now. Retry. |
The one that is genuinely ambiguous
A 503 on a send may come back saying the send may or may not have been queued. That is the truth, not hedging: the moment of handoff is the one point where we cannot know whether it landed.
Retry with the same idempotencyKey. If it did go out, the retry is recognised as a duplicate and ignored. This is the whole reason idempotency keys exist, and why you should always send one.
Objects
| Status | error | Meaning |
|---|---|---|
| 404 | domain_not_found | no such domain on this account |
| 404 | template_not_found | no such template on this account |
| 404 | message_not_found | no such message for this key |
A 404 here never distinguishes "does not exist" from "belongs to someone else". A key only ever sees its own account's objects.
The _unavailable family
Any error code ending in _unavailable — auth_unavailable, send_unavailable, messages_unavailable, domain_unavailable, domain_store_unavailable, template_store_unavailable, scheduled_store_unavailable, rate_limit_unavailable.
All are 503. All mean the same thing: something we depend on did not answer in time. None of them mean your request was wrong.
Retry with backoff. These are transient by definition. Treat a 503 as "ask again in a moment", and a 4xx as "do not send this again unchanged".
What to retry, at a glance
| Status | Retry? |
|---|---|
| 400, 403, 404, 405, 413 | No. Fix the request. |
| 401 | No, unless your key rotation is mid-flight. |
| 402 | No. Resolve the billing issue. |
| 409 | Yes, shortly — or treat as already-sent. |
| 429 | Yes, after backing off — except account_quota_fair_share and account_pool_exhausted, which clear at the UTC day roll-over, not in seconds. |
| 503 | Yes, with backoff and the same idempotencyKey. |