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

One JSON object. message is always there. A structured failure adds type. A validation 422 may also include errors.

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"
}

Status codes

CodeTypeMeaningRetryBody
401—Missing, bad, revoked or expired key.stop retrying
401 response body
response body
{ "message": "Unauthenticated" }
403forbidden_abilityThe key lacks the ability this route requires.stop retrying
403 response body
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"
}
403application_archivedThe key is bound to an archived Application. No new sends.stop retrying
403 response body
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"
}
403sealed_body_deniedRevealing a sealed body is owner-only and audited.stop retrying
403 response body
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"
}
404—Unknown template, template version, application, or message id.stop retrying
404 response body
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." }

{ "message": "No template version with id \"999999\" on this account." }

{ "message": "No application with slug \"billing\" on this account. Create it in the panel, or omit \"application\" and use a key scoped to one." }
409—This Idempotency-Key was already used with a different body.stop retrying
409 response body
response body
{ "message": "Idempotency-Key was already used with a different request body" }
410—The stored body is past retention.stop retrying
410 response body
response body
{
  "message": "Content expired",
  "completed_at": null
}
413report_too_largeThe DMARC report is over 5 MB.stop retrying
413 response body
response body
{
  "message": "That report is larger than 5 MB, which no real aggregate report is.",
  "type": "report_too_large"
}
422—A message and no type: missing or unexpected template variables, no published version, application required, or From not verified.stop retrying
422 response body
response body
{ "message": "Template \"auth/password-reset\": missing variables: reset_url, expires_minutes" }

{ "message": "Template \"auth/password-reset\": unexpected variables: extra" }

{ "message": "Template has no published version" }

{ "message": "application is required for org-wide keys" }

{ "message": "From domain \"mail.example\" is not verified for this account. Verify it under Domains (TXT ownership), or send From an address on a domain you have already verified." }
422—Validation: a message plus errors, and no type. A cursor that is not a uuid fails here, before cursor_expired.stop retrying
422 response body
response body
{
  "message": "The to field must be an address, an object with an email, or a list of either.",
  "errors": {
    "to": [
      "The to field must be an address, an object with an email, or a list of either."
    ]
  }
}
422cursor_expiredThe message that cursor points at was erased or aged out. Start the listing again without a cursor.stop retrying
422 response body
response body
{
  "message": "That cursor no longer exists — the message it points at was erased or has aged out of retention. Start the listing again without a cursor.",
  "type": "cursor_expired"
}
422limit_exceededrecipients_per_messageToo many recipients in one message, across to, cc and bcc. No Retry-After.stop retrying
422 response body
response body
{
  "message": "Too many recipients in one message: 101 of a maximum 100 across to, cc and bcc.",
  "type": "limit_exceeded",
  "limit": {
    "dimension": "recipients_per_message",
    "binding": "platform",
    "lift": "Split the send into batches of 100 recipients or fewer. This ceiling is the same one the inbound SMTP shim applies.",
    "limit": 100,
    "used": 101
  }
}
422limit_exceededapplicationsThe plan's application cap on POST /v1/applications. No Retry-After.stop retrying
422 response body
response body
{
  "message": "Application limit reached: 3 of 3 on Free. Upgrade your plan to create more applications.",
  "type": "limit_exceeded",
  "limit": {
    "dimension": "applications",
    "binding": "plan",
    "lift": "Upgrade your plan to create more applications.",
    "limit": 3,
    "used": 3
  }
}
422empty_reportPOST /v1/dmarc-reports with no file.stop retrying
422 response body
response body
{
  "message": "Send the report as the request body, or as a file field named \"report\". Gzip, zip and plain XML are all accepted.",
  "type": "empty_report"
}
422unreadable_reportThe report file cannot be read. A document type declaration is one of those refusals.stop retrying
422 response body
response body
{
  "message": "Refusing a DMARC report containing a document type declaration.",
  "type": "unreadable_report"
}
422invalid_cursorGET /v1/dmarc-reports was given an integer id, or a cursor that is not a position in the list. Start again without a cursor.stop retrying
422 response body
response body
{
  "message": "That cursor is an integer id. This list is ordered by range_end, then id, and a cursor has to name both or a later page drops reports. Start the listing again without a cursor.",
  "type": "invalid_cursor"
}
429limit_exceededapi_rateThe key's minute bucket.honour Retry-After
429 response body
response body
HTTP/1.1 429 · Retry-After: 42
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0

{
  "message": "API rate limit exceeded: used 60 of 60 requests this minute. Wait for the next minute window, or raise the key/plan API rate limit.",
  "type": "limit_exceeded",
  "limit": {
    "dimension": "api_rate",
    "binding": "plan",
    "limit": 60,
    "used": 60,
    "lift": "Wait for the next minute window, or raise the key/plan API rate limit."
  }
}
429limit_exceededdaily_sendThe plan, account or application ceiling on the account counter, or a member's own counter (binding member). Retry-After is the seconds until midnight in the account's timezone, at least 1. application_daily_send uses that same header and has no separate sample.honour Retry-After
429 response body
response body
HTTP/1.1 429 · Retry-After: 21600

{
  "message": "Daily send limit exceeded: plan daily send limit — used 1,000 of 1,000 recipients today. Upgrade your plan to raise the daily send cap, or wait until midnight (UTC).",
  "type": "limit_exceeded",
  "limit": {
    "dimension": "daily_send",
    "binding": "plan",
    "lift": "Upgrade your plan to raise the daily send cap, or wait until midnight (UTC).",
    "limit": 1000,
    "used": 1000
  }
}
429limit_exceededrecipient_messages_per_hourOne address from this account during the current UTC hour. Retry-After is 600, which can still fall inside that hour.honour Retry-After
429 response body
response body
HTTP/1.1 429 · Retry-After: 600

{
  "message": "One recipient has already received 60 messages from this account in the last hour, of a maximum 60.",
  "type": "limit_exceeded",
  "limit": {
    "dimension": "recipient_messages_per_hour",
    "binding": "account",
    "lift": "This ceiling exists to catch a retry loop, not to shape normal traffic. If an address legitimately needs more, it can be raised per account.",
    "limit": 60,
    "used": 60
  }
}
429content_view_quota_exceededThis person's content reads for today. No Retry-After. The same read keeps failing until midnight in the account's timezone.stop retrying
429 response body
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
}
429edge_rate_limitedUnauthenticated traffic from this address.honour Retry-After
429 response body
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"
}
503dependency_unavailableA dependency the request will not run without. Retry-After is 30.honour Retry-After
503 response body
response body
{
  "message": "Rate limiter unavailable — refusing request rather than bypassing the cap",
  "type": "dependency_unavailable",
  "dependency": "redis"
}
503body_unreadableThe ciphertext is present and cannot be decrypted.stop retrying
503 response body
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_transportNothing could take the message, so it was not queued.fix the transport, then the same key
503 response body
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. The refusals are in the list above: 422 empty_report, 422 unreadable_report and 413 report_too_large.

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

Decide from the type first, then from limit.dimension. The status code is shared by rows that stop and rows that retry.

Stop retrying when there is no type — 401, 404, 409, 410, and a 422 that is only a message or a validation errors object — and when the type is forbidden_ability, application_archived, sealed_body_denied, cursor_expired, empty_report, unreadable_report, report_too_large, body_unreadable or content_view_quota_exceeded. That content-view 429 sends no Retry-After. The same read keeps failing until midnight in the account's timezone. Also stop retrying a 422 limit_exceeded whose dimension is recipients_per_message or applications. Those send no Retry-After.

Honour Retry-After when the type is limit_exceeded and the dimension is api_rate, daily_send, application_daily_send or recipient_messages_per_hour, and when the type is edge_rate_limited. On POST /v1/messages, send the same Idempotency-Key. daily_send and application_daily_send set Retry-After to the seconds until midnight in the account's timezone, at least 1. A member cap uses dimension daily_send with binding member. recipient_messages_per_hour sets Retry-After to 600, and the counter is the current UTC hour, so that delay can still fall inside it. edge_rate_limited is unauthenticated traffic from this address.

A 503 dependency_unavailable sends Retry-After: 30. Retry it. A 503 no_usable_transport means the message was not queued. Fix the transport, then send again with the same key. It sends no Retry-After.