Errors
You'll decide whether to trust this API by reading this list — that's what it's for. Every error is JSON with a human message. Structured failures also carry a stable type.
Envelope
The wrapper around every error — a JSON object with a human message. Default shape: { "message": "…" }. Transport and dependency failures add type and related fields. Laravel validation errors on malformed JSON fields may also include a errors object (422).
Status codes
{ "message": "Unauthenticated" }{
"message": "This API key does not have the \"messages:send\" ability. Abilities are chosen when the key is minted and cannot be widened — mint a new key that includes this ability.",
"type": "forbidden_ability",
"required_ability": "messages:send"
}{ "message": "No template with slug \"auth/password-resett\" on this account. Sync it from git or create it in the panel before sending against it." }{ "message": "Idempotency-Key was already used with a different request body" }{
"message": "Content expired",
"completed_at": null
}{ "message": "Missing template variables: reset_url, expires_minutes" }HTTP/1.1 429 · Retry-After: 60 { "message": "Daily send limit exceeded: plan daily send limit — used 1,000 of 1,000. Upgrade your plan to raise the daily send cap, or wait until midnight (UTC).", "type": "limit_exceeded", "limit": { "dimension": "daily_send", "binding": "plan", "limit": 1000, "used": 1000, "lift": "Upgrade your plan to raise the daily send cap, or wait until midnight (UTC)." } }
HTTP/1.1 429 { "message": "Content view limit reached: you have opened 20 of 20 message bodies today. It resets at midnight Europe/Kyiv. Every body opened on this account is recorded in the audit log; ask an owner to raise the per-person cap if you need more.", "type": "content_view_quota_exceeded", "limit": 20, "used": 20 }
HTTP/1.1 429 · Retry-After: 12 { "message": "Too many requests from this address. This limit is on unauthenticated traffic reaching the API, not on your account or key.", "type": "edge_rate_limited" }
{
"message": "This message is sealed. Revealing a sealed body is owner-only and audited, and this key was not minted by an owner — have an owner mint a read-content key, or open the message in the panel as an owner.",
"type": "sealed_body_denied",
"required_ability": "reveal_sealed_body"
}{
"message": "This key is bound to the Application \"legacy-mailer\", which is archived. Archived Applications accept no new sends; use a key for a live Application.",
"type": "application_archived",
"application": "legacy-mailer"
}{
"message": "Rate limiter unavailable — refusing request rather than bypassing the cap",
"type": "dependency_unavailable",
"dependency": "redis"
}{
"message": "Message body cannot be decrypted — the keyring is unavailable on this server. This is a platform fault, not missing content. Contact support.",
"type": "body_unreadable",
"reason": "decrypt"
}{
"message": "no_usable_transport: No transport is configured for this application, or all configured transports are disabled. Add or re-enable a transport in the panel, then retry with the same idempotency key. The message was not queued.",
"type": "no_usable_transport",
"last_transport_error": "connection refused to smtp.example:587"
}Reports you forward to us
POST /v1/dmarc-reports takes the report file as the request body — gzip, zip or plain XML. Three refusals are its own. An empty body is 422 with type: "empty_report"; a file that does not parse as a DMARC aggregate report, or that carries a document type declaration, is 422 with type: "unreadable_report" and a message naming what is wrong with it; anything over 5 MB is 413 with type: "report_too_large", which no real aggregate report is.
Sending the same report twice is not an error: it replaces the one already held and answers 200 instead of 201, so a provider retry never doubles a number on your screen.
When to stop retrying
Stop: 401, 403, 404, 409, 410, 422 — fixing the request is required; blind retries only burn quota. Retry with backoff: 429 (honour Retry-After, same Idempotency-Key), 503 with dependency_unavailable (Retry-After: 30). For no_usable_transport, the message was not queued — fix the transport, then re-send with the same key.