Error reference
Error reference
Every API error uses one canonical envelope: an error object with a stable machine-readable code, a human message, and a fields object (empty unless a specific field failed validation).
Response shape
Error body
{
"error": {
"code": "not_found",
"message": "Resource not found.",
"fields": {}
}
}
Match on error.code, never on the message text — codes are stable; messages may change.
Codes
| Code | HTTP | Meaning | How to handle |
|---|---|---|---|
authentication_required |
401 |
Missing or invalid API key. | Send a valid Authorization: Bearer ms_live_... header. |
account_forbidden |
403 |
The account is not permitted to use the API (unverified, suspended, or inactive). | Verify your email or contact support. |
not_found |
404 |
The resource does not exist or is not owned by this key. | Check the id; foreign and deleted ids are indistinguishable by design. |
method_not_allowed |
405 |
The HTTP method is not supported for this path. | Use the documented method for the endpoint. |
not_acceptable |
406 |
The requested response format is not acceptable. | Request application/json. |
conflict |
409 |
The request conflicts with the current state (e.g. allocation retries exhausted). | Retry, or inspect the other fields. |
plan_limit_reached |
409 |
The plan limit for this action has been reached. | Upgrade your plan, or free capacity first. |
slug_taken |
409 |
The requested inbox routing_key is already claimed by another account. | Choose a different routing_key, or omit it for a random one. |
unsupported_media_type |
415 |
The request media type is not supported. | Send a JSON request body. |
validation_error |
422 |
The request body or a parameter failed validation. | Read the fields object for per-field messages. |
rate_limited |
429 |
The per-key request rate limit was exceeded. | Honour Retry-After and slow down. |
too_many_wait_requests |
429 |
The per-account concurrent-wait cap was reached (2 free / 3 paid). | Wait for a wait to return or time out, then retry. |
wait_capacity |
429 |
The global concurrent-wait cap across all accounts was reached. | Retry shortly; capacity frees automatically. |
plan_upgrade_required |
402 |
This feature (batch message operations) requires a paid plan. | Upgrade your plan to use POST /messages/batch. |
webhook_entitlement_required |
403 |
Webhooks require a paid plan. | Upgrade your plan to configure or test a webhook. |
webhook_not_configured |
404 |
No webhook is configured for this inbox. | Create a webhook for the inbox before testing it. |
webhook_ssrf_rejected |
400 |
The webhook URL was rejected by SSRF validation. | Use a public HTTPS URL; localhost/private/link-local addresses are blocked. |
webhook_unreachable |
502 |
The webhook receiver could not be reached (timeout, TLS, or network error). | Check the receiver is up and reachable over HTTPS. |
server_error |
500 |
An internal error occurred. | Retry; if it persists, report it. |
byod_disabled |
403 |
Custom domains are not enabled for this account. | Custom domains are gated behind a feature flag; contact support. |
domain_entitlement_required |
403 |
Custom domains require a Business plan. | Upgrade to the Business plan to add a custom domain. |
domain_limit_reached |
403 |
The custom-domain limit for this plan has been reached. | Remove an existing domain, or upgrade your plan. |
domain_taken |
409 |
The requested domain name is already registered (by any account). | Choose a different domain, or contact support if you believe this is an error. |
domain_has_inboxes |
409 |
The domain still has inboxes bound to it. | Remove the bound inboxes before deleting the domain. |
domain_verify_rate_limited |
429 |
Too many manual domain-verify attempts. | Honour Retry-After and slow down. |
domain_not_found_or_unverified |
422 |
The domain id was not found, is not owned by this account, or is not verified. | Create/verify the domain first, or omit `domain` to create a shared-domain inbox. |
public_not_allowed_on_custom_domain |
422 |
A custom-domain (BYOD) inbox cannot be made public — `public` is shared-domain only. | Omit `public`, or omit `domain` to create a shared-domain inbox. |
metadata_must_be_object |
422 |
The `metadata` field must be a JSON object (not a list, string, or number). | Send `metadata` as a JSON object, e.g. {"run": "ci-123"}. |
metadata_too_large |
422 |
`metadata` exceeds 4096 bytes (compact UTF-8 JSON) or 50 top-level keys. | Shrink the payload — metadata is for correlation ids, not blobs. |
catch_all_entitlement_required |
403 |
Catch-all requires a Business plan. | Upgrade to the Business plan to enable catch-all. |
domain_not_verified |
409 |
The domain must be `verified` before catch-all can be enabled. | Verify the domain first (`POST /domains/{id}/verify`). |
catch_all_target |
409 |
This inbox is the active catch-all target for its domain and cannot be deleted or disabled. | Disable catch-all on the domain first (`PATCH /domains/{id}/catch_all` with `catch_all: false`). |
catch_all_target_invalid |
422 |
The designated `inbox_id` is not a live inbox bound to this domain. | Pass an enabled, non-deleted inbox_id already bound to this domain, or omit it to auto-create a sentinel. |
inbox_limit_reached |
403 |
The plan's inbox limit was reached while auto-creating the catch-all sentinel. | Remove an existing inbox, or upgrade your plan. |
catch_all_rate_limited |
429 |
Too many catch-all toggle attempts for this domain. | Honour Retry-After and slow down. |
outbound_disabled |
403 |
Outbound email is not enabled for this account. | Contact support; outbound send is gated behind a feature flag. |
outbound_entitlement_required |
403 |
Outbound email requires a Business plan (or the account is a demo/agent account, which can never send). | Upgrade to the Business plan. |
sending_domain_not_eligible |
403 |
The from_address's domain is not owned, verified, and DKIM-verified by this account (all four causes collapse to this one code — no oracle). | Verify the domain and complete DKIM setup (`POST /domains/{id}/sending_setup`), then re-check its DNS. |
recipient_rejected |
422 |
One or more recipients are on the suppression list (hard-bounced or explicitly suppressed). | Remove the suppressed recipient(s) and resend. |
outbound_quota_exceeded |
403 |
The Business plan's per-day or per-hour outbound recipient cap has been reached. | Wait for the window to roll over, or contact support. |
outbound_rate_limited |
429 |
Too many outbound send requests, or the per-tenant queue-depth guard was hit. | Honour Retry-After and slow down. |
recipient_not_approved |
422 |
One or more recipients are not on the caller's approved-recipients allowlist. | Approve the recipient (`POST /outbound_allowlist`) and resend. |
outbound_allowlist_entitlement_required |
403 |
The approved-recipients allowlist requires a Business plan. | Upgrade to the Business plan. |
approved_recipient_exists |
409 |
That email address is already on the caller's approved-recipients allowlist. | No action needed; the address is already approved. |
outbound_allowlist_limit_reached |
422 |
The approved-recipients allowlist entry limit has been reached. | Remove an existing entry first. |
outbound_allowlist_rate_limited |
429 |
Too many allowlist changes. | Honour Retry-After and slow down. |
smtp_verify_disabled |
403 |
Full SMTP verification (`mode: "full"`) is not enabled for this account. | Full SMTP verify is gated behind a feature flag; contact support. |
verify_full_entitlement_required |
403 |
Full SMTP verification requires a Business plan. | Upgrade to the Business plan, or use `mode: "safe"` (the default). |
reply_target_invalid |
422 |
No usable reply address (Reply-To / From / envelope From) could be parsed from the original message. | Use compose instead, or address the recipient directly. |
batch_too_large |
422 |
The batch exceeds the maximum number of unique rows for this mode (100 safe / 25 full). | Split the batch into smaller requests. |
file_too_large |
422 |
The uploaded file exceeds the 64KB maximum (checked via Content-Length and re-checked against the actual parsed size). | Split the file, or paste the addresses as `text`/`emails` instead. |
unsupported_file_type |
422 |
The uploaded file must be a `.csv` or `.txt` file. | Rename/re-export the file with a `.csv` or `.txt` extension. |
malformed_file |
422 |
The uploaded file could not be decoded as UTF-8 text, or contains a NUL byte (binary content). | Re-save the file as plain UTF-8 text. |
The two 429s
The wait endpoint can return 429 for two distinct reasons, distinguished only by error.code:
| Code | Cap | Release |
|---|---|---|
too_many_wait_requests | Per-account concurrent waits: 2 free / 3 paid. | When one of your waits returns or times out. |
wait_capacity | Global cap across all accounts (16). | Automatically when any wait frees a slot; retry shortly. |