MOLTED EMAIL

Batch Operations

Send, simulate, classify, and get recommendations for up to 500 recipients in a single request.

Batch endpoints let you process multiple items in a single API call -- up to 500 per request. Each item is evaluated independently with full policy enforcement.

Batch send

Send email to up to 500 recipients in one request. Each recipient gets their own policy evaluation and deduplication check.

molted send batch --template <id> --recipients '<json>' [--mailbox <id>] [--stdin]

Options

FlagTypeRequiredDescription
--templatestringYesTemplate slug or ID to use for all recipients.
--recipientsstringNoJSON array of recipient objects (required unless using --stdin).
--mailboxstringNoMailbox ID to send from.
--stdinflagNoRead recipients JSON from stdin instead of --recipients.
--humanize / --no-humanizeflagNoBatch default: ask for or turn off the humanizer. A recipient with its own humanize overrides this.
--humanize-stylestringNoBatch default style: casual, professional, or friendly. A recipient with its own humanizeStyle overrides this.
--reply-tostringNoBatch default Reply-To: one plain address where replies go. A recipient with its own replyTo overrides this.

Recipient format

[
  { "email": "alice@example.com", "dedupeKey": "promo-alice", "payload": { "name": "Alice" } },
  { "email": "bob@example.com", "dedupeKey": "promo-bob", "payload": { "name": "Bob" }, "humanize": true, "humanizeStyle": "casual" }
]
FieldTypeRequiredDescription
emailstringYesRecipient email address. A plain string in place of the object works too.
dedupeKeystringNoUnique key to prevent duplicate sends. Omitted, the CLI generates a random one (batch-...), so a rerun sends again: pass your own for safe retries.
payloadobjectNoTemplate variables for this recipient.
humanizebooleanNoOverrides --humanize/--no-humanize for this recipient only.
humanizeStylestringNoOverrides --humanize-style for this recipient only.
humanizePreviewIdstringNoThis recipient's own previewed rewrite (preview with --template and this recipient's payload). One preview per recipient: an id sends once. A preview that doesn't fit, or was already used, fails only this recipient.
replyTostringNoOverrides --reply-to for this recipient only.

Example

molted send batch --template campaign-march \
  --recipients '[{"email":"alice@example.com","dedupeKey":"march-alice","payload":{"name":"Alice"}},{"email":"bob@example.com","dedupeKey":"march-bob","payload":{"name":"Bob"}}]'

Using stdin (useful for large recipient lists):

cat recipients.json | molted send batch --template campaign-march --stdin
POST https://mailbox.molted.email/v1/agent/batch/request-send

Requires Bearer authentication.

Parameters

FieldTypeRequiredDescription
tenantIdstringNoOnly on api.molted.email, where it is required; mailbox.molted.email fills it in from your key.
templateIdstringNoTemplate slug or ID to use for all recipients (default _default).
sendsarrayYesArray of recipient objects (max 500).
sends[].recipientEmailstringYesRecipient email address. Missing or blank fails only this recipient (recipient_email_required).
sends[].dedupeKeystringYesUnique key to prevent duplicate sends. Missing or blank fails only this recipient (dedupe_key_required); a key repeated within the batch fails every later occurrence (duplicate_in_batch).
sends[].payloadobjectNoTemplate variables for this recipient.
sends[].sendReasonstringNoAnnotate why the agent is sending (for audit trails).
sends[].attachmentsarrayNoUp to 20 { "id": "<attachment uuid>" } refs for this recipient, checked as on the agent send.
sends[].humanizebooleanNoOverrides the batch-level humanize below for this recipient only.
sends[].humanizeStylestringNoOverrides the batch-level humanizeStyle below for this recipient only.
sends[].humanizePreviewIdstringNoThis recipient's previewed rewrite. Per recipient only (there is no batch-level preview id), and each recipient needs its own preview: an id sends once. A preview that doesn't fit gives that recipient status: "error" with reason humanize_preview_mismatch, humanize_preview_not_found, humanize_preview_used, humanize_preview_conflict or invalid_humanize_preview_id; the rest of the batch still goes.
sends[].replyTostringNoOverrides the batch-level replyTo below for this recipient only.
mailboxIdstringNoMailbox ID to send from.
humanizebooleanNoBatch default. false always tightens (delivered exactly as written). true can't turn the humanizer on by itself -- see Humanizer precedence.
humanizeStylestringNoBatch default style: casual, professional, or friendly.
replyTostringNoBatch default Reply-To: one plain address where replies go.

Example

curl
curl -X POST https://mailbox.molted.email/v1/agent/batch/request-send \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "campaign-march",
    "sends": [
      {
        "recipientEmail": "alice@example.com",
        "dedupeKey": "march-alice",
        "payload": { "name": "Alice" }
      },
      {
        "recipientEmail": "bob@example.com",
        "dedupeKey": "march-bob",
        "payload": { "name": "Bob" }
      }
    ]
  }'

Response

{
  "batchId": "batch_abc123",
  "total": 2,
  "queued": 1,
  "blocked": 1,
  "scheduled": 0,
  "errors": 0,
  "results": [
    { "recipientEmail": "alice@example.com", "requestId": "req_001", "status": "queued" },
    { "recipientEmail": "bob@example.com", "requestId": "req_002", "status": "blocked", "reason": "suppressed" }
  ]
}
FieldTypeDescription
batchIdstringUnique identifier for this batch.
totalnumberTotal number of recipients in the batch.
queuednumberNumber of sends that passed policy and are queued for delivery.
blockednumberNumber of sends blocked by the policy engine.
schedulednumberNumber deferred (e.g. domain warmup); see each item's scheduledAt.
errorsnumberNumber that failed validation (duplicate dedupe key in this batch, missing recipient email, missing dedupe key) or hit an unexpected internal error (not a policy block either way).
resultsarrayPer-recipient results with status (queued, scheduled, pending_approval, blocked, or error) and optional reason. Note: queued/blocked/scheduled/errors above don't count pending_approval items -- tally those from results yourself if any recipient's mailbox runs below autonomy level 3.

Each item gets its own result -- some may be queued while others are blocked, scheduled, or held for approval. A scheduled item carries its scheduledAt. An item for a contact a human has taken over in the sending mailbox is blocked with reason: "thread_taken_over" and an empty requestId. An error item's reason is duplicate_in_batch, recipient_email_required, dedupe_key_required, invalid_humanize_style, one of the preview codes above, or internal_error; see Batch item errors.

In MCP the tool is batch_send: template_id, sends (each to, dedupe_key, and optional payload, send_reason, humanize, humanize_style, humanize_preview_id, reply_to), mailbox_id, and the batch-level humanize, humanize_style and reply_to.

Simulate batch

Dry-run a batch send to check policy decisions without delivering. Use this to verify which recipients would be allowed before committing to a real send.

molted send simulate-batch --template <id> --recipients '<json>'

Options

FlagTypeRequiredDescription
--templatestringYesTemplate ID to simulate against.
--recipientsstringYesJSON array of email strings or recipient objects.

The recipients flag accepts either plain email strings or full {email, dedupeKey, payload} objects. For simulation, only the email address is used. There is no MCP tool for a batch simulation; MCP dry_run checks one recipient.

Example

molted send simulate-batch --template campaign-march \
  --recipients '["alice@example.com", "bob@example.com"]'
POST https://mailbox.molted.email/v1/agent/simulate-batch

Requires Bearer authentication.

Parameters

FieldTypeRequiredDescription
tenantIdstringNoOnly on api.molted.email, where it is required; mailbox.molted.email fills it in from your key.
templateIdstringYesTemplate ID to simulate against.
recipientEmailsstring[]YesArray of recipient email addresses (max 500).

Each recipient gets the batch send's own checks (no lease check). No payload is taken, so content is never rendered: this is a policy-only check.

Example

curl
curl -X POST https://mailbox.molted.email/v1/agent/simulate-batch \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "templateId": "campaign-march",
    "recipientEmails": ["alice@example.com", "bob@example.com"]
  }'

Response

{
  "total": 2,
  "wouldAllow": 1,
  "wouldBlock": 1,
  "results": [
    { "recipientEmail": "alice@example.com", "wouldAllow": true },
    { "recipientEmail": "bob@example.com", "wouldAllow": false, "reason": "cooldown", "cooldownExpiresAt": "2025-03-15T14:30:00Z" }
  ]
}
FieldTypeDescription
totalnumberTotal recipients evaluated.
wouldAllownumberNumber that would pass policy (including those that would be held or deferred).
wouldBlocknumberNumber that would be blocked.
wouldDefernumberNumber that would be deferred to a later warmup slot.
results[].wouldAllowbooleanWhether this recipient would pass policy checks.
results[].reasonstringWhy the recipient would be blocked (only present when wouldAllow is false).
results[].cooldownExpiresAtstringWhen the cooldown expires, if applicable.
results[].wouldRequireApproval, results[].approvalReasonboolean, stringPresent when the send would wait for a human (autonomy_level, thread_risk_flagged or content_check).
results[].wouldDefer, results[].scheduledForboolean, stringPresent when the send would land on a later day (the warmup projection is spread across the batch).

Next-best-action for one contact

Ask what to do next with one contact. The answer comes from the contact's recent sends and inbound mail, their suppression status and the safety verdicts on their latest messages, and it is one of four recommendations.

molted next-action --contact alice@example.com

In MCP the tool is next_best_action (contact_email).

POST https://mailbox.molted.email/v1/agent/next-best-action

Requires Bearer authentication.

FieldTypeRequiredDescription
tenantIdstringNoOnly on api.molted.email, where it is required; mailbox.molted.email fills it in from your key.
contactEmailstringYesThe contact's email address.
curl
curl -X POST https://mailbox.molted.email/v1/agent/next-best-action \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contactEmail": "alice@example.com" }'
Response
{
  "recommendation": "nudge",
  "reasoning": "No activity for 9 days. Consider a gentle follow-up.",
  "contactSummary": {
    "email": "alice@example.com",
    "name": "Alice",
    "lastSendAt": "2026-10-16T09:12:00.000Z",
    "lastInboundAt": null,
    "isSuppressed": false,
    "activeIncidents": 0
  }
}
recommendationWhen
stopThe contact is suppressed, or one of their latest messages was an opt-out (platformLabel: "opt_out"). Do not send.
escalateOne of their latest messages was flagged by the safety check (any verdict except clean). A person should look first.
waitYou sent to them in the last 24 hours (a send waiting for approval counts) and they have not replied since, or there is no strong signal either way.
nudgeNothing has happened for more than 7 days, or there is no prior activity at all.

The rules are checked in this order: suppressed (stop), then a flagged safety verdict (escalate), then an opt-out (stop), then the 24-hour and 7-day checks. "Latest messages" means the 10 most recent classified messages from that address. reasoning is a sentence you can log or show. contactSummary.name is null when the contact has no saved name, and activeIncidents counts the workspace's open or acknowledged incidents, not the contact's. A mailbox-scoped key only sees activity in its own mailboxes: a contact whose activity is all elsewhere reads as if it had none.

Batch next-best-action

Get recommendations for up to 500 contacts at once, the same answer as above for each (recommendation and reasoning; no contactSummary).

molted next-action batch --contacts '["alice@example.com", "bob@example.com"]'
# or: cat contacts.json | molted next-action batch --stdin

In MCP the tool is batch_next_action (contact_emails).

POST https://mailbox.molted.email/v1/agent/batch/next-best-action

Requires Bearer authentication.

Parameters

FieldTypeRequiredDescription
tenantIdstringNoOnly on api.molted.email, where it is required; mailbox.molted.email fills it in from your key.
contactEmailsstring[]YesArray of contact email addresses (max 500).

Example

curl
curl -X POST https://mailbox.molted.email/v1/agent/batch/next-best-action \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contactEmails": ["alice@example.com", "bob@example.com"]
  }'

Response

{
  "results": [
    { "contactEmail": "alice@example.com", "recommendation": "nudge", "reasoning": "No reply in 5 days" },
    { "contactEmail": "bob@example.com", "recommendation": "wait", "reasoning": "Recent send 2 hours ago" }
  ]
}

A contact that cannot be looked up is left out of results instead of failing the whole batch.

Batch classify

Classify up to 500 inbound messages at once: each gets its platform label, safety verdict and route.

molted classify batch --emails '[{"subject": "Re: Order", "body": "When does it ship?"}]'
# or: cat emails.json | molted classify batch --stdin

In MCP the tool is batch_classify (messages, each with subject, body_text and the optional sender fields).

POST https://mailbox.molted.email/v1/agent/batch/classify-intent

Requires Bearer authentication.

Parameters

FieldTypeRequiredDescription
tenantIdstringNoOnly on api.molted.email, where it is required; mailbox.molted.email fills it in from your key.
messagesarrayYesArray of message objects to classify (max 500).
messages[].subjectstringNoThe email subject line (default empty).
messages[].bodyTextstringNoThe plain text body of the email (default empty).
messages[].bodyHtmlstringNoHTML body -- feeds the excessive-links safety signal.
messages[].fromEmailstringNoSender address -- loads that sender's tenant safety settings, spam feedback, reputation and allow-list status. Omit when there's no real sender.
messages[].fromNamestringNoFrom display name, if known.
messages[].replyTostringNoReply-To address, if known.

Example

curl
curl -X POST https://mailbox.molted.email/v1/agent/batch/classify-intent \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "messages": [
      { "subject": "Re: Partnership proposal", "bodyText": "Sure, let'\''s set up a call next week", "fromEmail": "alice@example.com" },
      { "subject": "Re: Your offer", "bodyText": "Please remove me from your mailing list." }
    ]
  }'

Response

One result per message, in order. platformLabel is opt_out or null here (the endpoint takes no headers, so autoReplyKind is always unknown); see Classify an email for the full field reference. The deprecated intent is still there, unchanged, and the response carries a Deprecation: true header for it.

{
  "results": [
    { "platformLabel": null, "autoReplyKind": "unknown", "safetyVerdict": "clean", "route": { "actionType": "notify_owner", "slaMinutes": 60, "status": "pending" }, "intent": "unclassified", "confidence": 0.5, "masked": true, "suggestedAction": "notify_owner" },
    { "platformLabel": "opt_out", "autoReplyKind": "unknown", "safetyVerdict": "clean", "route": { "actionType": "trigger_unsubscribe", "slaMinutes": 0, "status": "completed" }, "intent": "unsubscribe", "confidence": 0.95, "masked": false, "suggestedAction": "trigger_unsubscribe" }
  ]
}

Limits

All batch endpoints accept a maximum of 500 items per request. A request above the limit is rejected whole with a 400 Bad Request, and nothing in it is processed:

{ "error": "batch_too_large", "message": "sends has 600 items; the maximum is 500. Split it into batches of 500 or fewer.", "field": "sends", "max": 500, "received": 600 }

Split larger jobs into batches of 500 or fewer.

  • Sending Email -- single-recipient send with full policy trace
  • Policy Simulation -- understand how the policy engine evaluates sends
  • Templates -- manage the templates referenced in batch sends
  • Errors -- all possible block reasons and error codes