# Humanizer

Source: https://molted.email/docs/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](https://molted.email/docs/sending-email.md) 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](#preview-a-rewrite).

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](#send-exactly-what-you-previewed). 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](#send-exactly-what-you-previewed), once.

```bash title="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
```

```json title="MCP (preview_humanized)"
{ "template_id": "welcome", "payload": { "firstName": "Ada" }, "diff": true }
```

```bash title="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`.

```json title="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](#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](#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](#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](#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.

```bash title="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
```

```json title="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" }
```

```bash title="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:

| 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](#preview-a-rewrite) counts as a rewrite whenever it reaches the model, even when its rewrite doesn't pass the checks. [Sending it by its id](#send-exactly-what-you-previewed) 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.

| 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:

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](https://molted.email/docs/adoption.md) 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](#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](https://molted.email/docs/errors.md#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

```bash title="CLI"
molted humanizer get
```

```bash title="curl"
curl https://mailbox.molted.email/v1/agent/config/humanizer \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json title="Response"
{
  "enabled": true,
  "style": "professional",
  "provider": "anthropic"
}
```

### Update config

```bash title="CLI"
molted humanizer update --config '{"enabled": true, "style": "friendly"}'
```

```bash title="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"
  }'
```

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

```json title="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:

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

```bash title="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](#precedence) above for what these can and can't override.

```bash title="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
  }'
```

```bash title="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](https://molted.email/docs/cli-quickstart.md). The MCP `send_email`, `batch_send`, and `reply` tools take the same fields as `humanize`/`humanize_style`; see [MCP Server](https://molted.email/docs/mcp-server.md#sending).

| Field | Type | Required | Description |
| - | - | - | - |
| `humanize` | `boolean` | No | `false` always tightens (delivered exactly as written). `true` cannot turn the humanizer on by itself -- see [Precedence](#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:

```json title="Response: the account has the humanizer off"
{
  "requestId": "3f0c...",
  "status": "queued",
  "warnings": ["humanizer_ignored_owner_disabled"]
}
```

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

## Related

- [Sending Email](https://molted.email/docs/sending-email.md) -- send endpoints that support humanization
- [Templates](https://molted.email/docs/templates.md) -- manage email templates
- [Authentication](https://molted.email/docs/authentication.md) -- how to get your API key
