Errors#
Every error uses the same envelope:
json Copy {
"statusCode" : 422 ,
"name" : "missing_required_field" ,
"message" : "Missing `subject` field."
}
Switch on name. It is the stable machine-readable slug; message is written for humans
and may change.
Reference#
400#
nameCause validation_errorThe request body failed schema validation invalid_parameterA parameter had an unacceptable value invalid_idempotency_keyThe key was empty or longer than 256 characters invalid_idempotent_requestThe same idempotency key was reused with a different body
401#
nameCause missing_api_keyNo Authorization header restricted_api_keyA sending_access key called a non-send endpoint invalid_accessA dashboard endpoint was called without a session
403#
nameCause invalid_api_keyThe key does not exist, or is domain-restricted and was used elsewhere invalid_user_agentNo User-Agent header invalid_from_addressfrom is malformed, its domain is unverified, or sending is disabled for it
404#
nameCause not_foundThe resource, or the endpoint, does not exist
409#
nameCause concurrent_idempotent_requestsThe original request with this key is still in flight invalid_parameterA uniqueness constraint was violated โ a duplicate
422#
nameCause missing_required_fieldA required field was absent validation_errorThe body did not match the expected shape invalid_attachmentMissing content, a blocked extension, or over the size limit invalid_accessThe operation is not permitted on this resource
429#
nameCause rate_limit_exceededMore than RATE_LIMIT_PER_SECOND requests in a second
5xx#
nameCause application_errorAn unhandled server error. Check the server log
Handling them#
ts Copy const res = await fetch (`${base} /emails` , { method : "POST" , headers, body })
if (!res.ok ) {
const error = await res.json ()
switch (error.name ) {
case "rate_limit_exceeded" : {
const wait = Number (res.headers .get ("retry-after" ) ?? 1 ) * 1000
await new Promise ((r ) => setTimeout (r, wait))
return retry ()
}
case "invalid_from_address" :
throw new Error (error.message )
case "missing_required_field" :
case "validation_error" :
throw new Error (error.message )
default :
if (res.status >= 500 ) return retryWithBackoff ()
throw new Error (error.message )
}
}
What to retry#
Retry Do not retry 429 โ after retry-after4xx validation and auth errors5xx โ with backoffinvalid_from_addressNetwork timeouts โ with an idempotency key restricted_api_key
Always use an idempotency key when retrying a send. A timeout
does not tell you whether the message was accepted, and retrying without one is how
people send the same receipt twice.
Common mistakes#
403 invalid_user_agent โ you cleared the header. Set it to something identifying
your application.
403 invalid_from_address in production but not locally โ locally you were on the
console transport, which skips the domain check. Verify the domain.
422 validation_error with an SDK โ usually a camelCase key that reached the wire
unconverted. Check request_body in the logs .
409 concurrent_idempotent_requests โ two identical requests raced. Wait and retry;
the first will have completed.