Errors
Every refusal the API can return, what it means, and what to do about it.
There are two kinds of failure here, and they are shaped differently on purpose.
A refused send is a result. It comes back as HTTP 422 with a body of { "status": "rejected", "reason": ... } and it means the request was understood and declined for a reason an agent can act on. The SDKs do not throw on these: check result.status. Some carry retryAfter, an ISO timestamp saying when the thing that blocked you resets.
A held send is neither. If the agent is set to have a person read its mail first, the result is { "status": "held", "pendingId": ... } on a 202, meaning the request was accepted and is queued for review. It is not an error and must not be retried. See /docs/sending for how to find out what was decided.
Everything else is an error. It comes back with a 4xx or 5xx status and a body of { "error": ..., "hint": ... }, where hint is written to be read by whoever is debugging. The SDKs throw McpmailerError for these, carrying the status, the code, and the hint.
// 422: understood, declined. Not an exception in any SDK.
{ "status": "rejected", "reason": "monthly_send_quota_exhausted", "retryAfter": "2026-08-01T00:00:00Z" }
// 4xx / 5xx: an error. McpmailerError in the SDKs.
{ "error": "secret_not_found_or_not_granted", "hint": "No secret with that id is granted to this agent." }Send rejections
Returned as reason on a 422 from POST /v1/messages, and from send in every SDK. Two of them embed a number, so match on the prefix rather than the whole string.
| reason | What happened | What to do |
|---|---|---|
sending_locked_verify_email | The workspace email address has never been verified, so nothing can go out. | Verify the address from the dashboard. Nothing sends until you do. |
workspace_paused | The workspace is paused. | An owner has to resume it. An agent cannot clear this itself. |
mailbox_inactive | The identity exists but its mailbox is not active. | Check the agent page. Usually a mailbox that was suspended. |
mailbox_receive_only | This mailbox can accept mail but never send it. Disposable inboxes are always receive-only. | Send from a real identity instead. |
mailbox_expired | A temporary mailbox is past its expiry. | Create a new one. |
mailbox_not_found | No mailbox backs this key. | Almost always a key belonging to a deleted agent. |
domain_not_verified | The custom domain this address sits on has not finished verifying. | Finish the DNS records. See /docs/domains. |
too_many_recipients_max_5 | More than 5 recipients across to and cc. | Split the send. The number in the code is the limit. |
reply_target_not_found | reply_to_message_id names a message that does not exist. | Look the message up first, or send it as a new message. |
recipient_suppressed | One or more recipients previously bounced or complained. The offending addresses follow the colon. | Drop those addresses. Sending to them again is what gets a domain blocked. |
daily_send_quota_exhausted | The daily allowance is gone. Free plans only. | Retry after retryAfter, or upgrade. |
monthly_send_quota_exhausted | The monthly allowance is gone and the plan does not meter overage. | Retry after retryAfter, or upgrade. |
monthly_spend_cap_reached | The workspace hit the spend ceiling its owner set. | Only an owner can raise it. Do not retry on a timer. |
velocity_spike_detected | Cold sends jumped far above this mailbox's normal rate. | Stop and look at what the agent is doing. Recorded on the agent page. |
velocity_spike_cooldown | A velocity trip already fired and the cooldown has not passed. | Wait out retryAfter, after you have found out what tripped it. |
duplicate_content_burst_detected | The same body is going to many recipients in a short window. | Vary the message, or use fewer, better-targeted sends. |
attachments_too_large_max_10485760_bytes | Attachments exceed 10 MB in total. | Send a link instead. The number in the code is the limit in bytes. |
Rate limiting
A 429 with { "error": "rate_limited" } applies to the whole API, REST and MCP alike, at 300 requests per minute per key. It carries retry-after in seconds, which is a different thing from the retryAfter timestamp on a send rejection. Wait the stated interval rather than retrying immediately.
Authentication and routing
These decide whether a call happens at all, so they arrive before any tool runs.
| error | Status | What it means |
|---|---|---|
unauthorized | 401 | No Authorization header, or one that is not a Bearer token. |
invalid_key | 401 | The mmk_ key is unknown, revoked, or expired. |
invalid_token | 401 | The OAuth access token is malformed or expired. |
insufficient_scope | tool result | The token lacks the scope this tool needs. The response names it. |
unknown_agent | 403 | X-MCPmailer-Agent names a handle that is not in this workspace. |
ambiguous | 403 | An OAuth token covers several agents and none was named. Set X-MCPmailer-Agent: we refuse rather than guess which address mail leaves from. |
no_agent | 403 | The account has no agent to act as yet. |
no_workspace | 403 | The account has no workspace. |
rate_limited | 429 | 300 requests per minute per key. Carries retry-after in seconds. |
Not found
A 404 means the thing does not exist, or exists and this key is not allowed to see it. The two are deliberately not distinguished: telling an agent that a secret exists but is not granted to it would leak the workspace it cannot enumerate.
| error | Where | What it means |
|---|---|---|
message_not_found | messages | No message with that id in this mailbox. |
thread_not_found | threads | No thread with that id in this mailbox. |
attachment_not_found | attachments | That message carries no such filename. The response lists the ones it does have. |
attachment_bytes_missing | attachments | The attachment is known but its bytes are gone. |
raw_message_unavailable | forward | The original MIME is no longer stored, so it cannot be forwarded inline. |
contact_not_found | contacts | No contact with that id in this workspace. |
note_not_found_or_not_granted | notes | No such note, or this agent has not been granted it. |
secret_not_found_or_not_granted | vault | No such secret, or this agent has not been granted it. |
no_totp_on_secret | vault | The secret has no TOTP seed, so no code can be generated. |
no_identity | identity, notes, vault | The key resolves to no identity. |
rule_not_found | filtering | No filter rule with that id. |
unknown_domain | mailboxes | The domain named for a new mailbox is not one of yours. |
not_found | domains, webhooks | No domain or webhook endpoint with that id. |
temp_address_not_found | disposable inboxes | No disposable address with that id. |
temp_address_not_found_or_expired | disposable inboxes | The address is gone or past its expiry. |
Limits and conflicts
| error | Status | What it means |
|---|---|---|
custom_domain_limit_reached | 402 | The plan's custom domain allowance is used up. The response carries included and used. |
webhook_limit_reached | 402 | The plan's webhook endpoint allowance is used up. |
temp_address_limit_reached | tool result | Too many live disposable addresses. Let some expire. |
handle_taken | 409 | Handles are unique across all of MCPmailer, not just your workspace. |
domain_taken | 409 | That domain is already claimed by another workspace. |
domain_in_use | 400 | The domain still has mailboxes on it, so it cannot be removed. |
too_many_cards | 400 | A vCard import over 1,000 entries. Split the file. |
no_recipients | 422 | reply_all had nobody to reply to once you were excluded. |
vault_unavailable | 503 | The key store did not answer. Retry. |
secret_unreadable | 500 | The secret is stored but did not decrypt. Retrying will not help. |
Bad requests
All 400s, all meaning the request was malformed rather than refused. invalid_json is a body that did not parse; invalid_request is a body that parsed but failed validation. The rest name the field they are about: body_required, name_required, fact_required, handle_required, events_required, id_required, https_url_required, invalid_handle, invalid_email, invalid_domain, invalid_value, invalid_filter_mode, not_your_domain, nothing_to_update, no_valid_cards, too_many_cards. Webhook registration adds a few of its own: private_url_not_allowed for an endpoint on a private or loopback address, credentials_in_url for a token in the URL rather than a header, and reserved_header_name, invalid_header_name, invalid_header_value, too_many_headers, invalid_template, template_too_large for custom headers and body templates.
Vault failures
vault_unavailable is a 503: the key store did not answer and the secret was not read. It is worth retrying. secret_unreadable is a 500 and is not: the ciphertext is there but did not open, which means something is wrong on our side and retrying will not fix it.
Over MCP
The transport-level refusals above still arrive as HTTP status codes. Everything a tool itself refuses comes back as an ordinary tool result whose JSON carries an error field, because a tool that failed still returned. insufficient_scope means the token or key was not granted the scope this tool needs, and the required scope is named in the response. The REST API returns the same error as a 403 when a restricted key reaches past its grant.