Errors · MachineMail Docs
The envelope
Every /v0 error, and errors from /health, /inbound/* and /auth/*, has this body:
Error 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:
http
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. |
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. |
402 on a send
{
"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:
OAuth error body
{
"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
- Retry
429after the number of seconds inRetry-After, when present. - Do not retry
402beforeresets_at(also inRetry-After). Withoutresets_at, waiting does not help: change the request or upgrade. - 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.