Errors

Every error code, and which are safe to retry.

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

StatuserrorMeaning
401missing_api_keyno bearer token on the request
401invalid_api_keykey not recognised, or wrong secret
401api_key_revokedthe key has been revoked
403insufficient_scopethe key lacks the scope this route needs
503auth_unavailablewe could not check the key right now. Retry.

Request

StatuserrorMeaning
400invalid_requestmalformed JSON, or a field failed validation. issues[] names the fields.
404not_foundno such route, or no such object for this key
405method_not_allowedthe route exists but not for that HTTP method
413payload_too_largethe request body is over the limit. The message gives the actual and permitted byte counts.
429rate_limitedover your per-minute limit. Back off and retry.

Sending

StatuserrorMeaning
400recipient_refusedthe address is near-certain to bounce. findings[] gives risk, detail and often a suggestion. Refused before anything is charged.
400unsubscribe_requireda marketing send without unsubscribe configured
402billing_pausedsending is paused for a billing reason
403from_domain_not_allowedthis key may not send from that domain
409send_in_progressan identical send is already in flight. It will not be sent twice.
409idempotency_conflictthis idempotencyKey is already in use by a different in-flight message. Use a new key.
413attachments_too_largeattachments exceed the limit. The message gives the byte counts.
429account_pool_exhaustedthe sending pool your account draws from has used its daily allocation. Retry after the UTC day rolls over.
429account_quota_fair_sharethe 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.
503account_pausedsending is paused on this account
503send_unavailablecould 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

StatuserrorMeaning
404domain_not_foundno such domain on this account
404template_not_foundno such template on this account
404message_not_foundno 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

StatusRetry?
400, 403, 404, 405, 413No. Fix the request.
401No, unless your key rotation is mid-flight.
402No. Resolve the billing issue.
409Yes, shortly — or treat as already-sent.
429Yes, after backing off — except account_quota_fair_share and account_pool_exhausted, which clear at the UTC day roll-over, not in seconds.
503Yes, with backoff and the same idempotencyKey.