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.

Something here wrong or missing? It is generated from the running API — tell us and we will fix the source.