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
- You send an email through the normal send endpoint with humanization enabled.
- The humanizer rewrites the email body in your chosen style.
- 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:
reason | Why |
|---|---|
not_in_plan, allowance_exhausted, credit_cap_reached | A plan limit (see Plans and limits below). |
held_for_approval, human_authored | The approval/authorship exemption above: delivered exactly as approved or written. |
truncated | The reply was cut off partway through. |
refused | The model declined to rewrite the content rather than running out of room. |
empty_response | The reply came back blank or whitespace-only. |
check_failed | A link, address, or number was added, dropped, or changed. |
nothing_to_rewrite | There was nothing left to rewrite (not a failed check). |
timeout | The rewrite call took too long. |
provider_error | Any 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.
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{ "template_id": "welcome", "payload": { "firstName": "Ada" }, "diff": true }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.
{
"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": []
}effectiveis the setting a send would get, resolved the same way (see Precedence);sourcesays which level turned it on or off (agent,mailbox,tenantordefault). With the humanizer off, the preview returns the original withhumanized: nulland makes no model call.applied: falsemeans a send would go out as written;skipReasonis one of the reasons in the table above.checksis the link/address/number check from Guardrails:ok: truewhen the rewrite kept everything,ok: falsewithfailures(kinds and counts, e.g.{"kind": "url", "count": 1}) when it didn't, andnullwhen 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 theunsubscribe_link_placeholderwarning): a real send mints its own per-recipient link. previewIdandexpiresAtcome with an applied rewrite only (not withapplied: false, and not with the humanizer off). In the rare case the rewrite can't be stored, the response has nopreviewIdand carries thepreview_not_storedwarning.- Other
warnings:preview_style_overridewhen you passedstyle, andpreview_used_paid_creditwhen 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.
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{ "to": "ada@example.com", "subject": "Quick question", "body": "Hi Ada, following up on our call.", "humanize_preview_id": "6f1c1b1e-3a5f-4c6e-9a61-1c2d3e4f5a6b" }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
humanizedhtml 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_mismatchand 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'sRe: ...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
mailboxIdfor a different mailbox than the send's, is404 humanize_preview_not_found. A preview made without amailboxId(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: ahumanizeStyleon the send that differs from it is400 humanize_preview_conflict(leave it out, or pass the same style). - Not with
humanize: false. Together they are400 humanize_preview_conflict; an id that is not a preview id is400 invalid_humanize_preview_id. In a batch, each of these errors (andhumanize_preview_used) fails only its own recipient (status: "error"with the code asreason). - Retries. A retry with the same
dedupeKeygets 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.appliedevent withsource: "preview"and thepreviewId.
Plans and limits
Each rewrite counts against your plan's humanizer allowance:
| Plan | Rewrites included | Past the allowance |
|---|---|---|
| Free | None | Not available |
| Solo | 50 a month | Credits at $0.02 each, up to 50 more a month |
| Team | 300 a month | Credits at $0.02 each, up to 300 more a month |
| Paid trial (3 days) | 5 in total | Not available |
| Enterprise | Unlimited |
- 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
warningsentry, and the skip is recorded in your audit log ashumanizer.skipped. - On Free, turning the humanizer on returns
403 humanizer_not_in_plan. Turning it off always works. - Check usage with
molted billing statusorGET /v1/billing/status: thehumanizerblock hasallowance,used,remaining,creditsUsedand awarningwhen rewrites are being skipped.
| Warning | Meaning |
|---|---|
humanizer_not_in_plan | Your plan doesn't include the humanizer. |
humanizer_allowance_exhausted | Your 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_reached | This 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):
| Style | Description |
|---|---|
casual | Relaxed, conversational tone. |
professional | Polished, business-appropriate tone. Default if not specified. |
friendly | Warm 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:
- Send.
humanize: falseon the send request always wins: the mail goes out as written. - 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 withbindKey(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). - An explicit on at the agent or the mailbox turns it on.
- Tenant. Your workspace-wide setting (
GET/PUT /v1/agent/config/humanizer), the default for every mailbox that has no setting of its own. - 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: falseis always honored: the mail goes out exactly as written, whatever the agent, mailbox or tenant setting says.humanize: truecannot 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 ahumanizer_ignored_owner_disabledwarning. If they already have it on, the send goes ahead rewritten.humanizeStyleis honored whenever the humanizer ends up running on a send, whether that's because ofhumanize: trueor an agent, mailbox or tenant setting that was already on.humanizeProvideris 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
molted humanizer getcurl https://mailbox.molted.email/v1/agent/config/humanizer \
-H "Authorization: Bearer YOUR_API_KEY"{
"enabled": true,
"style": "professional",
"provider": "anthropic"
}Update config
molted humanizer update --config '{"enabled": true, "style": "friendly"}'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"
}'| Field | Type | Required | Description |
|---|---|---|---|
enabled | boolean | No | Whether outbound emails are humanized before delivery. |
style | string | No | Humanization style: casual, professional, or friendly. |
provider | string | No | Accepted 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.
{ "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:
| Field | Type | Description |
|---|---|---|
config.humanizer.enabled | boolean or null | true 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.style | string or null | This 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 -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 -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
}'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 casualmolted 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.
| Field | Type | Required | Description |
|---|---|---|---|
humanize | boolean | No | false always tightens (delivered exactly as written). true cannot turn the humanizer on by itself -- see Precedence. |
humanizeStyle | string | No | casual, 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:
{
"requestId": "3f0c...",
"status": "queued",
"warnings": ["humanizer_ignored_owner_disabled"]
}{
"requestId": "3f0c...",
"status": "queued",
"warnings": ["humanizer_credit_cap_reached"]
}Related
- Sending Email -- send endpoints that support humanization
- Templates -- manage email templates
- Authentication -- how to get your API key