Errors
Every error, from a malformed request to an internal fault, has the same shape:
{
"error": {
"code": "BAD_REQUEST",
"message": "to: Invalid input"
}
}
A validation failure adds details, one entry per offending field:
{
"error": {
"code": "BAD_REQUEST",
"message": "to: Invalid input; subject: String must contain at least 1 character(s)",
"details": [
{ "path": ["to"], "message": "Invalid input" },
{ "path": ["subject"], "message": "String must contain at least 1 character(s)" }
]
}
}
Branch on code, log message, and show details to whoever wrote the
request. message is written for a person and may be reworded; code will not
change without a version bump.
Codes
| Code | Status | What it means |
|---|---|---|
BAD_REQUEST |
400 | The request is wrong. The message says how. Do not retry unchanged. |
UNAUTHORIZED |
401 | Missing or invalid API key. |
FORBIDDEN |
403 | Valid key, not allowed to do this — usually a SENDING key reaching past sending, or a domain-scoped key reaching another domain. |
NOT_FOUND |
404 | No such thing, or it belongs to another account. The two are deliberately indistinguishable. |
CONFLICT |
409 | It already exists, or the state does not allow this. |
RATE_LIMITED |
429 | Too many requests. See rate limits. |
INTERNAL_SERVER_ERROR |
500 | Ours. Safe to retry with backoff. |
Which errors to retry
Retry 429 and 5xx, with exponential backoff. Do not retry 4xx — the
request will fail again in exactly the same way, and retrying a 400 in a loop
is how a bug becomes an incident.
If you retry anything that creates or sends, use an idempotency key, or a retry after a timeout will send twice.
Why a missing thing and someone else's thing look the same
Asking for a resource that belongs to another account returns 404, not 403.
403 would confirm the id exists, which turns any id field into a way to probe
what other accounts have.