errors.md


title: "Errors" description: "The MachineMail API error envelope, every error type and the messages each area returns, plus agent onboarding and MCP errors and how to handle them." canonical: "https://machinemail.dev/docs/reference/errors"

Errors

The MachineMail API error envelope, every error type and the messages each area returns, plus agent onboarding and MCP errors and how to handle them.

Source: https://machinemail.dev/docs/reference/errors

The envelope

Every /v0 error, and errors from /health, /inbound/* and /auth/*, has this body:

{
  "error": {
    "type": "validation",
    "message": "username may contain a-z, 0-9 and . _ + - (1-64 characters)",
    "param": "username"
  }
}
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.
error.code string, optional A more specific reason, on some errors. On a 403: signup_address_not_allowed (new accounts need an email address outside MachineMail: sign-up, invitations, and creating an organization), disposable_email_not_allowed (MachineMail doesn't accept disposable email addresses for new accounts, or as recipients of an unclaimed sandbox inbox's sends) or mailbox_has_account (the mailbox already has an account: Gmail dots and + aliases count as the same mailbox).
error.limit_key, error.limit, error.used, error.resets_at optional plan_limit_exceeded only. See Plan limits.

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.
plan_limit_exceeded 402 The organization's plan does not allow this until the allowance resets or the organization upgrades. Comes with the limit, the usage and, when waiting helps, resets_at and a Retry-After header. See Plan limits.
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 Sending is not available. Contact support@machinemail.dev.
unavailable 503 A part of MachineMail the request needs is briefly unavailable (for example the scheduler, for a draft with send_at). Nothing was changed: retry with backoff.

not_implemented is not in the OpenAPI ErrorResponse enum, but the API returns it.

A 401 always carries:

WWW-Authenticate: Bearer resource_metadata="https://api.machinemail.dev/.well-known/oauth-protected-resource"

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)
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)
400 attachments[<i>] needs content (base64) or url
400 attachments[<i>].url is larger than 10 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
403 Sandbox inboxes can't send to disposable email addresses. Claim this agent to remove sandbox limits. (param to, cc or bcc: the field holding it; code: disposable_email_not_allowed; unclaimed sandbox inboxes only)
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)
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)
501 Sending is not available right now. Contact support@machinemail.dev.

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 slug cannot be changed: it is fixed when the organization is created slug
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)

Plan limits

Free plan limits (how they are enforced) refuse a request with 402 plan_limit_exceeded. Paid plans are never refused, except past their trusted domains limit. The body has four extra fields:

Field Type Meaning
error.limit_key string messages_per_month, messages_per_day, storage_bytes, custom_domains, seats, held_messages, held_bytes, spam_storage_bytes, free_organizations or trusted_domains.
error.limit integer The value of that limit for your organization (bytes for storage_bytes, held_bytes and spam_storage_bytes).
error.used integer How much of it was used before this request.
error.resets_at string, optional When the limit resets (ISO 8601, UTC). Absent when waiting does not help: custom domains, seats, trusted domains, or a send larger than the limit.
{
  "error": {
    "type": "plan_limit_exceeded",
    "message": "The Free plan allows 100 messages a day (inbound messages and outbound recipients together); this organization has used 98 and this send needs 3. The allowance resets 2026-10-15T00:00:00Z. Upgrade at https://machinemail.dev/app/billing.",
    "param": "to",
    "limit_key": "messages_per_day",
    "limit": 100,
    "used": 98,
    "resets_at": "2026-10-15T00:00:00.000Z"
  }
}
Status Message param
402 The Free plan allows 3,000 messages a month (inbound messages and outbound recipients together); this organization has used <used> and this send needs <n>. The allowance resets <resets_at>. Upgrade at https://machinemail.dev/app/billing. (with Retry-After) to
402 The same with 100 messages a day, resetting at the next 00:00 UTC (with Retry-After) to
402 The Free plan allows <limit> messages a month (inbound messages and outbound recipients together) and this send has <n> recipients, so it can never be sent on this plan. Send it to fewer recipients, or upgrade at https://machinemail.dev/app/billing. to
402 The Free plan allows 1 custom domain and this organization has <n>. Upgrade at https://machinemail.dev/app/billing. domain
402 The Free plan allows 1 member and this organization has <n> (including pending invitations). Upgrade at https://machinemail.dev/app/billing. email
402 This organization reached its Free plan limit and already holds <n> undelivered messages, the most it may hold; this message was not stored. Upgrade at https://machinemail.dev/app/billing. (POST /inbound/raw only)
402 This organization reached its Free plan limit and already holds <n> MB of undelivered mail; with this message it would pass the <max> MB it may hold, so this message was not stored. Upgrade at https://machinemail.dev/app/billing. (held_bytes; POST /inbound/raw only)
402 The Free plan includes 1 GB of storage and this organization uses <used> GB (messages, drafts and held mail). Delete mail or drafts to make room, or upgrade at https://machinemail.dev/app/billing. (creating or growing a draft; removing SPAM or QUARANTINE) attachments, add_attachments or remove_labels
402 The messages-a-month or messages-a-day error (this message needs 1), when removing SPAM or QUARANTINE from a message would take the organization past it
402 The Free plan allows 3,000 messages a month (inbound messages and outbound recipients together); this organization has used <used> and the agent's inbox holds <n> received messages, so it cannot be claimed into this organization this month. Upgrade at https://machinemail.dev/app/billing. (POST /auth/claim)
402 Each account can own 1 organization on the Free plan and you own <n>. Upgrade at https://machinemail.dev/app/billing. (POST /auth/organizations)

Sends, replies, reply-all, forwards and draft sends return the send errors; MCP send tools return the same envelope in structuredContent. A scheduled draft that reaches a limit stays scheduled and is sent after the reset. Inbound mail past a Free limit is never refused to the sender: it is held.

Agent onboarding errors

/agent/* and /oauth2/* follow OAuth conventions instead of the envelope. Their bodies look like this and carry Cache-Control: no-store:

{
  "error": "authorization_pending",
  "error_description": "The claim has not been completed yet",
  "message": "The claim has not been completed yet"
}
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), disposable_email_not_allowed (403: an identity_assertion whose verified email is a disposable address and has no account)
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