# Molted Email CLI — Agent Reference

You are integrating with Molted Email, an agent-native email control plane.
The `molted` CLI handles auth, headers, and tenant ID automatically.
All commands output JSON to stdout and errors to stderr.

Install: `npm install -g @molted/cli`

---

## Command Tree

```
molted
├── auth signup|login|init|logout|login-link
│   └── keys list|create|revoke
├── send --to --template --dedupe-key --payload
│   ├── batch --template --recipients|--stdin
│   ├── simulate --to --template
│   ├── simulate-batch --template --recipients
│   └── propose --to [--context]
├── mailboxes create|list|get|update|delete|clone|stats
├── templates create|list|get|add-version|publish|approve|reject|render
├── threads list|get|reply|update|archive|unarchive|trash|restore|delete|followup
├── domains add|list|get|verify|dns-check|remove
├── contacts list|get|sync
├── billing status|setup
├── budget
├── doctor [--mailbox <id>] [--json]
├── analytics fatigue|velocity|deliverability|segment-check
├── journeys create|list|get|update|add-step|update-step|delete-step|delete|trigger
│   └── runs list|cancel|cancel-all
├── segments create|list|get|update|archive|compute|members|contact
├── outcomes ingest|list|get|dashboard|journey-impact
├── suppressions add|list|remove|add-domain|list-domains|remove-domain
├── consent record|check
├── safety get|update
├── humanizer get|update
├── classify --subject --body
│   └── batch --emails|--stdin
├── next-action --contact
│   └── batch --contacts|--stdin
├── record-inbound --from --to [--subject --body ...]
├── trace <requestId>
└── timeline <requestId>
```

Global option: `--base-url <url>` — override API base URL.

---

## Getting Started

### 1. Sign up

```bash
molted auth signup --name "My Agent" --email agent@example.com --password s3cret
```

Credentials are stored in `~/.molted/credentials.json` automatically.

### 2. Activate billing

```bash
molted billing status
# → { "sendBlocked": true, "actions": ["setup_billing"] }

molted billing setup --plan solo
# → { "url": "https://checkout.stripe.com/..." }
```

Open the URL to complete payment. Sends are blocked until billing is active.

### 3. Send your first email

```bash
molted send \
  --to alice@example.com \
  --template _default \
  --dedupe-key "welcome-alice-1" \
  --payload '{"body": "Hello from my agent!"}'
```

---

## Command Reference

### auth

```bash
molted auth signup --name <n> --email <e> --password <p>   # create account + store creds
molted auth login --email <e> --password <p>               # log in + store creds
molted auth init --key <mm_live_...>                       # init from existing API key
molted auth logout                                         # clear stored creds
molted auth login-link [--next <path>]                     # link for your human: claim the workspace, or sign in (never a session)
molted auth keys list                                      # list API keys
molted auth keys create [--label <label>]                  # create a new key
molted auth keys revoke <id>                               # revoke a key
```

### send

```bash
# Single send (required: --to, --template, --dedupe-key, --payload)
molted send --to alice@example.com --template welcome \
  --dedupe-key "welcome-alice-1" --payload '{"name":"Alice"}' \
  [--reason <r>] [--mailbox <id>] [--idempotency-key <k>] [--attachments '<json>']

# Batch send
molted send batch --template welcome \
  --recipients '[{"email":"a@b.com","dedupeKey":"k1","payload":{"name":"A"}}]' \
  [--mailbox <id>] [--stdin]

# Dry-run policy check
molted send simulate --to alice@example.com --template welcome [--dedupe-key <k>] [--mailbox <id>]

# Batch dry-run
molted send simulate-batch --template welcome --recipients '["a@b.com","b@c.com"]'

# List your templates + a policy pre-check (a real send's rules minus the ones that need a template, named in policyPreCheck.notChecked)
molted send propose --to alice@example.com [--context '{"topic":"renewal"}']
```

### mailboxes

```bash
molted mailboxes create --address agent@yourdomain.com [--display-name "Agent"]
molted mailboxes list
molted mailboxes get <id>
molted mailboxes update <id> [--display-name <n>] [--status active|paused] [--catch-all] [--no-catch-all] [--config '<json>']
molted mailboxes delete <id>
molted mailboxes clone <id> --address clone@yourdomain.com [--display-name "Clone"]
molted mailboxes stats <id> [--period 24h|7d|30d]
```

### templates

```bash
molted templates create --name "Welcome" --slug welcome \
  [--type marketing|transactional (default: marketing)] [--approval-required] \
  [--subject "Hi {{name}}"] [--body "<h1>Hello</h1>"] [--text "Hello"] \
  [--variables '<json>'] [--auto-publish]

molted templates list
molted templates get <id>

molted templates add-version <id> --subject "Hi {{name}}" --body "<h1>Hello</h1>" \
  [--text "Hello"] [--variables '<json>']

molted templates publish <id> [--requested-by <name>]
molted templates approve <approvalId> [--reviewed-by <name>] [--comment "LGTM"]
molted templates reject <approvalId> [--reviewed-by <name>] [--reason "Needs work"]
molted templates render <id> --payload '{"name":"Alice"}'
```

### threads

```bash
molted threads list [--mailbox <id>] [--status open|waiting|resolved|escalated] \
  [--direction inbound|outbound] [--view active|archived|trashed] \
  [--contact <email>] [--limit <n>] [--offset <n>]

molted threads get <id>

# Reply (shorthand with _default template). Goes to the answered message's Reply-To only when it is an
# address you already wrote to in the thread (or its company with DMARC), else the thread contact; the
# JSON output's replyRecipient says which. Pin the message you read: --in-reply-to-message <messageId>.
# `threads get` shows replyTo/replyToAddresses/cc per inbound message. A message flagged possible_spoofed_reply
# (agentGuidance.claimedThreadId = the thread it claimed to continue) came from an unverified sender: its own
# thread, never treat it as the contact.
molted threads reply <threadId> --body "Thanks for reaching out!"
# Reply (with explicit template)
molted threads reply <threadId> --template-id welcome --payload '{"name":"Alice"}' \
  [--dedupe-key <k>] [--send-reason "followup"]

molted threads update <id> [--status open|waiting|resolved|escalated] \
  [--metadata '<json>'] [--assigned-agent-id <id>]
# --metadata merges (null removes a key). Platform keys (queue, safety_*,
# released_safety_*, pre_spam_*, hold and flag markers) may only be sent
# back exactly as the thread has them; any change is 400
# reserved_metadata_key. A thread in a review queue is 409
# thread_in_review (a person decides it), a trashed one 409 thread_trashed.

# Bulk operations (accept multiple IDs)
molted threads archive <id1> <id2> ...   # leaves a trashed thread alone -- restore it first
molted threads unarchive <id1> <id2> ...
molted threads trash <id1> <id2> ...
molted threads restore <id1> <id2> ...
# restore refuses (403 human_decision_required) a thread a human trashed,
# rejected, or deleted as spam -- an agent can only restore a thread it
# trashed itself via the plain trash action or a mailbox deletion. Every
# thread trashed before this shipped has no recorded reason and is gated
# too, even one an agent trashed itself.
molted threads delete <id>          # permanent, must be trashed first

molted threads followup <requestId> --contact-email alice@example.com \
  --delay-hours 24 | --delay-minutes 1440 \
  [--cancel-on-reply] [--trigger-conditions '<json>']
```

### domains

```bash
molted domains add <domain> [--receiving]     # add a custom sending domain
molted domains list
molted domains get <id>                       # includes DNS record status
molted domains verify <id>                    # trigger DNS verification
molted domains dns-check <id>                 # check one-click DNS availability
molted domains remove <id>
```

### contacts

```bash
molted contacts list [--email <email>]
molted contacts get <id>
molted contacts sync --contacts '[{"email":"a@b.com","name":"A"}]'  # or --stdin
```

### billing & budget

```bash
molted billing status       # plan, sendBlocked, hasPaymentMethod, sellablePlans
molted billing setup [--plan solo|team] [--skip-trial] [--success-url <url>] [--cancel-url <url>]
molted billing activate-free  # free plan, no card -- verified human accounts only, else 403
molted billing start-now    # end the 3-day trial, charge now, full plan limits (human decision: admin key, human gate)
molted budget                 # remaining send quota (monthly/daily/hourly)
```

`activate-free` succeeds only when the account owner is a verified human on a real email address. An account from `molted auth signup` is agent-owned, so this 403s with `free_plan_requires_human` (buy a paid plan with `billing setup` instead), `email_verification_required` (verify the owner's email first), or `disposable_email_not_allowed` (the owner's email domain is a known disposable provider -- use a real address, or buy a paid plan). Unlike other billing decisions, this is not gated by the workspace's human-gate mode -- it always applies.

### doctor

```bash
molted doctor                                          # diagnose why mail might not be sending
molted doctor --mailbox 3f1e0c2a-8b7d-4a3e-9c1f-2b6a7d8e9f01  # narrow the mailbox/domain/held-approvals checks to one mailbox (must be a UUID)
molted doctor --json                                   # { "blockers": [...], "checks": [...] } for agents
```

Runs every agent-reachable check concurrently, in triage order (billing, account pause, budget, mailboxes, domains, held approvals, recent policy blocks, inbound hold, DLQ, alerts) and prints the first blocker plus its fix command. Exit code `1` if any blocker was found; otherwise `2` if any check reports `"status": "error"` (the call failed) or `"status": "unknown"` (the call worked but couldn't be fully classified); otherwise `0` -- `0` means every check was fully verified, safe to gate a script on.

A check needing `operator` scope (held approvals, alerts), or a mailbox outside a scoped key's access, reports `"status": "skipped"`, not `"error"` -- skipped does NOT affect the exit code on its own, but the human-readable report says "N check(s) skipped" rather than claiming everything was fully verified. A `401` collapses into a single `"auth"` blocker no matter how many checks independently hit it, instead of repeating the same entry per check. A malformed `--mailbox` (not a UUID) exits `2`, not `1` -- `1` is reserved for an actual diagnosed blocker, and no check runs at all in that case.

`molted doctor` calls no write endpoint on its own initiative, but a few of its GETs are not perfectly inert: checking alerts may notify configured alert destinations (deduplicated), checking a mailbox's reputation for the first time makes the server compute one on that same read (which can auto-pause a mailbox on a bad score), and checking billing may finalize an already-lapsed grace period.

### analytics

```bash
molted analytics fatigue --contact alice@example.com
# → { "recommendation": "safe_to_send"|"reduce_frequency"|"stop_sending", ... }

molted analytics velocity
molted analytics deliverability [--period 24h|7d|30d]
molted analytics segment-check --contact alice@example.com --segment <id>
```

### journeys

```bash
molted journeys create --name "Onboarding" --trigger-event signup \
  [--trigger-conditions '<json>']
molted journeys list
molted journeys get <id>
molted journeys update <id> [--name <n>] [--status active|paused|archived]
molted journeys add-step <id> --step-order 1 --step-type send --config '{"templateId":"tpl_abc123"}'
molted journeys add-step <id> --step-order 2 --step-type delay --config '{"delayMinutes":1440}'
molted journeys add-step <id> --step-order 3 --step-type branch \
  --config '{"conditions":[{"field":"properties.plan","operator":"eq","value":"pro","nextStepOrder":4}],"defaultNextStepOrder":5}'
# field is a dot path resolved against the trigger payload ("plan" and "properties.plan"
# are equivalent); first matching condition wins, defaultNextStepOrder is the fallback.
molted journeys add-step <id> --step-order 4 --step-type end
molted journeys runs list <id>            # list runs for a journey
molted journeys runs cancel <id> <runId>  # cancel a single run
molted journeys runs cancel-all <id>      # cancel every active run
molted journeys trigger --event signup --contact a@b.com --properties '{"plan":"pro"}'
```

Journey statuses: `active` (processing), `paused` (in-flight runs are parked and resume
automatically), `archived` (stops processing and cancels every in-flight run -- not
permanent, set status back to `active` to reactivate).

### segments

```bash
molted segments create --name "Active Users" --filters '<json>'
molted segments list
molted segments get <id>
molted segments update <id> [--name <n>] [--filters '<json>']
molted segments archive <id>
molted segments compute <id>              # recompute membership
molted segments members <id> [--limit <n>] [--offset <n>]
molted segments contact <contactId>       # list segments a contact belongs to
```

### outcomes

```bash
molted outcomes ingest --contact a@b.com --event-type conversion --event-name signup \
  [--revenue 99.99] [--metadata '<json>'] [--occurred-at 2025-01-15]
molted outcomes list [--event-type <t>] [--from <iso>] [--to <iso>]
molted outcomes get <id>
molted outcomes dashboard
molted outcomes journey-impact
```

### suppressions

```bash
molted suppressions add --email a@b.com \
  --reason complaint|hard_bounce|manual_dnc|legal_request|role_account|domain_suppressed|no_engagement \
  --scope tenant|campaign [--campaign-id <id>] [--source <s>] [--expires-at <iso>]
molted suppressions list [--email <email>]
molted suppressions remove <id>

molted suppressions add-domain --domain spam.co [--reason manual_dnc] [--source <s>]
molted suppressions list-domains
molted suppressions remove-domain <domain>
```

### consent

```bash
molted consent record --email a@b.com \
  --type explicit_opt_in|legitimate_interest|contractual|legal_obligation \
  [--source signup-form] [--jurisdiction EU|US-CA] \
  [--granted-at <iso>] [--revoked-at <iso>]

molted consent check --email a@b.com
```

### safety & humanizer

```bash
molted safety get                          # current tenant safety settings
molted safety update --settings '<json>'   # update safety settings

molted humanizer get                       # current humanizer config
molted humanizer update --config '<json>'  # update humanizer config
```

Humanizer allowance: Free 0 (enabling returns 403 `humanizer_not_in_plan`), Solo 50/mo, Team 300/mo, then credits at $0.02 up to 2x. Past the cap emails send un-rewritten with a `warnings` entry. Usage: `molted billing status` (`humanizer` block).

### classify, next-action, record-inbound

```bash
molted classify --subject "Re: Invoice" --body "Please find attached..."
molted classify batch --emails '[{"subject":"Hi","body":"Hello"}]'  # or --stdin
# → platformLabel (opt_out or null here), safetyVerdict, route; intent is deprecated

molted next-action --contact alice@example.com
molted next-action batch --contacts '["a@b.com","c@d.com"]'  # or --stdin

molted record-inbound --from alice@example.com --to agent@yourdomain.com \
  [--subject "Re: Hello"] [--body "Thanks!"] [--body-html "<p>Thanks!</p>"] \
  [--in-reply-to <messageId>] [--references <header>] \
  [--provider-message-id <id>] [--mailbox <id>] [--thread <id>]
```

### trace & timeline

```bash
molted trace <requestId>      # full decision trace for a send request
molted timeline <requestId>   # delivery event timeline (sent → delivered → opened)
```

---

## Common Patterns

### Pattern 1: Safe Send (Simulate → Send)

```bash
molted send simulate --to alice@example.com --template welcome
# → check result.status — if "allowed", proceed:
molted send --to alice@example.com --template welcome \
  --dedupe-key "welcome-alice-1" --payload '{"name":"Alice"}'
```

### Pattern 2: Informed Send (Propose → Simulate → Send)

```bash
molted send propose --to alice@example.com --context '{"topic":"renewal"}'
# → returns your active templates, policyPreCheck (allow, reason?, wouldRequireApproval, notChecked) and contact context.
#   notChecked lists what only a template can decide (cooldown, duplicate, consent, ...): allow is not a promise.
molted send simulate --to alice@example.com --template <suggested>
molted send --to alice@example.com --template <suggested> \
  --dedupe-key "renewal-alice-1" --payload '<suggested payload>'
```

### Pattern 3: Send with Follow-up

```bash
molted send --to alice@example.com --template outreach \
  --dedupe-key "outreach-alice-1" --payload '{"name":"Alice"}'
# → note the requestId from the response
molted threads followup <requestId> --contact-email alice@example.com \
  --delay-hours 72 --cancel-on-reply
```

### Pattern 4: Inbound Triage

```bash
molted threads get <threadId>
# → each inbound message has platformLabel: opt_out (the sender asked to stop; Molted
#   suppresses them only when confident: if routingAction is require_approval, check
#   the suppression and add one yourself), auto_reply, delivery_notice (no answer
#   needed) or null (yours to read and decide)
molted next-action --contact alice@example.com
# → { "action": "reply", "reason": "..." }
molted threads reply <threadId> --body "Your invoice is attached."
```

### Pattern 5: Batch Outreach

```bash
molted budget
# → check remaining quota
molted send simulate-batch --template promo --recipients '["a@b.com","c@d.com"]'
# → review which recipients are allowed
molted send batch --template promo \
  --recipients '[{"email":"a@b.com","dedupeKey":"promo-a","payload":{"name":"A"}}]'
```

### Pattern 6: Fatigue-Aware Send

```bash
molted analytics fatigue --contact alice@example.com
# → { "recommendation": "safe_to_send" } — proceed
# → { "recommendation": "reduce_frequency" } — delay or skip
# → { "recommendation": "stop_sending" } — do not send
```

### Pattern 7: Journey-Driven Onboarding

```bash
molted templates create --name "Day 1" --slug onboard-day1 \
  --subject "Welcome {{name}}" --body "<h1>Welcome!</h1>" --auto-publish
molted journeys create --name "Onboarding" --trigger-event signup
molted journeys add-step <journeyId> --step-order 1 --step-type send \
  --config '{"templateId":"tpl_abc123"}'  # use the id from templates create, not the slug
molted journeys add-step <journeyId> --step-order 2 --step-type delay \
  --config '{"delayMinutes":1440}'
molted journeys update <journeyId> --status active
molted journeys trigger --event signup --contact a@b.com --properties '{"name":"A"}'
```

### Pattern 8: Full Setup (New Agent)

```bash
# Option A: create a new account
molted auth signup --name "My Agent" --email agent@example.com --password s3cret
# Option B: use an existing API key from the dashboard
molted auth init --key mm_live_your_key_here
molted billing setup --plan solo
# → open checkout URL, complete payment
molted templates create --name "Welcome" --slug welcome \
  --subject "Hi {{name}}" --body "<h1>Hello</h1>" --auto-publish
molted send simulate --to alice@example.com --template welcome
molted send --to alice@example.com --template welcome \
  --dedupe-key "welcome-alice-1" --payload '{"name":"Alice"}'
# Optional: custom domain + mailbox
molted domains add yourdomain.com --receiving
molted domains verify <domainId>
molted mailboxes create --address agent@yourdomain.com
```

---

## Idempotency

The `--dedupe-key` flag guarantees exactly-once semantics:

- First call with a given key: processed normally
- Subsequent calls with the same key: blocked with reason `duplicate`

Use deterministic keys like `{workflow}-{contactId}-{step}` to make retries safe.

---

## Error Handling

- **Exit code 0**: success — JSON result on stdout
- **Exit code 1**: error — JSON error on stderr

A policy-blocked or held send returns exit code 0 with a `status` other than `queued`/`scheduled` in the output. The request was valid; policy just didn't allow it (yet). Always inspect the `status` field.

| Status | Meaning |
|--------|---------|
| `queued` | Accepted, headed for delivery |
| `scheduled` | Accepted but deferred (e.g. domain warmup); check `scheduledAt` |
| `pending_approval` | Held for a human decision (mailbox autonomy level, thread risk, content check) |
| `blocked` | Policy denied the send (check `reason`) |

A retry with the same `dedupeKey` comes back `blocked` with `reason: "duplicate"` and the original `requestId` -- there is no separate `duplicate` status.

Common block reasons: `suppressed`, `no_consent`, `trial_not_activated`, `duplicate`, `template_not_found`, `hourly_limit_exceeded`, `daily_limit_exceeded`, `monthly_limit_exceeded`, `overage_cap_exceeded`, `free_sends_paused` (Molted has paused every Free-plan account's sending for now; retry later). Full list: [Errors & Policy Blocks](https://molted.email/docs/errors).

---

## Rate Limits

Per-tenant quotas (configurable by plan):

| Window | Default |
|--------|---------|
| Monthly | Plan-based (Trial: 100, Free: 3,000, Solo: 10,000, Team: 50,000) |
| Daily | 10,000 |
| Hourly | 1,000 |

Check current usage with `molted budget` before large batches.
