Skip to content

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"
}
FieldAlways?What it's for
messageYesHuman-readable. Written for a developer reading a log, not for an end user.
codeYesSwitch on this. Messages get reworded; codes don't.
errorsValidation onlyField → problems, mirroring the request body's shape.
request_idYesThe id of this call in your dashboard's Logs page.

Codes

CodeStatusMeaning
validation_error422The body failed validation. errors says where.
invalid_parameter422A query parameter is out of range or unrecognised — a limit above 100, an unknown pagination cursor.
email_not_pending422You tried to reschedule or cancel an email that has already left.
missing_api_key401No Authorization: Bearer … header.
invalid_api_key401The key doesn't exist, or it was revoked.
restricted_api_key403The key exists but lacks the access this endpoint needs — usually a Sending key being used to read.
account_frozen403Sending is paused for this account: a bounce/complaint threshold, or a manual freeze.
not_found404No such object — or it belongs to another team.
method_not_allowed405Wrong verb for this path.
rate_limit_exceeded429Too many requests this second. See Rate limits.
daily_quota_exceeded429Today's quota is spent. It resets at 00:00 UTC.
internal_server_error500Our fault. Safe to retry — and send us the request_id.

What to retry

StatusRetry?
429Yes — after Retry-After seconds.
5xxYes — with exponential backoff.
other 4xxNo. 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

FieldMessageCause
fromProvide a from address.from was missing or empty.
fromCould not parse from address.Not a valid email or Name <email>.
fromDomain X isn't attached to your account.The domain isn't added.
fromDomain X isn't verified yet.Added, but its DNS isn't verified.
fromThis API key is scoped to a different domain.The key is domain-scoped elsewhere.
toAt least one recipient is required.to resolved to nothing.
toA message may have at most 50 recipients across to, cc and bcc.SES caps a message at 50 destinations.
toCannot send to disposable email address(es): …A recipient uses a throwaway domain.
subjectProvide a subject.Missing, and no template supplied one.
bodyProvide html or text body.Both empty, and no template supplied one.
attachmentsCombined attachment size exceeds the 10 MB limit.The decoded attachments total more than 10 MB.
attachments.N.contentAttachment content must be valid base64.content wasn't base64.
attachmentsAttachments 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.

Transactional email on your own domain.