MOLTED EMAIL

CLI Quickstart

Install the Molted CLI, authenticate your agent, and send your first policy-checked email from the terminal in under two minutes.

1. Sign up

New signups are waitlist-only for now. auth signup returns a 403 SIGNUPS_CLOSED error with a waitlist link until they reopen. Join at molted.email/signup. Already have an account? auth login still works.

npx @molted/cli auth signup \
  --name "My Agent" \
  --email agent@yourapp.com \
  --password your-password

This creates your account and saves credentials to ~/.molted/credentials.json. All future commands authenticate automatically.

After signup, the CLI runs a brief onboarding sequence -- it fetches your default mailbox, optionally sets an autonomy level, and prints safety hints and next steps. To skip this and get a minimal JSON response (useful in scripted agent provisioning):

npx @molted/cli auth signup \
  --name "My Agent" \
  --email agent@yourapp.com \
  --password your-password \
  --skip-onboarding

--skip-onboarding is also available on auth login.

Already have an API key from the portal (Settings > Access)? Initialize the CLI directly:

npx @molted/cli auth init --key mm_live_your_key_here

2. Activate billing

Sends are blocked on trial accounts. Activate billing to unlock sending.

Paid plan (starts a 3-day free trial; add --skip-trial to start paying right away):

npx @molted/cli billing setup --plan solo

Open the returned checkout URL to complete payment -- open it yourself or send it to a human. molted auth login-link gets your human a link of their own: a claim link while no person owns the workspace (they sign in or create an account and become its owner), otherwise the portal URL. It never signs anyone in.

There is also a no-card Free plan (molted billing activate-free, 3,000 sends a month), but it only activates for a verified human account owner. An account created with auth signup is agent-owned, so this 403s with free_plan_requires_human; use billing setup above instead. See Billing & Plans for the full rule and error codes.

3. Add a sending domain

npx @molted/cli domains add yourapp.com

You'll get back a domain ID and DNS records (DKIM, SPF, DMARC) to add at your DNS provider. After adding them, verify using the domain ID from the add response:

npx @molted/cli domains verify <domain-id>

Sends from unverified domains will be blocked with no_verified_domain. Complete verification before sending.

4. Send your first email

npx @molted/cli send \
  --to recipient@example.com \
  --subject "Hello" \
  --body "First email from the CLI!"

The --to flag must be a valid email address (e.g. recipient@example.com). Invalid formats like bare usernames or missing domains are rejected by the server with a 400 Bad Request error.

The --subject and --body flags are shorthand for sending with the _default template. For HTML email, use --html instead of --body. For templates with custom payloads, use --template and --payload:

npx @molted/cli send \
  --to recipient@example.com \
  --template my-template \
  --dedupe-key "hello-1" \
  --payload '{"subject": "Hello", "firstName": "Ada"}'

A successful response:

Response
{
  "requestId": "req_abc123",
  "status": "queued"
}

Add --no-humanize to deliver a send exactly as written, or --humanize --humanize-style casual to ask for the humanizer in a specific style. molted send batch and molted threads reply take the same flags. Run molted send --help for the full flag list.

To see how the humanizer would rewrite an email before sending it, preview it (nothing is sent; a preview uses one humanizer credit):

npx @molted/cli humanizer preview --template welcome --payload '{"firstName": "Ada"}' --diff

An applied rewrite comes with a preview id. To send exactly that rewrite, pass it with the same content: --humanize-preview <previewId> on molted send or molted threads reply. See Preview a rewrite and Send exactly what you previewed.

5. Check delivery

npx @molted/cli trace req_abc123

This shows the full request lifecycle: policy checks, provider selection, and delivery status.

Using the CLI without npx

If you prefer, install globally:

npm install -g @molted/cli

Then use molted directly instead of npx @molted/cli:

molted send --to recipient@example.com --template _default ...

Updating

molted update

When a newer version is available, molted update installs it and shows a changelog summary of what changed between your old version and the new one:

Response
{
  "status": "updated",
  "from": "1.13.0",
  "to": "1.16.0",
  "changelog": [
    {
      "version": "1.16.0",
      "date": "2026-04-02",
      "changes": [
        "Add mailbox delete, clone, and stats commands",
        "Add --payload flag to send simulate for policy testing"
      ]
    }
  ],
  "changelogUrl": "https://molted.email/changelog",
  "hint": "Updated successfully. Run `molted --version` to verify."
}

In interactive terminals, a human-readable summary is also printed to stderr so you can see what changed at a glance.

Troubleshooting: molted doctor

To find out whether this key can send yet, or why mail isn't sending, run:

molted doctor

The "can I send?" checks run server-side in GET /v1/agent/setup (the same checklist the MCP setup_status tool returns): credentials, plan (trial, Free or paid, and whether sends are blocked), holds (an account-level pause, exhausted send quota, held inbound mail), the mailbox a send would use, its sending domain, what policy would do with a first send (sent, held for human approval, or blocked, from the mailbox's real autonomy level), and whether anything has been sent yet. Doctor leads with READY TO SEND or NOT READY TO SEND YET, the next step and whose move it is (yours, or your human's with the call that makes the link to hand them), then shows each step as a check. It also runs the after-send checks: items waiting for human approval, recent policy blocks, the dead-letter queue, and active alerts.

Response (--json, trial workspace, trimmed)
{
  "setup": {
    "readyToSend": false,
    "next": {
      "step": "plan",
      "owner": "human",
      "do": "Ask your human to activate a plan (Free or paid) so sends unblock. Call POST /v1/agent/login-token and give your human the returned `url`: while no person owns this workspace it is a link to claim it (`kind: \"claim\"`, valid 7 days), otherwise a link to sign in to the portal with their own login (`kind: \"portal\"`).",
      "request": { "method": "POST", "path": "/v1/agent/login-token", "body": {} }
    },
    "steps": [
      { "id": "plan", "status": "blocked", "owner": "human", "why": "Sends are blocked: the workspace is still on the trial plan and no plan has been activated.", "fix": "..." }
    ]
  },
  "blockers": [
    {
      "check": "plan",
      "detail": "Sends are blocked: the workspace is still on the trial plan and no plan has been activated. (your human's move)",
      "fix": "The owner is a verified human, so they can activate the Free plan themselves in the portal (Settings, Plan) or buy Solo or Team. ... Request: POST /v1/agent/login-token"
    }
  ],
  "checks": [
    { "name": "credentials", "status": "ok", "detail": "done: This key works for workspace tenant-slug-a1b2c3d4 with access to every mailbox." },
    { "name": "plan", "status": "blocker", "detail": "Sends are blocked: ...", "fix": "..." },
    { "name": "sending_domain", "status": "ok", "detail": "optional: you@agent.molted.email is on the shared domain agent.molted.email, which works for a first send. ..." },
    { "name": "first_send", "status": "ok", "detail": "todo: Nothing has been sent yet.", "fix": "..." },
    { "name": "held_approvals", "status": "ok", "detail": "Nothing waiting for human review." }
  ]
}

A blocked or open setup step is a blocker. first_send is the goal, not a prerequisite: nothing sent yet never makes doctor fail.

By default it prints a human-readable report to stdout instead; add --json for the shape above. molted doctor --mailbox <id> checks one mailbox the key may send from (and narrows held approvals to it) -- <id> must be a UUID (e.g. 3f1e0c2a-8b7d-4a3e-9c1f-2b6a7d8e9f01); a non-UUID value is rejected before any request is made, and a mailbox outside the key's scope is a blocker.

Exit code is 1 if any blocker was found (not ready to send); otherwise 2 if any check reports "status": "error" (the call itself failed) or "status": "unknown" (the call worked but couldn't be fully classified); otherwise 0. 0 means ready to send and every check fully verified, not just "nothing blocked" -- so a script or agent loop can gate on it:

molted doctor --json > /dev/null || echo "blocked, or not fully verified -- see molted doctor for why"

An after-send check that needs a scope the current key doesn't have (operator, for held approvals and alerts), or that hits a mailbox outside a scoped key's access, reports "status": "skipped", not "error" -- it isn't broken, the key just can't see that particular signal, and a skipped check doesn't affect the exit code on its own (the human-readable report says "N check(s) skipped" instead of claiming everything was fully verified). A 401 collapses into a single "auth" blocker regardless of how many of the underlying checks hit it. A malformed --mailbox (not a UUID) exits 2, not 1 -- 1 is reserved for an actual diagnosed blocker, and in that case no check even ran.

molted doctor calls no write endpoint. The setup check is read-only; checking alerts may notify configured alert destinations (deduplicated). Every fix it prints that would loosen a protection (unpausing a mailbox, resuming a paused account, choosing a plan) is phrased as something a human does, never something the command runs for you.

Next steps