MOLTED EMAIL

Humanizer

LLM-powered email rewriting that gives your automated emails a natural, human tone.

The humanizer rewrites outgoing emails using an LLM to make them sound more natural. Instead of sending template output verbatim, the humanizer adjusts phrasing, tone, and structure while preserving your message's content and intent.

How it works

  1. You send an email through the normal send endpoint with humanization enabled.
  2. The humanizer rewrites the email body in your chosen style.
  3. The rewritten version is sent to the recipient.

To see the rewrite before you send, preview it.

If rewriting fails for any reason, the original email is sent unchanged -- your delivery is never blocked by the humanizer.

Approved and human-authored mail

Mail a human approves through the approval queue, or writes directly (a portal reply, or a send made after taking over a thread), is delivered exactly as approved or written: it is never rewritten by the humanizer, whatever your tenant-level or per-send settings say. This holds for a soft-bounce retry of one of these sends too.

To have a held send rewritten, rewrite it before it is held: preview it and send with the preview id. The draft waiting for approval is then the rewrite, so the approver sees and approves exactly the text that goes out.

Guardrails

Every rewrite is checked before it is used. Nothing here needs any configuration.

  • Links, email addresses, and numbers can't be added, dropped, or changed. Every URL in the original (including your signed unsubscribe link), every email address, and every number-like token (a price, a percentage, a date, a time, phone-number digits) must appear in the rewrite exactly as written, and the rewrite may not introduce a new one either. A single character changed in a link or an address fails the check, the same as dropping it.
  • Your compliance footer is protected. When the email has an HTML part, the physical-address footer required for marketing mail is kept out of the rewrite entirely and reattached afterward, exactly as it was added. A text-only send (no HTML part at all) has no equivalent structural protection for its footer; it relies on the link/address/number check above.
  • No cut-short or refused replies. A reply the model cut off partway through, or declined to complete, is treated the same as a failed rewrite.
  • No empty replies. A blank or whitespace-only reply is treated the same as a failed rewrite.
  • The email body is sent as data, not instructions. The body (including any quoted prior message) is delimited so the model treats it as content to restyle. This stops it from being read as a new command; it does not by itself guarantee wording is left alone beyond what the link/address/number check above enforces.

If any check fails, the original (un-rewritten) email is sent instead. A failed rewrite (including a retry that fails again) is never counted against your plan's allowance and never billed as a credit, only rewrites that actually go out count.

See why a specific send was rewritten or skipped with molted trace <requestId> (or GET /v1/ops/trace/:requestId), using the requestId from the send response. A successful rewrite shows a humanizer.applied event with the style, provider, and model used. A skipped one shows humanizer.skipped with a reason:

reasonWhy
not_in_plan, allowance_exhausted, credit_cap_reachedA plan limit (see Plans and limits below).
held_for_approval, human_authoredThe approval/authorship exemption above: delivered exactly as approved or written.
truncatedThe reply was cut off partway through.
refusedThe model declined to rewrite the content rather than running out of room.
empty_responseThe reply came back blank or whitespace-only.
check_failedA link, address, or number was added, dropped, or changed.
nothing_to_rewriteThere was nothing left to rewrite (not a failed check).
timeoutThe rewrite call took too long.
provider_errorAny other rewrite failure (a configuration or provider-side error).

Preview a rewrite

See what the humanizer would do to an email before you send it. A preview renders the email exactly as a send would (your template, the compliance footer, the same unsubscribe placement), works out which setting applies to it, and runs the same rewrite and the same checks. Nothing is sent. A rewrite that passes the checks is kept, encrypted, for 24 hours under a previewId, so you can send exactly it, once.

CLI
molted humanizer preview --template welcome --payload '{"firstName": "Ada"}'

# raw content (built exactly like `molted send --body`), with a line diff of the rewrite instead of both texts
molted humanizer preview --subject "Quick question" --body "Hi Ada, following up on our call." --diff

# rewrite in another style (a send by this preview's id delivers it), for the mailbox you'll send from
molted humanizer preview --template welcome --payload @vars.json --style casual --mailbox MAILBOX_ID
MCP (preview_humanized)
{ "template_id": "welcome", "payload": { "firstName": "Ada" }, "diff": true }
curl
curl -X POST https://mailbox.molted.email/v1/agent/humanizer/preview \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"templateId": "welcome", "payload": {"firstName": "Ada"}, "diff": true}'

Pass either templateId (with payload) or raw html and/or text (with an optional subject), not both. The CLI's --body/--html and the MCP tool's body/html build the html and text parts exactly the way molted send and send_email do, so a send with the same flags matches the preview; --text/text sets the text part as given. Optional: mailboxId (the mailbox whose style applies; defaults to the one a send would use), style (rewrite in casual, professional or friendly instead of the configured style: a send by this preview's id delivers this rewrite in that style, while a send without the id uses its own humanizeStyle or the configured style), and diff: true.

Response
{
  "effective": { "enabled": true, "style": "friendly", "provider": "anthropic", "source": "tenant" },
  "original": { "subject": "Welcome, Ada", "html": "<p>Hi Ada, thanks for signing up...</p>", "text": "Hi Ada, thanks for signing up..." },
  "humanized": { "html": "<p>Hi Ada, great to have you...</p>", "text": "Hi Ada, great to have you..." },
  "applied": true,
  "checks": { "ok": true, "failures": [] },
  "model": "claude-sonnet-5-5",
  "usage": { "inputTokens": 412, "outputTokens": 356, "latencyMs": 2140 },
  "diff": "- Hi Ada, thanks for signing up...\n+ Hi Ada, great to have you...",
  "previewId": "6f1c1b1e-3a5f-4c6e-9a61-1c2d3e4f5a6b",
  "expiresAt": "2026-09-30T10:00:00.000Z",
  "warnings": []
}
  • effective is the setting a send would get, resolved the same way (see Precedence); source says which level turned it on or off (agent, mailbox, tenant or default). With the humanizer off, the preview returns the original with humanized: null and makes no model call.
  • applied: false means a send would go out as written; skipReason is one of the reasons in the table above.
  • checks is the link/address/number check from Guardrails: ok: true when the rewrite kept everything, ok: false with failures (kinds and counts, e.g. {"kind": "url", "count": 1}) when it didn't, and null when the check never ran (the humanizer is off, or the reply was rejected before the check, e.g. truncated).
  • The subject is never rewritten, in a preview or a send.
  • The unsubscribe link is a placeholder (https://unsubscribe.molted.invalid/preview-only, flagged by the unsubscribe_link_placeholder warning): a real send mints its own per-recipient link.
  • previewId and expiresAt come with an applied rewrite only (not with applied: false, and not with the humanizer off). In the rare case the rewrite can't be stored, the response has no previewId and carries the preview_not_stored warning.
  • Other warnings: preview_style_override when you passed style, and preview_used_paid_credit when the preview went past your included rewrites and used a paid credit.

A preview costs a rewrite. Each preview that reaches the model uses one humanizer credit from the same allowance as your sends (see Plans and limits), whether or not the rewrite passes the checks: a rejected, cut-short or timed-out rewrite still costs its credit. Only a preview that never calls the model is free (the humanizer is off, or there is nothing to rewrite). With none left the preview answers 403 humanizer_not_in_plan, or 402 humanizer_allowance_exhausted / humanizer_credit_cap_reached. Other errors: 400 invalid_humanize_style or invalid_preview_input, 404 template_not_found, 400 missing_physical_address or template_render_failed (a send would be blocked too), 413 preview_too_large (more than 64 KB of rendered html and text), 403 mailbox_scope_denied (a mailbox your key can't send from), 404 mailbox_not_found (a deleted mailbox, or one in another workspace), and 503 humanizer_unavailable when the Molted server has no key configured for the provider.

A send made without the preview id is not tied to the preview: the worker runs the rewrite again, so the delivered wording can differ from the preview (the same checks apply). A preview also waits less for the model (20 seconds, so it answers within the 30 seconds the CLI and MCP connection allows) than a send does (25 seconds), so a very long email can show timeout in the preview and still be rewritten when sent without the id. And a send that is held for approval and then approved goes out exactly as approved, never rewritten (see Approved and human-authored mail): send it with the preview id to have the approver see, and send, the rewrite.

Send exactly what you previewed

Pass the preview's previewId as humanizePreviewId on the send, and the send delivers that stored rewrite instead of asking for a new one. Each preview id sends once.

CLI
molted humanizer preview --subject "Quick question" --body "Hi Ada, following up on our call."
# -> Preview id: 6f1c1b1e-... (valid until ...)
molted send --to ada@example.com --subject "Quick question" --body "Hi Ada, following up on our call." \
  --humanize-preview 6f1c1b1e-3a5f-4c6e-9a61-1c2d3e4f5a6b

molted threads reply THREAD_ID --body "Thanks, see you Thursday." --humanize-preview PREVIEW_ID
MCP (send_email, reply, or a batch_send item)
{ "to": "ada@example.com", "subject": "Quick question", "body": "Hi Ada, following up on our call.", "humanize_preview_id": "6f1c1b1e-3a5f-4c6e-9a61-1c2d3e4f5a6b" }
curl
curl -X POST https://mailbox.molted.email/v1/agent/request-send \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"recipientEmail": "ada@example.com", "templateId": "welcome", "dedupeKey": "welcome-ada",
       "payload": {"firstName": "Ada"}, "humanizePreviewId": "6f1c1b1e-3a5f-4c6e-9a61-1c2d3e4f5a6b"}'

humanizePreviewId works on POST /v1/agent/request-send, on each item of POST /v1/agent/batch/request-send (per recipient, and each recipient needs its own preview; a batch has no batch-level preview id), and on thread replies (POST /v1/agent/outbound/reply, and /v1/agent/outbound/send).

  • Byte for byte. The html and text that go out are the preview's humanized html and text. The one difference is the unsubscribe link: the preview's placeholder becomes the send's own signed, per-recipient link.
  • The content must match. The send's html and text, as rendered, must be the ones the preview was built from. Preview with the same template and payload, or the same body and html, that you send with. Otherwise the send is refused with 409 humanize_preview_mismatch and nothing is sent: a changed template, a different variable, other body text or a changed physical-address footer all count. The subject is not compared (it is never rewritten, and a reply's Re: ... subject is fine).
  • Expiry and scope. A preview can be sent for 24 hours, from the workspace that made it. An unknown or expired id, one from another workspace, or one previewed with a mailboxId for a different mailbox than the send's, is 404 humanize_preview_not_found. A preview made without a mailboxId (with a key that can send from every mailbox) fits any mailbox, also for a send by a mailbox-scoped key: that key's own scope still decides which mailboxes it may send from.
  • Once. A preview id is used up by the first send that applies it, and any other send with it is 409 humanize_preview_used: preview again for the next one. Of two sends racing with one id, exactly one gets the rewrite. This keeps the metering the same as a normal send, one humanizer credit per humanized email, so in a batch every recipient needs its own preview. A preview that isn't applied (the humanizer was turned off since, or the send is refused with one of the errors here) stays unused.
  • No second credit. The preview already paid its humanizer credit. The send takes none, and it is never rewritten again: not by the worker, not after an approval, not on a soft-bounce retry.
  • Approvals. A send that is held for approval stores the rewrite as its draft, so the approver sees exactly the text that will go out, and that is the text delivered on approval.
  • The owner's setting still wins. If the humanizer was turned off (for the workspace or the agent) after the preview, the send goes out as written, with the warning humanize_preview_ignored_humanizer_off. The preview's style is the one delivered: a humanizeStyle on the send that differs from it is 400 humanize_preview_conflict (leave it out, or pass the same style).
  • Not with humanize: false. Together they are 400 humanize_preview_conflict; an id that is not a preview id is 400 invalid_humanize_preview_id. In a batch, each of these errors (and humanize_preview_used) fails only its own recipient (status: "error" with the code as reason).
  • Retries. A retry with the same dedupeKey gets the original send's result without touching the preview, even after it was used or expired.
  • Trace. The send's audit trail has a humanizer.applied event with source: "preview" and the previewId.

Plans and limits

Each rewrite counts against your plan's humanizer allowance:

PlanRewrites includedPast the allowance
FreeNoneNot available
Solo50 a monthCredits at $0.02 each, up to 50 more a month
Team300 a monthCredits at $0.02 each, up to 300 more a month
Paid trial (3 days)5 in totalNot available
EnterpriseUnlimited
  • Credits are billed on your next renewal invoice. Usage resets on the 1st of each month (UTC). A paid trial's rewrites don't count against your first paid month.
  • A workspace whose plan is set by Molted support gets its plan's allowance but no credits: past it, emails go out without rewriting.
  • A rewrite is counted once per email, even when a delivery is retried. A rewrite that fails isn't counted.
  • A preview counts as a rewrite whenever it reaches the model, even when its rewrite doesn't pass the checks. Sending it by its id costs nothing more.
  • When the allowance and credits are used up (or on Free), emails still go out, just not rewritten. The send response carries a warnings entry, and the skip is recorded in your audit log as humanizer.skipped.
  • On Free, turning the humanizer on returns 403 humanizer_not_in_plan. Turning it off always works.
  • Check usage with molted billing status or GET /v1/billing/status: the humanizer block has allowance, used, remaining, creditsUsed and a warning when rewrites are being skipped.
WarningMeaning
humanizer_not_in_planYour plan doesn't include the humanizer.
humanizer_allowance_exhaustedYour included rewrites are used and your plan has no credits (a trial, or a plan set by Molted support). Rewrites resume when the allowance resets, or once your plan starts during a trial.
humanizer_credit_cap_reachedThis month's allowance and credits are used. Rewrites resume next month.

Styles

The humanizer supports three styles. Pass one of these values as the style field in the config, or as humanizeStyle on a send (--humanize-style in the CLI, humanize_style in MCP):

StyleDescription
casualRelaxed, conversational tone.
professionalPolished, business-appropriate tone. Default if not specified.
friendlyWarm and approachable while staying clear.

An unknown style is rejected with a 400 error, never silently ignored.

Precedence

Whether the humanizer runs on a given send is decided in this order:

  1. Send. humanize: false on the send request always wins: the mail goes out as written.
  2. An explicit off at the agent or the mailbox wins next. The agent is the config of the agent that sent it (PUT /v1/agent/agents/:agentId/config), resolved from the agent your API key is bound to -- a key gets one through agent adoption or by registering with bindKey (the CLI and MCP default it on); a key minted directly (molted auth signup, the portal) has none until then, so this level never applies to its sends. The mailbox is the sending mailbox's own switch (config.humanizer.enabled, see Configuration).
  3. An explicit on at the agent or the mailbox turns it on.
  4. Tenant. Your workspace-wide setting (GET/PUT /v1/agent/config/humanizer), the default for every mailbox that has no setting of its own.
  5. Default. Off.

So a mailbox can be humanized while the workspace default is off, a mailbox turned off is never humanized whatever an agent says, and an agent turned off is never humanized whatever a mailbox says. A mailbox with no setting of its own follows the workspace, which is how every mailbox starts.

The style, once the humanizer is running, resolves on its own: send > agent > mailbox > tenant > professional (the default), whichever level turned it on. Molted picks the model (claude-sonnet-5-5) -- the provider is always anthropic and is not a per-tenant, per-agent, or per-mailbox setting.

The humanizer rewrites what is sent in your name, so a send can tighten this (turn rewriting off) but never loosen it (turn rewriting on, or change the style of an owner setting that is already on) -- the same rule that governs every agent-key config change. See Loosening a control.

  • humanize: false is always honored: the mail goes out exactly as written, whatever the agent, mailbox or tenant setting says.
  • humanize: true cannot turn the humanizer on by itself. If the agent, mailbox and tenant settings leave it off, the mail goes out as written and the send response carries a humanizer_ignored_owner_disabled warning. If they already have it on, the send goes ahead rewritten.
  • humanizeStyle is honored whenever the humanizer ends up running on a send, whether that's because of humanize: true or an agent, mailbox or tenant setting that was already on.
  • humanizeProvider is accepted and validated (a 400 on an unknown value) for backward compatibility, but no longer changes anything: Molted always rewrites with Claude Sonnet 5.5. It isn't exposed on the CLI or MCP tools, only the HTTP API.

Configuration

View and update your humanizer settings with the CLI or API (MCP can read them with get_humanizer_config, but has no tool to change them). Writing the workspace setting needs a key that reaches every mailbox and has the manage scope: a mailbox-scoped key is refused. Turning the humanizer on, or changing its style while it is on, is a human decision from an agent key at the tenant, agent and mailbox levels (the write returns 403 human_decision_required with a decisionUrl); at the mailbox level any change to its style (clearing it included) is gated the same way. Turning it off always works. A plan without the humanizer can't turn it on at any level (403 humanizer_not_in_plan). A person makes these changes in the portal: Settings > Safety for the workspace default, or the Safety card on a mailbox's page for that mailbox's own switch and style.

Get current config

CLI
molted humanizer get
curl
curl https://mailbox.molted.email/v1/agent/config/humanizer \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
{
  "enabled": true,
  "style": "professional",
  "provider": "anthropic"
}

Update config

CLI
molted humanizer update --config '{"enabled": true, "style": "friendly"}'
curl
curl -X PUT https://mailbox.molted.email/v1/agent/config/humanizer \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": true,
    "style": "friendly"
  }'
FieldTypeRequiredDescription
enabledbooleanNoWhether outbound emails are humanized before delivery.
stylestringNoHumanization style: casual, professional, or friendly.
providerstringNoAccepted for backward compatibility but no longer a choice: Molted always rewrites with Claude Sonnet 5.5. A value here is ignored and reported back in warnings as humanizer_provider_ignored.

Updates are partial -- any field you omit keeps its current value. Unknown fields are rejected with a 400 error.

Response
{ "tenantId": "tenant_abc123", "enabled": true, "style": "friendly", "provider": "anthropic", "updated": true }

The agent linked to your API key has its own humanizer config at PUT /v1/agent/agents/:agentId/config (read it with GET on the same path). Its keys are humanizer_enabled and humanizer_style (humanizer_provider is accepted and ignored, like provider above); it needs the same workspace-wide manage key and follows the same tighten-only rule as the tenant setting above (the human decision's kind is agent-config).

The sending mailbox has its own switch and style at PATCH /v1/agent/mailboxes/:id, editable in the portal from the Safety card on the mailbox's page:

FieldTypeDescription
config.humanizer.enabledboolean or nulltrue humanizes this mailbox's mail even while the workspace is off; false never humanizes it; null (or unset) follows the workspace. Any other value is a 400 invalid_humanizer_config.
config.humanizer.stylestring or nullThis mailbox's style; null uses the workspace style.

config replaces the whole object on write, but the mailbox service carries config.humanizer forward when a PATCH doesn't mention it (or only gives part of it), so you only need to include what you're actually changing; a null field clears just that field, and humanizer: null clears both back to the workspace. From an agent key, a change that turns the mailbox's humanizer on (its effective state going from off to on, given the workspace setting: setting true while the workspace is off, or clearing a false while it is on) is a human decision, and so is any change to its style; turning it off ({"humanizer": {"enabled": false}}) always works. Every response that returns a mailbox (get, list, PATCH, create, clone, and the mailbox in a restore) carries the result as humanizer: { "setting": true | false | null, "enabled": boolean, "source": "mailbox" | "tenant" | "default" }, where enabled is whether a send from that mailbox is humanized when neither the agent nor the send sets it.

curl -- turn the humanizer off for one mailbox
curl -X PATCH https://mailbox.molted.email/v1/agent/mailboxes/MAILBOX_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "config": { "humanizer": { "enabled": false } } }'

Enabling per send

Add the humanize and humanizeStyle fields to your send request. See Precedence above for what these can and can't override.

curl -- force this send out exactly as written
curl -X POST https://mailbox.molted.email/v1/send/request \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipientEmail": "user@example.com",
    "templateId": "follow-up",
    "dedupeKey": "followup-user@example.com",
    "payload": {
      "subject": "Following up",
      "html": "<p>Just checking in on our conversation last week.</p>",
      "text": "Just checking in on our conversation last week."
    },
    "humanize": false
  }'
CLI
molted send --to user@example.com --template follow-up \
  --dedupe-key followup-user@example.com --payload '{"subject":"Following up"}' \
  --no-humanize

# ask for the humanizer, honored only if the account already has it on:
molted send --to user@example.com --template follow-up \
  --dedupe-key followup-user@example.com --payload '{"subject":"Following up"}' \
  --humanize --humanize-style casual

molted send batch and molted threads reply take the same --humanize/--no-humanize/--humanize-style flags (run molted send --help for the full list); see CLI Quickstart. The MCP send_email, batch_send, and reply tools take the same fields as humanize/humanize_style; see MCP Server.

FieldTypeRequiredDescription
humanizebooleanNofalse always tightens (delivered exactly as written). true cannot turn the humanizer on by itself -- see Precedence.
humanizeStylestringNocasual, professional, or friendly. Honored whenever the humanizer runs on this send.

If a per-send humanize: true didn't turn rewriting on (the agent, mailbox and tenant are all off), or the rewrite will be skipped because of your plan's limits, the send is still accepted and the response says so:

Response: the account has the humanizer off
{
  "requestId": "3f0c...",
  "status": "queued",
  "warnings": ["humanizer_ignored_owner_disabled"]
}
Response: the account has it on, but this month's cap is reached
{
  "requestId": "3f0c...",
  "status": "queued",
  "warnings": ["humanizer_credit_cap_reached"]
}