Every failure from every surface — REST, MCP, the CLI — carries the same envelope with a stable
code. Branch on code and retryable. Never on the HTTP
status, and never on the message text. An empty result set is never an error, and a wait
timeout is an HTTP 200 with status: "timeout".
| Code | HTTP | Retryable | Means |
|---|---|---|---|
unauthenticated |
401 | no | No Authorization header reached us. |
invalid_api_key |
401 | no | The key does not exist, or has been revoked. |
insufficient_scope |
403 | no | The key is valid but lacks the scope this route needs. |
forbidden |
403 | no | Region mismatch, or a bad or expired download signature. |
inbox_not_found |
404 | no | No inbox at that id or address, for this project. |
message_not_found |
404 | no | Unknown, expired, or erased. |
attachment_not_found |
404 | no | No attachment with that id on that message. |
domain_not_found |
404 | no | No such domain on this project. |
domain_not_verified |
409 | yes | The domain exists but its DNS is not satisfied yet. |
project_not_found |
404 | no | The project behind this key or resource is gone. |
webhook_not_found |
404 | no | No webhook endpoint with that id on this project. |
article_not_found |
404 | no | No published blog article has that slug. |
invalid_request |
400 | no | The request could not be interpreted. |
unsupported_format |
400 | no | This message has no part in the format you asked for. |
payload_too_large |
413 | no | Over a hard size limit. |
conflict |
409 | no | Already claimed, or already in that state. |
quota_exceeded |
402 | no | The period allowance is spent, and this read was refused. |
organisation_limit_exceeded |
402 | yes | An organisation plan, spend or byte limit prevents this operation. |
rate_limited |
429 | yes | Too many requests per second on this route class. |
concurrency_limit |
429 | yes | Too many blocking waits in flight for this project. |
blocked_recipient |
403 | no | The recipient is on the abuse blocklist. |
abuse_suspended |
403 | no | The organisation is suspended. |
ingest_ceiling_exceeded |
429 | yes | The project's ingest ceiling for the period is reached, so this delivery was refused at SMTP with a transient 452 4.3.1. |
storage_unavailable |
503 | yes | The object store could not be read or written. |
internal |
500 | yes | An unexpected failure, wrapped so that it still carries a code. |
unauthenticatedNo Authorization header reached us.
Do: Send Authorization: Bearer is_live_… or is_test_…. Raised before anything else, so it tells you nothing about whether the resource exists.
invalid_api_keyThe key does not exist, or has been revoked.
Do: Check for a truncated paste, then mint a new key.
insufficient_scopeThe key is valid but lacks the scope this route needs.
Do: Read details.required_scope and mint a key carrying it.
forbiddenRegion mismatch, or a bad or expired download signature.
Do: Call the region named in details.project_region, or re-fetch the presigned URL.
inbox_not_foundNo inbox at that id or address, for this project.
Do: Create it with inboxes.create, or fix the address. Never treat this as "no mail yet" — an empty result set is what that looks like.
message_not_foundUnknown, expired, or erased.
Do: Messages are deleted at the inbox TTL. Check GET /v1/retention.
attachment_not_foundNo attachment with that id on that message.
Do: Use an id from the message's attachments[].
domain_not_foundNo such domain on this project.
Do: Add it with POST /v1/domains. Another project's domain reports this same code.
domain_not_verifiedThe domain exists but its DNS is not satisfied yet.
Do: Publish the records from domains.get, call domains.verify, and retry after about 60s.
project_not_foundThe project behind this key or resource is gone.
Do: An internal-consistency failure. Re-resolve the project.
webhook_not_foundNo webhook endpoint with that id on this project.
Do: List this project's endpoints and use an id you own. Another project's reports this same code.
article_not_foundNo published blog article has that slug.
Do: Browse /blog or list articles through /content/v1/articles for a current URL.
invalid_requestThe request could not be interpreted.
Do: Read details. Never retry unchanged.
unsupported_formatThis message has no part in the format you asked for.
Do: details.available lists the parts it does have.
payload_too_largeOver a hard size limit.
Do: Not raisable by making the same call again. Send less.
conflictAlready claimed, or already in that state.
Do: Read the current state first. Domain names are globally unique here.
quota_exceededThe period allowance is spent, and this read was refused.
Do: Inbound mail is still accepted and stored; only the read failed. Raise the plan limit, or wait for retry_after_seconds.
organisation_limit_exceededAn organisation plan, spend or byte limit prevents this operation.
Do: Read details.limit_kind and the remedy. An owner can review the plan or budget; accepted mail remains until normal expiry. Sender retries can expire while ingestion is paused.
rate_limitedToo many requests per second on this route class.
Do: Honour retry_after_seconds. If you were polling, replace the loop with messages.wait, which is never rate limited.
concurrency_limitToo many blocking waits in flight for this project.
Do: Let one finish, or raise max_concurrent_waits.
blocked_recipientThe recipient is on the abuse blocklist.
Do: Not fixable from the API. Contact the abuse address.
abuse_suspendedThe organisation is suspended.
Do: Contact support. Every request fails until it is lifted.
ingest_ceiling_exceededThe project's ingest ceiling for the period is reached, so this delivery was refused at SMTP with a transient 452 4.3.1.
Do: Nothing was stored and the sender was told to retry, so honest mail is not lost. If this project is not expected to be this busy, treat its SMTP credential as leaked and rotate it before raising the ceiling.
internalAn unexpected failure, wrapped so that it still carries a code.
Do: Ours. Retry, and quote the x-request-id response header.