Appearance
Errors
Every failure — validation, auth, throttling, a bug on our side — comes back in the same shape, so you can write one error handler and be done with it:
json
{
"message": "The to field is required.",
"code": "validation_error",
"errors": { "to": ["The to field is required."] },
"request_id": "8f2c1e64-1b17-4f8c-9f0e-3b6c1e0f5c31"
}| Field | Always? | What it's for |
|---|---|---|
message | Yes | Human-readable. Written for a developer reading a log, not for an end user. |
code | Yes | Switch on this. Messages get reworded; codes don't. |
errors | Validation only | Field → problems, mirroring the request body's shape. |
request_id | Yes | The id of this call in your dashboard's Logs page. |
Codes
| Code | Status | Meaning |
|---|---|---|
validation_error | 422 | The body failed validation. errors says where. |
invalid_parameter | 422 | A query parameter is out of range or unrecognised — a limit above 100, an unknown pagination cursor. |
email_not_pending | 422 | You tried to reschedule or cancel an email that has already left. |
missing_api_key | 401 | No Authorization: Bearer … header. |
invalid_api_key | 401 | The key doesn't exist, or it was revoked. |
restricted_api_key | 403 | The key exists but lacks the access this endpoint needs — usually a Sending key being used to read. |
account_frozen | 403 | Sending is paused for this account: a bounce/complaint threshold, or a manual freeze. |
not_found | 404 | No such object — or it belongs to another team. |
method_not_allowed | 405 | Wrong verb for this path. |
rate_limit_exceeded | 429 | Too many requests this second. See Rate limits. |
daily_quota_exceeded | 429 | Today's quota is spent. It resets at 00:00 UTC. |
internal_server_error | 500 | Our fault. Safe to retry — and send us the request_id. |
What to retry
| Status | Retry? |
|---|---|
429 | Yes — after Retry-After seconds. |
5xx | Yes — with exponential backoff. |
other 4xx | No. The request is wrong; retrying sends the same wrong request. |
Retrying a send is safe when you pass an Idempotency-Key: a repeat with the same key returns the original result instead of sending a second copy.
Debugging with request_id
Every response carries X-Request-Id, and every error repeats it in the body. That id is the id of the call's entry in your dashboard's Logs page — which holds the request body we received, the response we sent back, the status and the latency.
So the loop for a failing call is short: read request_id, open Logs → <request_id>, and look at exactly what we got. No hunting by timestamp, nothing to reproduce.
Log it on your side too. When you ask us about a request, the id is the fastest thing you can hand us.
Validation messages you'll actually hit
| Field | Message | Cause |
|---|---|---|
from | Provide a from address. | from was missing or empty. |
from | Could not parse from address. | Not a valid email or Name <email>. |
from | Domain X isn't attached to your account. | The domain isn't added. |
from | Domain X isn't verified yet. | Added, but its DNS isn't verified. |
from | This API key is scoped to a different domain. | The key is domain-scoped elsewhere. |
to | At least one recipient is required. | to resolved to nothing. |
to | A message may have at most 50 recipients across to, cc and bcc. | SES caps a message at 50 destinations. |
to | Cannot send to disposable email address(es): … | A recipient uses a throwaway domain. |
subject | Provide a subject. | Missing, and no template supplied one. |
body | Provide html or text body. | Both empty, and no template supplied one. |
attachments | Combined attachment size exceeds the 10 MB limit. | The decoded attachments total more than 10 MB. |
attachments.N.content | Attachment content must be valid base64. | content wasn't base64. |
attachments | Attachments cannot be sent through the batch endpoint. | Send those messages individually. |
Field-level rules also apply: subject up to 998 characters, from up to 320, at most 10 attachments per message.
For batch sends, error keys are prefixed with the item index — emails.1.from is the from of the second message. A batch is all-or-nothing: one bad item rejects the whole request, so you never have to work out which half of it went.