MOLTED EMAIL

Errors & Policy Blocks

Every policy block reason and suppression reason explained.

When a send request is blocked by the policy engine, the response includes a reason field. A status: "pending_approval" response also carries a reason (why a human needs to decide) and a status: "scheduled" response may carry one too (why it was deferred rather than sent immediately). This page documents every reason the send path (molted send, threads reply, send batch, and the dry runs send simulate / propose) can actually return. The CLI's own guidance for each reason comes from one table, and a test checks that every reason in it appears on this page; the descriptions here are kept by hand. Journey steps and follow-ups record the same reasons on the send's trace (see Journeys).

For errors outside the send path (auth, scopes, approvals, team addresses and so on), see Other error codes.

Policy engine

ReasonDescription
duplicateA request with the same dedupeKey was already processed. The response's requestId is the ORIGINAL send, not a new one -- use a unique key per send.
cooldownThe same recipient got the same template from the same mailbox too recently (the tenant's cooldownMinutes, 10 by default). Wait for the cooldown to expire, or use a different template. A reply that carries a threadId skips the cooldown when the recipient is that thread's contact (or the Reply-To its inbound mail resolves to) in the thread's own mailbox and they have written since your last message to them there: one reply per message received. Another recipient's thread id, or a thread with nothing new from them, doesn't. send simulate also returns cooldownExpiresAt.
suppressedThe recipient is on the suppression list (e.g., from a prior bounce or complaint). See Suppression reasons below for the underlying cause.
no_consentThis tenant has requireConsentForMarketing on and the recipient has no active consent on file. Off by default -- see Suppressions & Consent. Record consent with molted consent record, or check it first with molted consent check.
disengagedThe workspace has disengaged auto-suppression on, and the recipient has had at least 5 sends (by default) and no open, click or reply in 90 days (by default), so automated sends are paused for them. Nothing self-serve turns this on today: no setting in the portal, API or CLI switches it, so you only see this reason on a workspace where Molted staff have set it up by hand.
active_opportunityThe contact's deal stage is demo_scheduled, proposal, negotiation or contract_sent. Automated sends are paused to avoid interference.
tenant_pausedSending is paused for the whole workspace. A person, or an agent, paused it, and only a person resumes it; see Pause and resume sending.
mailbox_pausedThe sending mailbox is paused (e.g. reputation). Run molted mailboxes unpause <mailboxId>.
template_not_approvedThe template requires approval and has not been approved yet.
template_lint_failedThe template failed content linting checks (e.g., missing unsubscribe link, spam triggers).
negative_signal_budget_exceeded50 or more bounces plus complaints across the tenant in the last 24 hours. Every send is blocked until the count drops; molted budget shows negativeSignals.
budget_exceeded / monthly_budget_exceededPresent in the policy engine's type, but not set on the real send path today -- the tenant daily/monthly send budget context is never populated by a caller.

Billing

ReasonDescription
trial_not_activatedYour account hasn't picked a plan yet. Run molted billing setup --plan solo for a paid plan, or, only when the owner is a verified human (molted billing status lists activate_free in actions), molted billing activate-free for the free tier.
subscription_expiredYour subscription has expired (grace period over, or the plan lapsed). Run molted billing status to check the plan, then molted billing setup --plan solo to renew.
free_sends_pausedChecked with the send quota. Molted has paused sending for every account on the Free plan for now, for example while it deals with abuse on the shared sending infrastructure. It is not about your send and there is nothing to fix: retry later with a new dedupeKey. Paid plans keep sending. Journey steps and scheduled follow-ups wait and retry on their own; an approval stays pending (HTTP 429, error: "free_sends_paused") until sending resumes.

Quota

Exceeding a send quota window blocks with the specific window's reason, never a generic rate_limited -- see Rate Limits for what counts toward each window.

ReasonDescription
hourly_limit_exceededThe hourly send quota is used up.
daily_limit_exceededThe daily send quota is used up.
monthly_limit_exceededThe monthly send quota is used up for this billing period.
overage_cap_exceededYour plan's overage cap for this billing period was reached.
rate_limitedFallback only -- a quota/lease check that failed without a specific sub-reason. A real send (and its dry run, send simulate / dry_run) almost always returns one of the *_limit_exceeded reasons above instead.

Warmup and domain throttling

ReasonDescription
warmup_limitThe sending domain is still warming up and hit today's cap. Only transactional templates (id starting with _), sends with an explicit scheduledAt, and sends with deferOnWarmup: false block like this; other sends are deferred instead (warmup_defer, below). Check molted domains warmup <domainId>.
warmup_horizon_exceededThe sending domain's warmup schedule is booked out too far to defer this send; the response carries projectedDate.
domain_throttledToo many sends to this recipient's domain this hour -- a per-tenant, per-recipient-domain cap set by Molted operators (off by default), separate from the sending domain's own limits below.
domain_daily_limit_exceeded / domain_hourly_limit_exceeded / domain_minute_limit_exceededThe sending domain hit its own daily / hourly / per-minute rate limit. Check molted domains rate-limits <domainId>.
domain_rate_limitedFallback only, when the per-domain check fails without a specific sub-reason.

Template and content

ReasonDescription
template_not_foundThe template slug or ID does not exist for this tenant.
template_render_failedThe template failed to render (likely a missing required variable).
missing_physical_addressMarketing emails require a physical mailing address (CAN-SPAM). Run molted settings update --physical-address "<address>" (or PATCH /v1/agent/config/settings / /v1/me/settings with physicalAddress). Setting one where none exists is not a loosening, so an agent key may do it directly; changing or clearing an existing one is human-gated (see Loosening a control).

Mailbox and domain resolution

ReasonDescription
no_verified_domainNo sender address could be resolved for this send at all. Rare: every account can send from the shared domain.
mailbox_not_activeThe mailbox is not active yet. Run molted mailboxes verify <id>.
sender_domain_not_verifiedThis mailbox's custom domain isn't verified for your account (unverified, removed, or a domain the tenant doesn't own). Verify it with molted domains verify <domainId> (see Domains).
mailbox_scope_denied (403)The send names a mailbox your key isn't scoped to (the code is the error's message).
thread_mailbox_mismatch (400)The send's threadId is a thread of another mailbox than its mailboxId. A send into a thread goes out from that thread's mailbox: leave mailboxId out, or pass the thread's own.

Mailbox create and restore errors

These are error codes from creating or restoring a mailbox, not send reasons:

CodeDescription
mailbox_archived (409)The address belongs to a mailbox deleted in the last 14 days. Restore it instead (POST /v1/agent/mailboxes/:id/restore, mailboxId is in the error). See Deleting a mailbox.
human_archive (403)A person deleted this mailbox, so only a person can restore it, from the portal.
archive_expired (410)The 14 days to restore a deleted mailbox are over; it is being deleted for good.
mailbox_limit (403)Creating or restoring the mailbox would go over your plan's mailbox limit. Delete another mailbox or upgrade first.
domain_conflict (409)The mailbox's domain belongs to another workspace.
address_taken (409)The address is already in use.

Coordination and safety

ReasonDescription
lease_conflictAnother agent currently holds a coordination lease on this contact. The response has requestId: null and leaseHolder ({ agentId, agentName, expiresAt }). Wait for the lease to expire or be released. Not checked on batch sends.
canary_violationA canary token (an internal probe value) came back in the outbound payload -- usually a prompt-injection attempt from the recipient. Checked on the mailbox-scoped outbound API (/v1/agent/outbound/send when it carries a threadId, and /v1/agent/outbound/reply), never on /v1/agent/request-send; the request ID is null. Blocks only while the safety setting blockCanaryViolations is on (the default); with it off, the thread is flagged and the send goes through.
thread_taken_overA human has taken over this thread, so this mailbox can't email the contact until they hand it back. A single (non-batch) send gets this as an HTTP 409 with { error, message, threadId } (plus contactEmail when the send went to that contact outside the thread) instead of a blocked body; a batch item gets it as an ordinary blocked result.

Held for human approval (status: "pending_approval")

ReasonDescription
autonomy_levelThe mailbox's autonomy level requires approval for this recipient.
thread_risk_flaggedRecent messages on this thread were flagged as high risk (possible prompt injection).
content_checkThe content check flagged this email (possible phishing-like request, leaked personal data, deceptive claims, or spam). Only fires where Molted has the outbound content hold switched on (a platform flag, off by default) and the tenant is in its rollout.

The response also carries a human-readable message for each reason.

Review held items with molted overrides queue needs_approval; approving is human-only (molted auth login-link for a link to give them, or a human runs molted overrides approve <threadId>).

Deferred (status: "scheduled")

ReasonDescription
warmup_deferDeferred to a future warmup slot for the sending domain; the response carries scheduledAt. Nothing to do -- it sends automatically.

A scheduled response with no reason means the caller passed an explicit scheduledAt.

Batch item errors (results[].status: "error")

These are validation failures on one recipient in a send batch call, not policy blocks -- the rest of the batch is unaffected.

ReasonDescription
duplicate_in_batchTwo entries in the same batch call used the same dedupeKey.
recipient_email_requiredThis batch entry has no (or an empty) recipient email.
dedupe_key_requiredThis batch entry has no dedupeKey.
invalid_humanize_style / invalid_humanize_providerThis entry's humanizeStyle or humanizer provider isn't a known value.
internal_errorAn unexpected error on this recipient specifically; other recognizable reasons pass through unchanged (an unrecognized error's message can also come through as the reason). Use molted trace <requestId> if a requestId is present.

A whole batch over 500 entries is refused before anything runs: 400 { "error": "batch_too_large", "message", "field", "max": 500, "received" } (also on simulate-batch, batch/next-best-action and batch/classify-intent).

Single-send request errors

These are refused with an HTTP error before the policy engine runs, and nothing is sent:

StatusErrorCause
400dedupe_key_required/v1/agent/request-send without a dedupeKey (or idempotencyKey), or with a blank one.
400invalid_humanize_style / invalid_humanize_providerAn unknown humanizeStyle or humanizer provider.
400invalid_reply_toA malformed replyTo.
400scheduledAt must be a valid ISO 8601 timestamp / scheduledAt must be at least 60 seconds in the future / scheduledAt must be at most 1 year in the future / scheduledAt must be an ISO 8601 timestamp with an explicit UTC offset or Z / scheduledAt is not a real calendar date and timeA bad scheduledAt on request-send.
400At most 20 attachments are allowed per send., Attachment(s) not found: ..., Attachment(s) not available: ..., Attachment(s) not yet scanned: ...An attachments problem (see Storage).
403pending_classification, inbound_quota_held, quarantined_requires_operator (as message)Forwarding an inbound attachment that isn't readable yet.

Suppression reasons

When a recipient is on the suppression list (suppressed block reason), the suppression was created for one of these reasons:

Reason codeDescription
complaintThe recipient previously marked an email as spam.
hard_bounceA previous email to this address hard-bounced (e.g., address does not exist).
soft_bounce3 soft bounces in 30 days. Added automatically and expires after 90 days.
manual_dncThe recipient was manually added to the do-not-contact list (including a person's "keep agents away" on a contact).
legal_requestA suppression was added due to a legal request, including a GDPR contact deletion.
role_accountThe address is a role account (e.g., info@, admin@) that should not receive automated email.
no_engagementAdded by the disengagement scan for a recipient with no engagement. The scan only runs for a workspace with disengaged auto-suppression on, which is not self-serve today; you can still add this reason yourself.
domain_suppressedThe recipient's whole domain is suppressed.
unsubscribe_linkThe recipient used the hosted one-click unsubscribe link. Only the server sets this.
unsubscribe_requestAn inbound reply was classified (or approved) as an unsubscribe request.

When you add a suppression yourself, reasonCode can be complaint, hard_bounce, manual_dnc, legal_request, role_account, domain_suppressed or no_engagement.

Validation errors

These errors are returned as 400 Bad Request before the policy engine runs. They indicate malformed input rather than a policy decision.

ErrorCauseFix
recipientEmail must be an emailOn POST /v1/send/request and the mailbox outbound routes (/v1/outbound/send, /v1/outbound/schedule-followup): the address is malformed. /v1/agent/request-send (what molted send uses) only checks that it is a string (recipientEmail must be a string).Use a properly formatted email address (e.g. user@example.com). Validate before sending.
templateId must be a stringtemplateId is not a string (and, on /v1/send/request and /v1/outbound/*, when it is missing). On /v1/agent/request-send it is optional and defaults to _default.Pass a valid template slug or ID.
dedupeKey must be a string / dedupeKey must not be empty / dedupeKey must contain at least one non-whitespace characterOn /v1/send/request and /v1/outbound/*: dedupeKey is missing, not a string, or blank. On /v1/agent/request-send a missing key gives dedupe_key_required (above).Pass a unique deduplication key.
<field> must be a string / must be an object / must be an array / must be a boolean value / each value in nested property sends must be either object or arrayOn the agent routes (request-send, simulate-send, propose-email, classify-intent, next-best-action, schedule-followup, the batch/* routes, register, coordination/heartbeat, coordination/lease, coordination/consensus and its /vote, outcomes/ingest, segments, search, domains and domains/:id/rate-limits, adopt/invite, billing/setup-link, agent/humanizer/preview and templates/:id/render): a field has the wrong type, e.g. payload or filterGroup is not a JSON object, attachments or sends is not an array (or holds something other than objects), or humanize is a string.Send the type the API reference shows. Fields the API does not know are ignored, not rejected.
scheduledAt must be an ISO 8601 timestamp string / delayMinutes must be an integer number of minutes / cancelOnReply must be a booleanOn request-send and schedule-followup: scheduledAt is neither a string nor a number, delayMinutes is not a whole number, or cancelOnReply is not true/false.Pass an ISO 8601 timestamp, a whole number of minutes, and a boolean.
agent_bound_to_key403 on the heartbeat, lease, release-lease and consensus routes: your API key has no bound agent and the request names (or releases the lease of) an agent another API key is bound to. Only that key acts as it.Use the key bound to that agent, or act as an agent no key is bound to.
agent_mismatch403 on the heartbeat, lease, release-lease and consensus routes: your API key is bound to an agent (keyAgentId in the body) and the request names a different one, or tries to release another agent's lease.Act as the key's own agent (omit the agent, or name it), or use a key bound to the agent you need.
consensus_closed409 on coordination/consensus/:id/vote: the request is already approved, rejected or expired, or its timeoutAt has passed. The body is { "error": "consensus_closed", "status": "<current status>" } and the vote is not recorded.Read the outcome with get_consensus / molted agents consensus get; create a new request if the decision needs to be taken again.
durationMinutes must be a number of minutes / timeoutMinutes must be a number of minutesOn coordination/lease and coordination/consensus: durationMinutes/timeoutMinutes is neither a number nor a numeric string. Unlike delayMinutes above, fractions are fine (e.g. 1.5).Pass a number (or a numeric string) of minutes; a whole number is not required.

Validation errors return an array of messages describing each invalid field:

{
  "statusCode": 400,
  "message": ["recipientEmail must be an email"],
  "error": "Bad Request"
}

Human-only decisions

Approving, rejecting, releasing and editing held items is a decision for a person, not an agent. When an agent key makes one of these calls on an account that enforces this, the API answers 403. The kind names what is held:

kindRoutes
sendPATCH /v1/send/approvals/:id
threadPOST /v1/override/:threadId/approve, /reject, /edit; /delete-spam (classifier spam only) and /not-spam (when gated); POST /v1/threads/restore when the restore needs it
heldPOST /v1/override/held-messages/:messageId/release and /reject
inboundPOST /v1/inbound/:id/approve and /reject
inbound-intentPOST /v1/inbound/bulk-approve
templatePATCH /v1/templates/approvals/:id

The /v1/override/* routes are also reachable as /v1/agent/override/*.

{
  "error": "human_decision_required",
  "message": "This decision needs a human. Share the decisionUrl with a person on your team; they can approve or reject it in the Molted portal.",
  "decisionUrl": "https://molted.email/app/decide/send/APPROVAL_ID",
  "kind": "send",
  "id": "APPROVAL_ID",
  "reason": "enforce"
}

Don't retry. Send the decisionUrl to a person on your team. reason is enforce, warn_expired (the account's 30-day warning period is over) or thread_taken_over (a human has taken the thread over; the message then says only a person can approve or release messages on it). See Approval Queues.

Loosening a control

Agents may make an account safer, never less safe. Pausing, lowering autonomy, tightening a safety check, turning the humanizer off and narrowing or revoking your own keys always work. Changes that loosen a control are human decisions, gated the same way as approvals: enforce answers 403, warn lets the call through with a Molted-Deprecation: human-gate header, off allows it.

ControlAgents mayNeeds a human
PausePause a mailbox or the accountResume a paused mailbox (POST /v1/agent/mailboxes/:id/unpause, or PATCH /v1/agent/mailboxes/:id with status: "active") or a paused account (POST /v1/ops/resume, or resolving its pause incident), also after an auto-pause
AutonomyLower itRaise it (1 to 2, 2 to 3)
Safety settings and spam-filter rulesTurn a check on, pick a stricter action (from deliver to quarantine to reject), lower spamThreshold or maxLinksThreshold, add blockedKeywords, remove allowedSendersThe opposite of each
Humanizer (account, agent, or mailbox config)Turn it off (a mailbox: config.humanizer.enabled: false)Turn it on, or change its style while it is on (a provider value is accepted and ignored, and never gated). For a mailbox, "on" is its effective state given the account setting, and any change to its own style counts
Account settings (PATCH /v1/agent/config/settings, see Workspace Settings)Raise cooldownMinutes (0 to 1440); set physicalAddress when there is none; turn requireConsentForMarketing onLower cooldownMinutes; change or clear an existing physicalAddress; turn requireConsentForMarketing off
API keysMint keys within your own scope; narrow yourself or a key you minted; revoke yourself or a key you mintedAn all-mailbox key from a scoped key, a mailbox or permission you don't have, widening a key, narrowing or revoking a key you didn't mint
Adoption invitesInvite within your own scope; revoke an invite (a mailbox-scoped key only those it created; others are a 404)An invite wider than your own key
SuppressionsAdd a suppressionRemove one (DELETE /v1/suppressions/:id or /v1/ops/suppressions/:id), un-suppress a whole domain (DELETE /v1/suppressed-domains/:domain), or lift a contact's "keep agents away" (DELETE /v1/me/contacts/:id/keep-away)
Domain warmupCheck warmup statusSkip warmup (POST /v1/agent/domains/:id/warmup/skip) -- can't be undone
Domain rate limitsLower a limitRaise a limit, or clear one to unlimited (PATCH /v1/agent/domains/:id/rate-limits)
ContactsCreate, update, syncDelete a contact record (DELETE /v1/contacts/:id) -- irreversible, see Contacts
Mailbox address and catch-allSend the value already storedChange the address (case counts), or turn catchAll on or off, on PATCH /v1/agent/mailboxes/:id; create or clone onto an address another mailbox's catch-all receives; delete a catch-all mailbox (kind: "mailbox-address"), see Address and catch-all changes

A loosening attempt in enforce mode gets a 403 shaped like this (an autonomy raise, in this example):

{
  "error": "human_decision_required",
  "message": "Agents may tighten controls but not loosen them. This change needs a human: share the decisionUrl with a person on your team; they can make it in the Molted portal.",
  "decisionUrl": "https://molted.email/app/decide/autonomy/MAILBOX_ID",
  "kind": "autonomy",
  "id": "MAILBOX_ID",
  "reason": "enforce",
  "loosens": ["autonomyLevel 1 -> 3"]
}

The same loosenings are gated on their other routes too: autonomy also through PATCH /v1/agent/mailboxes/:id/autonomy and PATCH /v1/mailboxes/:id; safety settings through PUT /v1/agent/config/safety-settings, PUT /v1/ops/safety-settings and PUT /v1/spam-filter/rules; the humanizer through PUT /v1/agent/config/humanizer, PUT /v1/ops/humanizer and, for one agent, PUT /v1/agent/agents/:agentId/config (kind: "agent-config").

loosens lists what the change would loosen. kind is mailbox-resume, account-resume, autonomy, safety-settings, humanizer, tenant-settings, agent-config, api-key, adopt-invite, suppression, domain-warmup, domain-rate-limits, contact, mailbox-address or billing; for a key or invite that doesn't exist yet, id is new. Don't retry: send the decisionUrl to a person, or drop the loosening part of the change (a tightening on its own goes through). People signed in to the portal are never gated.

If a person changes the same control while an agent's request is being checked, and the agent's change would now loosen what is stored, the answer is 409 settings_changed with the loosens list, and nothing is written. Read the settings again and retry.

Pausing the whole account (not just one mailbox) is one such tightening/loosening pair -- pausing always works, resuming is usually gated the same way:

CLI
molted pause --reason "investigating a bounce spike"   # POST /v1/ops/pause -- always works
molted resume                                          # POST /v1/ops/resume -- usually needs a human, per the table above
molted incidents list                                  # GET /v1/ops/incidents -- always works
molted incidents ack <id>                              # PATCH .../incidents/:id {status: acknowledged} -- always works
molted incidents resolve <id>                          # PATCH .../incidents/:id {status: resolved} -- same gate as resume, if it's an open pause incident

Loosenings with no portal screen

Most loosenings have somewhere to go in the portal: a person resumes a mailbox or the account, raises autonomy, creates or manages a key, and edits the spam-filter rules and the humanizer under Settings > Safety. Five kinds do not. A decisionUrl for them opens a page that explains what the agent asked for; it has no form to make the change. This is how a person makes each one today:

kindWhat the agent triedHow a person does it today
tenant-settingsLower the cooldown, change or clear the postal address, turn consent offPATCH /v1/me/settings with the portal session, which is never gated. See Workspace Settings.
domain-rate-limitsRaise a domain's rate limit, or clear onePATCH /v1/me/domains/:domainId/rate-limits with the owner's portal session. See Sender Addresses & Domain Rate Limits.
suppressionRemove a suppression, or un-suppress a domainOnly two cases have a route for a person: Undo on a contact's Keep agents away (see Contacts), and undoing an approved unsubscribe within 10 minutes. Any other suppression, such as a bounce, a complaint or a domain, has none.
domain-warmupSkip a domain's warmupNone. Only the agent route exists, and it can't be undone.
agent-configChange another agent's configuration in a way that loosens a controlNone. Only the agent route exists (PUT /v1/agent/agents/:agentId/config).

There is no self-serve way to make the last three changes yet: an agent key is refused on a workspace in enforce mode, the default, and a person has nothing to click. On a workspace in off mode, or in warn mode while its warning period lasts, an agent key can make them directly.

The safety settings have a smaller version of the same gap. The portal edits only the six spam-filter fields; a person changes the per-verdict actions and the guardrail flags with PUT /v1/me/safety-settings and their portal session. See Safety Settings.

Billing decisions

Starting checkout through the session route (POST /v1/billing/checkout), opening the billing portal, and ending a trial early ("start now") are human decisions, even for an agent holding the workspace owner's own session (e.g. one created by molted auth signup): money changes need a person, no matter how the change would tighten or loosen anything else. They follow the workspace's human-gate mode like the loosenings above (enforce answers 403, warn lets it through with a deprecation header, off allows it). The session routes are owner-only (403 Only the workspace owner can manage billing first), and POST /v1/billing/start-now with a key also needs admin scope on all mailboxes. This doesn't apply to the agent Bearer-token checkout route (POST /v1/billing/setup-link) -- see below.

{
  "error": "human_decision_required",
  "message": "Billing changes need a person. Open the link to manage the plan.",
  "decisionUrl": "https://molted.email/app/decide/billing/TENANT_ID",
  "kind": "billing",
  "id": "TENANT_ID",
  "reason": "enforce",
  "loosens": ["agent-held session"]
}

(loosens is on the session-route answer; the key route's start-now answer has none.)

Don't retry: send the decisionUrl (or the link from molted auth login-link) to a person, or complete it yourself if you're a human on the portal's own session.

Free plan requires a verified human

Activating the Free plan (POST /v1/billing/activate-free) is a different kind of decision: it is refused outright, not delegated to a person via a decisionUrl, and it does not consult the workspace's human-gate mode -- warn or off don't loosen it. It succeeds only when the tenant's owner is a human account (principal_kind = 'human') with a verified email on a non-disposable domain.

An agent-owned tenant (e.g. one created by molted auth signup or any non-portal signup) gets:

{
  "statusCode": 403,
  "error": "free_plan_requires_human",
  "message": "The Free plan needs a person as the owner. This workspace is agent-owned, so activate-free is refused. Run `molted auth login-link --next /app/settings#plan` (POST /v1/agent/login-token) and follow what it returns...",
  "plans": ["solo", "team"],
  "setupLinkRoute": "/v1/billing/setup-link",
  "loginLinkRoute": "/v1/agent/login-token",
  "loginLinkCommand": "molted auth login-link --next /app/settings#plan",
  "claimRoute": "/v1/me/principal/upgrade"
}

To get Free, run molted auth login-link --next "/app/settings#plan" (POST /v1/agent/login-token) and follow what it returns: when the account's email is your human's own inbox, Molted emails them a link to set their password and take the account over; when it is one of your Molted mailboxes, you get a claim link to give them (see Links for your human). Once they are done, activate-free succeeds, also with your existing key. Or buy a paid plan with POST /v1/billing/setup-link (molted billing setup --plan solo, or the billing_checkout_link MCP tool): agents may always request a checkout link, since a human still has to complete the Stripe-hosted page.

A human owner whose email isn't verified yet gets:

{
  "statusCode": 403,
  "error": "email_verification_required",
  "message": "Verify your email before activating the Free plan. Check your inbox for the verification link, or resend it from the portal (Settings > Email)."
}

A verified human owner whose email domain is a known disposable provider (mailinator.com and the like) gets:

{
  "statusCode": 403,
  "error": "disposable_email_not_allowed",
  "message": "The Free plan needs a real, reachable email address. Use one from a regular provider, or choose a paid plan instead...",
  "plans": ["solo", "team"],
  "setupLinkRoute": "/v1/billing/setup-link"
}

The checks run in that order (agent-owned, then unverified, then disposable). Activating from a plan other than trial is a 400 (Cannot activate free plan from "<plan>" plan. Only trial accounts can activate the free tier.), as is a workspace whose plan is set by Molted support.

GET /v1/billing/status never advertises "activate_free" in actions unless the owner already qualifies, and its activateFreeBlockedReason names which of the three applies, so an agent driven purely off actions won't hit this 403 in the first place. Existing Free tenants (activated before this rule shipped, or comped by an admin) keep their plan -- this never claws one back.

Plan limits on settings

Turning the humanizer on (PUT /v1/agent/config/humanizer with "enabled": true, or an agent's own humanizer_enabled) on a plan that doesn't include it (Free) is refused before any human decision:

{
  "error": "humanizer_not_in_plan",
  "message": "The humanizer is not included in your plan. Upgrade to Solo (50 rewrites a month) or Team (300 a month) to turn it on.",
  "plan": "free"
}

Sends are never refused for this: a send whose rewrite will be skipped is accepted and carries warnings (see Humanizer).

Sending a previewed rewrite

A send with humanizePreviewId (Send exactly what you previewed) is refused, and nothing is sent, when the preview doesn't fit it. In a batch, only that recipient fails, with the code as its reason.

StatusErrorCauseFix
409humanize_preview_usedAnother send already used this preview id (an id sends once).Preview again for this send; in a batch, give each recipient its own preview.
409humanize_preview_mismatchThe email's html or text differs from what was previewed (another template, variable, body or footer).Preview the exact content you send, or send without the id.
404humanize_preview_not_foundUnknown or expired id (previews last 24 hours), another workspace's, or previewed for a different mailbox.Preview again (with this send's mailboxId if you set one).
400humanize_preview_conflicthumanize: false together with a preview id, or a humanizeStyle other than the one the preview was rewritten in.Pass one or the other; leave humanizeStyle out (the preview's style is delivered) or preview again in that style.
400invalid_humanize_preview_idThe id is not a preview id (a UUID).Pass the previewId the preview returned.

If the humanizer was turned off after the preview, the send is not refused: the original goes out with the warning humanize_preview_ignored_humanizer_off.

Other error codes

Machine-readable codes the API returns outside the send path. Most come as error in the body; ones marked (message) come as the message of a plain error.

Auth and scope

StatusCodeWhen
401missing_api_key (message)No Authorization header.
401invalid_api_key (message)The key is unknown or revoked.
401missing_tenant_id (message)A call to api.molted.email without tenantId (mailbox.molted.email, the CLI and the MCP server fill it in).
403mailbox_scope_denied (message)The key is scoped to some mailboxes and the route needs one it doesn't cover, or covers every mailbox.
403insufficient_scope (message)The key lacks the scope the route needs (for example manage).
403missing_scope:operator (message)The route needs the operator scope (alert status, quarantined content).
403human_requiredA human-only portal route (/v1/me/*) called by an agent, including an agent-held session.
403oauth_forbiddenA route closed to hosted MCP (OAuth) apps.
401invalid_tokenA rejected OAuth token.
400tenant_header_required / invalid_tenant_headerA phone-app request without an X-Molted-Tenant, or with one that is not a single workspace id. See Workspaces and X-Molted-Tenant.
403not_a_memberX-Molted-Tenant names a workspace the signed-in person doesn't belong to.
403auto_pause_web_onlyThe phone app tried to resume something that was paused automatically (an account auto_pause or a mailbox paused for its reputation). Resume it from the portal; see Pause and resume sending.
404incident_not_found (message)PATCH /v1/ops/incidents/:id with an unknown incident, or one from another workspace.

Approvals and held items

StatusCodeWhen
404approval_not_foundNo such approval.
409approval_not_pending / already_decidedIt was already decided.
410expiredThe approval expired.
409superseded / batch_changed / thread_changedWhat you approved changed in the meantime; reload and decide again.
409thread_in_review / thread_trashed / pending_send_approval / quarantine_verdictThe thread's state doesn't allow the action.
429quota_exceeded / free_sends_pausedApproved, but the send would pass a quota (or Free sending is paused); reason says which.
422attachment_blockedAn attachment on the approved send isn't sendable (attachmentIds lists them).
502enqueue_failedThe approved send couldn't be queued; retry.

Plans, team addresses and limits

StatusCodeWhen
402subscription_expiredAn attachment upload on an expired plan.
402payment_action_required / payment_failed"Start my plan now" payment needs 3D Secure, or failed.
403sends_blockedAdding a team address on a workspace that can't send (trial or expired).
403team_address_cap_reachedThe plan's team-address cap (with cap).
429confirmation_rate_limitedToo many team-address confirmation emails (with retryAfterSeconds).
429rate_limitedA rate-limited route such as an alert test send (some with retryAfterSeconds).
400reserved_metadata_keyThread metadata uses a reserved key (keys lists them).
413preview_too_large / 503 humanizer_unavailableA humanizer preview that is too large, or the humanizer is down.
503suppression_check_unavailableThe suppression check couldn't run, so the request was refused rather than risk a send.

Suppressions, labels and lists

StatusCodeWhen
400invalid_email / invalid_domain / invalid_params / reserved_sourceA malformed suppression request.
403global_suppression_not_removable / global_scope_not_allowedPlatform-wide suppressions can't be changed by a tenant.
404suppression_not_found (message)No such suppression.
409label_exists / label_limit_reachedA label with that name exists, or the label cap is reached.
404label_not_found / threads_not_foundUnknown label or threads.
409list_name_conflict / list_archivedSee Lists.
404segment_not_found / contact_not_found (message)Unknown segment or contact.

Request IDs

Every response from the API and from the mailbox host carries an x-request-id header, errors included. When a request goes through the mailbox host to the API, both services use the same ID, so one ID follows the call all the way.

An error body repeats the ID as traceId:

{
  "statusCode": 404,
  "message": "Thread not found",
  "error": "Not Found",
  "traceId": "cor_0f3a9c1d5b7e4a2c8d6e1f0a3b5c7d9e"
}

The field is traceId, never requestId: requestId in a send response means the send request, not the HTTP request. Most CLI commands print the error body under detail, so the traceId is in their output.

You can set the ID yourself. An x-request-id (or x-correlation-id) of 8 to 128 characters made of letters, digits, ., _, : and - is kept as sent; anything else is replaced with one Molted makes, so a malformed value never travels on. That lets a caller tag a request and find it again in its own logs.

Quote the ID when you report a problem. Molted keeps a short record of each failed request for 30 days, found by this ID: the route, the status, the error code, which workspace and which kind of caller. Never a request body and never an email address. Failures on the portal, agent and claim routes and on the mailbox host are kept, except 401 answers, which are not recorded.

Handling blocked sends

  1. Check the reason field in the response to understand why the send was blocked.
  2. Refer to the tables above for the specific reason.
  3. Take the appropriate action -- wait for a cooldown, verify a domain, upgrade your plan, etc.
  4. Retry the request once the issue is resolved. Use a new dedupeKey if the original was consumed.