Skip to content
On this site

Errors

Every failure answers with the same three fields and a name from a closed list. Match on the name, not on the message.

The shape

{
  "statusCode": 422,
  "name": "missing_required_field",
  "message": "subject is required."
}

statusCode repeats the HTTP status, name is the stable identifier to branch on, and message is a sentence for a human. Messages are written to be readable and may be reworded; names are part of the contract and are not.

Validation failures add an errors array naming each field that failed. It is additive — code that ignores it keeps working.

{
  "statusCode": 400,
  "name": "validation_error",
  "message": "The request body is invalid.",
  "errors": [
    { "path": "to.0", "message": "Not a valid email address." },
    { "path": "subject", "message": "Must not be empty." }
  ]
}

One error carries a fourth field: a missing User-Agent answers with code: 1010. No other response has it, so it can be matched exactly.

The vocabulary

Two names appear more than once, with different statuses, because the situation genuinely differs — the status is what tells them apart, so branch on both.

  • validation_error is 400 when a field is wrong, 403 when the request is well-formed and not allowed — an unverified sending domain, a missing User-Agent, a policy restriction — and 422 when the body is fine and the resource itself refuses it, such as a plan limit reached.
  • restricted_api_key is 401 when a sending_access key is used on an endpoint that is not sending, and 403 when the key has been revoked. The first is the wrong key for the job; the second is a key that no longer works at all.

not_implemented at 501 is not a mistake on your side either. It means the route is ours and reserved for a later phase — it exists so an SDK gets a straight answer rather than a 404 that would imply something about the id it asked for.

Every error name and the status it carries
nameStatusMeans
invalid_idempotency_key400key length outside 1-256
validation_error400field-level validation failed (body, query, headers)
missing_api_key401no/malformed Authorization, or hash not found
restricted_api_key401sending_access key used on a non-sending endpoint
email_above_quota403email content unavailable because the team is over quota
invalid_permission403actor lacks a required scope (unverified user, future scoped keys/OAuth)
restricted_api_key403key status = revoked (not active)
suspended_api_key403key status = suspended or team risk_state = suspended
validation_error403domain not verified, from-domain not allowed for this key, sandbox restriction, missing User-Agent (code 1010), policy restriction
not_found404resource not found or not in this team (no existence leak), unknown route
method_not_allowed405known path, wrong method
concurrent_idempotent_requests409same key in flight
invalid_idempotent_request409same key, different fingerprint
resource_locked409scheduled email already dispatched / resource being updated
validation_error422the body is well-formed but the resource refuses it: a plan limit reached, an unroutable webhook endpoint, a replay against a disabled webhook (14 §2)
invalid_attachment422neither content nor path, both, fetch blocked, too large, bad type
invalid_parameter422path/query parameter malformed (not a UUID, bad enum)
missing_required_field422body missing from/to/subject/name/...
missing_required_parameter422required query/path parameter absent
daily_quota_exceeded429team daily quota
monthly_quota_exceeded429team monthly quota
rate_limit_exceeded42910 rps team limit
application_error500unexpected error (request id in message)
not_implemented501the route exists and is reserved for a later phase
service_unavailable503dependency down, global send disabled, readiness failed

The OAuth flow's own vocabulary

The OAuth flow's routes answer in the shape OAuth libraries already parse instead: POST /oauth/token and POST /oauth/revoke use RFC 6749 §5.2's, and /oauth/register and its management routes RFC 7591 §3.2.2's. Branch on error; the description is for a human.

{
  "error": "invalid_grant",
  "error_description": "The authorization code has expired."
}
Every OAuth flow error and the status it carries
errorStatusMeans
invalid_request400A parameter is missing, repeated or malformed. The same name at 429 means the client or address is over its limit.
invalid_client401The client is unknown or disabled.
invalid_grant400The code or refresh token is expired, revoked, already used, issued to another client, or the verifier or redirect URI does not match.
unauthorized_client400The client may not use this grant type.
unsupported_grant_type400The grant type is neither authorization_code nor refresh_token.
invalid_scope400A scope is unknown, or wider than the grant.
invalid_client_metadata400Registration: a metadata field was refused.
invalid_redirect_uri400Registration: a redirect URI was refused — not https or loopback, or malformed.
invalid_token401Client management: the registration access token is missing, malformed or unknown.

One refusal in the flow is not a body at all. GET /oauth/authorize with an unknown client or an unregistered redirect URI answers 400 with an error page and never redirects; every other authorize failure redirects back to the client with error and state. And an invalid_token from client management also carries WWW-Authenticate: Bearer error="invalid_token".

Every other route — /oauth/grants included, and any endpoint called with an OAuth access token — answers with the Rasket error body above.

What to retry

429 and 503 are worth retrying, with backoff — a 429 tells you exactly how long to wait in its retry-after header. 500 is worth one retry with an idempotency key, and worth reporting if it persists. Everything in the 4xx range other than 429 describes something about the request that will not change on its own; retrying it unchanged will fail again.