Errors
The error envelope, every error type, and the messages the API returns.
The envelope
Every /v0 error, and errors from /health, /inbound/* and /auth/*, has this body:
| Field | Type | Meaning |
|---|---|---|
error.type | string | One of the types below. The HTTP status always matches the type. |
error.message | string | A human-readable sentence. Do not parse it; it can change. |
error.param | string, optional | The request field or parameter at fault, when there is one. |
There is no list of field errors: a validation error reports the first problem found.
Types
type | Status | Meaning |
|---|---|---|
validation | 400 | The request is malformed or a field is invalid. |
unauthorized | 401 | No credential, or the credential is unknown, revoked or expired. Comes with a WWW-Authenticate header. |
forbidden | 403 | The credential lacks a permission or scope, or a policy forbids the action (paused inbox, suppressed recipient, blocked recipient, inbox cap). |
account_suspended | 403 | MachineMail suspended the organization (writes refused; reads, exports and inbound still work) or its sending (no outbound; everything else works). message says which and how to appeal. Do not retry. |
not_found | 404 | The resource does not exist, or exists outside the credential’s scope. Unknown routes too. |
conflict | 409 | A uniqueness or state conflict (address taken, domain registered, pod not empty). |
rate_limited | 429 | Too many requests. See Rate limits. |
internal | 500 | An unexpected server error. The message is always Internal server error. |
not_implemented | 501 | The deployment has no email transport configured, so it cannot send. |
not_implemented is not in the OpenAPI ErrorResponse enum, but the API returns it.
A 401 always carries:
An unknown route returns 404 with Route not found: <METHOD> <path>.
Messages by area
These are the messages the API returns today. param is shown where it is set.
Requests
| Status | Message | param |
|---|---|---|
400 | Request body must be a JSON object | |
400 | <name> must be a number | the parameter |
400 | <name> must be an ISO 8601 timestamp | the parameter |
400 | <name> must be a list | the parameter |
400 | q is required | q |
400 | format must be mbox or jsonl | format |
400 | message_ids must be a list |
Authentication and scope
| Status | Message |
|---|---|
401 | Missing or invalid API key (Not signed in on cookie-only /auth/* routes) |
403 | API key lacks the <permission> permission |
403 | Only organization-scoped credentials can create API keys |
403 | A key with limited permissions cannot create an unrestricted key (param: permissions) |
403 | Requested permissions exceed the current key (param: permissions) |
403 | Only organization-scoped credentials can create pods |
403 | Only organization-scoped credentials can update the organization |
403 | Only organization-scoped credentials can remove suppressions |
403 | Complaint suppressions can only be removed by MachineMail support; email support@machinemail.dev (param: email) |
403 | Only organization-scoped credentials can manage agent registrations |
403 | Organization scope required |
403 | Inbox-scoped credentials cannot create inboxes |
403 | Inbox-scoped credentials cannot delete their inbox |
403 | Inbox-scoped credentials cannot add domains |
404 | Pod not found: <pod_id> |
404 | Inbox not found: <inbox_id> |
Inboxes
| Status | Message | param |
|---|---|---|
400 | username may contain a-z, 0-9 and . _ + - (1-64 characters) | username |
400 | ttl_seconds must be a positive number | ttl_seconds |
400 | status must be active or paused | status |
403 | Inbox limit reached for <domain> (<cap> per organization). Add your own domain for more. | domain |
404 | Domain not found: <domain> | domain |
409 | Inbox with email <email> already exists | username |
Sending
| Status | Message |
|---|---|
400 | At least one recipient is required |
400 | Invalid recipient: <address> |
400 | Too many recipients: <n> across to, cc and bcc. A message can have at most <max> recipients; send separate messages instead. (param: to; <max> is 50 by default) |
400 | Too many recipients: <n> across to, cc and bcc. Sandbox inboxes can have at most <max> recipients per message. Have your operator claim this inbox to lift sandbox limits (see /auth.md). (param: to; unclaimed sandbox inboxes, <max> is 5 by default) |
400 | attachments[<i>] needs content (base64) or url |
400 | attachments[<i>].url is larger than 25 MB, or another reason the URL could not be fetched |
403 | Inbox <inbox_id> is paused |
403 | Recipient <address> is suppressed (<reason>) |
403 | Sending to <address> is blocked by a send list |
429 | Sandbox inboxes can send <n> messages per 24 hours. Have your operator claim this inbox to lift sandbox limits (see /auth.md). (with Retry-After; <n> is 10 by default) |
429 | Sandbox inboxes can email <n> different recipients per 24 hours; this inbox has used <used>, and this message adds <new> new. Have your operator claim this inbox to lift sandbox limits (see /auth.md). (with Retry-After; <n> is 10 by default) |
501 | No email transport configured. Cannot send messages. |
The OpenAPI description of inbox status says sends to a paused inbox return 403 with code inbox_paused. The API returns 403 with type forbidden and the message above; there is no inbox_paused code in the body.
URLs (webhooks and attachment URLs)
400 when a URL is not http(s), contains credentials, points at a private or loopback address, or does not resolve. Examples: url must be an http(s) URL, url must point at a public host.
Other resources
| Status | Message | param |
|---|---|---|
400 | send_at must be an ISO timestamp | send_at |
400 | Unknown event type: <type> | event_types |
400 | domain must be a valid hostname | domain |
400 | catch_all_ttl_seconds must be positive | catch_all_ttl_seconds |
400 | name cannot be empty | name |
400 | direction must be send, receive or reply | direction |
400 | type must be allow or block | type |
400 | entry is required | entry |
400 | email is required | email |
400 | period must be between 1 and 86400 seconds | period |
400 | start must be before end | |
400 | window must be a multiple of period | window |
400 | Unknown permission: <name> | permissions |
400 | expires_at must be an ISO timestamp | expires_at |
404 | <Resource> not found or <Resource> not found: <id> for threads, messages, drafts, webhooks, deliveries, domains, events, API keys, list entries, suppressions, trust reports, attachments and raw messages | |
409 | <domain> is a shared domain | domain |
409 | Domain <domain> is already registered | domain |
409 | The default pod cannot be deleted | |
409 | Pod still has inboxes; delete them first | |
409 | <collection> conflicts with an existing record |
Raw injection (POST /inbound/raw)
| Status | Message |
|---|---|
400 | Message too large (over 40 MB) |
400 | No recipient: set X-Recipient or a To header |
404 | No inbox found for recipients: <addresses> (param: recipient) |
403 | Inbox <inbox_id> is paused |
403 | API key lacks the inbound_raw permission (full-access keys too) |
Agent onboarding errors
/agent/* and /oauth2/* follow OAuth conventions instead of the envelope. Their bodies look like this and carry Cache-Control: no-store:
| Endpoint | error values (status 400 unless shown) |
|---|---|
POST /agent/identity | invalid_request, anonymous_not_enabled, service_auth_not_enabled, identity_assertion_not_enabled, invalid_issuer, invalid_signature, invalid_audience, expired, invalid_client_id, missing_verified_email, replay_detected, rate_limited (429, with Retry-After), login_required (401, with max_age), interaction_required (401, with claim_token and claim) |
POST /agent/identity/claim | invalid_request, invalid_claim_token (404, or 401 with WWW-Authenticate: Bearer error="invalid_token"), claimed_or_in_flight (409), claim_expired (410) |
POST /oauth2/token | invalid_request, unsupported_grant_type, invalid_grant, invalid_client (401), invalid_target, authorization_pending, slow_down, expired_token, access_denied |
POST /agent/event/notify | {"err": "...", "description": "..."} with invalid_request, invalid_issuer, invalid_key or invalid_audience |
Human claim lookups in the dashboard (GET and POST /auth/claim) use the standard envelope: 400 user_code must be 6 digits, 404 Claim code (unknown or expired) not found, 403 This agent can only be claimed by <masked email>, 429 Too many claim attempts....
MCP errors
MCP tools report API errors as a tool result with isError: true. The text content is <type>: <message> and structuredContent is the error envelope above. Transport errors (wrong method, server failure) are JSON-RPC errors with code -32000.
Handling errors
- Retry
429after the number of seconds inRetry-After, when present. - Retry
500with backoff. Do not blindly retry sends: a send that timed out may have gone out. Use drafts with aclient_idwhen you need a retry-safe send. - Do not retry
400,403,404or409without changing the request.