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.

JSON
// 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.

reasonWhat happenedWhat to do
sending_locked_verify_emailThe workspace email address has never been verified, so nothing can go out.Verify the address from the dashboard. Nothing sends until you do.
workspace_pausedThe workspace is paused.An owner has to resume it. An agent cannot clear this itself.
mailbox_inactiveThe identity exists but its mailbox is not active.Check the agent page. Usually a mailbox that was suspended.
mailbox_receive_onlyThis mailbox can accept mail but never send it. Disposable inboxes are always receive-only.Send from a real identity instead.
mailbox_expiredA temporary mailbox is past its expiry.Create a new one.
mailbox_not_foundNo mailbox backs this key.Almost always a key belonging to a deleted agent.
domain_not_verifiedThe custom domain this address sits on has not finished verifying.Finish the DNS records. See /docs/domains.
too_many_recipients_max_5More than 5 recipients across to and cc.Split the send. The number in the code is the limit.
reply_target_not_foundreply_to_message_id names a message that does not exist.Look the message up first, or send it as a new message.
recipient_suppressedOne 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_exhaustedThe daily allowance is gone. Free plans only.Retry after retryAfter, or upgrade.
monthly_send_quota_exhaustedThe monthly allowance is gone and the plan does not meter overage.Retry after retryAfter, or upgrade.
monthly_spend_cap_reachedThe workspace hit the spend ceiling its owner set.Only an owner can raise it. Do not retry on a timer.
velocity_spike_detectedCold 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_cooldownA velocity trip already fired and the cooldown has not passed.Wait out retryAfter, after you have found out what tripped it.
duplicate_content_burst_detectedThe 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_bytesAttachments 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.

errorStatusWhat it means
unauthorized401No Authorization header, or one that is not a Bearer token.
invalid_key401The mmk_ key is unknown, revoked, or expired.
invalid_token401The OAuth access token is malformed or expired.
insufficient_scopetool resultThe token lacks the scope this tool needs. The response names it.
unknown_agent403X-MCPmailer-Agent names a handle that is not in this workspace.
ambiguous403An OAuth token covers several agents and none was named. Set X-MCPmailer-Agent: we refuse rather than guess which address mail leaves from.
no_agent403The account has no agent to act as yet.
no_workspace403The account has no workspace.
rate_limited429300 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.

errorWhereWhat it means
message_not_foundmessagesNo message with that id in this mailbox.
thread_not_foundthreadsNo thread with that id in this mailbox.
attachment_not_foundattachmentsThat message carries no such filename. The response lists the ones it does have.
attachment_bytes_missingattachmentsThe attachment is known but its bytes are gone.
raw_message_unavailableforwardThe original MIME is no longer stored, so it cannot be forwarded inline.
contact_not_foundcontactsNo contact with that id in this workspace.
note_not_found_or_not_grantednotesNo such note, or this agent has not been granted it.
secret_not_found_or_not_grantedvaultNo such secret, or this agent has not been granted it.
no_totp_on_secretvaultThe secret has no TOTP seed, so no code can be generated.
no_identityidentity, notes, vaultThe key resolves to no identity.
rule_not_foundfilteringNo filter rule with that id.
unknown_domainmailboxesThe domain named for a new mailbox is not one of yours.
not_founddomains, webhooksNo domain or webhook endpoint with that id.
temp_address_not_founddisposable inboxesNo disposable address with that id.
temp_address_not_found_or_expireddisposable inboxesThe address is gone or past its expiry.

Limits and conflicts

errorStatusWhat it means
custom_domain_limit_reached402The plan's custom domain allowance is used up. The response carries included and used.
webhook_limit_reached402The plan's webhook endpoint allowance is used up.
temp_address_limit_reachedtool resultToo many live disposable addresses. Let some expire.
handle_taken409Handles are unique across all of MCPmailer, not just your workspace.
domain_taken409That domain is already claimed by another workspace.
domain_in_use400The domain still has mailboxes on it, so it cannot be removed.
too_many_cards400A vCard import over 1,000 entries. Split the file.
no_recipients422reply_all had nobody to reply to once you were excluded.
vault_unavailable503The key store did not answer. Retry.
secret_unreadable500The 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.