Errors
Docs / API reference

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

401bad / missing / revoked / expired keystop retrying
response body
{ "message": "Unauthenticated" }
403key lacks the required abilitystop retrying
response body
{
  "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"
}
404unknown template or message idstop retrying
response body
{ "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." }
409Idempotency-Key conflictstop retrying
response body
{ "message": "Idempotency-Key was already used with a different request body" }
410content past retentionstop retrying
response body
{
  "message": "Content expired",
  "completed_at": null
}
422missing template variables (all named)stop retrying
response body
{ "message": "Missing template variables: reset_url, expires_minutes" }
429rate or daily limit — retry with backoffmay retry
response body
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)."
  }
}
429content view limit — per person, per day, not a rate limitstop retrying
response body
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
}
429unauthenticated edge limit, by IP — not your account or keymay retry
response body
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"
}
403sealed body — revealing one is owner-only and auditedstop retrying
response body
{
  "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"
}
403key bound to an archived Application — no new sendsstop retrying
response 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"
}
503a dependency we refuse to run withoutmay retry
response body
{
  "message": "Rate limiter unavailable — refusing request rather than bypassing the cap",
  "type": "dependency_unavailable",
  "dependency": "redis"
}
503ciphertext present but undecryptable — not missing contentstop retrying
response body
{
  "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"
}
503no usable transport — the message was not queuedmay retry
response body
{
  "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.