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
| Flag | Type | Required | Description |
|---|---|---|---|
--template | string | Yes | Template slug or ID to use for all recipients. |
--recipients | string | No | JSON array of recipient objects (required unless using --stdin). |
--mailbox | string | No | Mailbox ID to send from. |
--stdin | flag | No | Read recipients JSON from stdin instead of --recipients. |
--humanize / --no-humanize | flag | No | Batch default: ask for or turn off the humanizer. A recipient with its own humanize overrides this. |
--humanize-style | string | No | Batch default style: casual, professional, or friendly. A recipient with its own humanizeStyle overrides this. |
--reply-to | string | No | Batch 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" }
]| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Recipient email address. A plain string in place of the object works too. |
dedupeKey | string | No | Unique key to prevent duplicate sends. Omitted, the CLI generates a random one (batch-...), so a rerun sends again: pass your own for safe retries. |
payload | object | No | Template variables for this recipient. |
humanize | boolean | No | Overrides --humanize/--no-humanize for this recipient only. |
humanizeStyle | string | No | Overrides --humanize-style for this recipient only. |
humanizePreviewId | string | No | This 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. |
replyTo | string | No | Overrides --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 --stdinPOST https://mailbox.molted.email/v1/agent/batch/request-sendRequires Bearer authentication.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
tenantId | string | No | Only on api.molted.email, where it is required; mailbox.molted.email fills it in from your key. |
templateId | string | No | Template slug or ID to use for all recipients (default _default). |
sends | array | Yes | Array of recipient objects (max 500). |
sends[].recipientEmail | string | Yes | Recipient email address. Missing or blank fails only this recipient (recipient_email_required). |
sends[].dedupeKey | string | Yes | Unique 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[].payload | object | No | Template variables for this recipient. |
sends[].sendReason | string | No | Annotate why the agent is sending (for audit trails). |
sends[].attachments | array | No | Up to 20 { "id": "<attachment uuid>" } refs for this recipient, checked as on the agent send. |
sends[].humanize | boolean | No | Overrides the batch-level humanize below for this recipient only. |
sends[].humanizeStyle | string | No | Overrides the batch-level humanizeStyle below for this recipient only. |
sends[].humanizePreviewId | string | No | This 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[].replyTo | string | No | Overrides the batch-level replyTo below for this recipient only. |
mailboxId | string | No | Mailbox ID to send from. |
humanize | boolean | No | Batch default. false always tightens (delivered exactly as written). true can't turn the humanizer on by itself -- see Humanizer precedence. |
humanizeStyle | string | No | Batch default style: casual, professional, or friendly. |
replyTo | string | No | Batch default Reply-To: one plain address where replies go. |
Example
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" }
]
}| Field | Type | Description |
|---|---|---|
batchId | string | Unique identifier for this batch. |
total | number | Total number of recipients in the batch. |
queued | number | Number of sends that passed policy and are queued for delivery. |
blocked | number | Number of sends blocked by the policy engine. |
scheduled | number | Number deferred (e.g. domain warmup); see each item's scheduledAt. |
errors | number | Number 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). |
results | array | Per-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
| Flag | Type | Required | Description |
|---|---|---|---|
--template | string | Yes | Template ID to simulate against. |
--recipients | string | Yes | JSON 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-batchRequires Bearer authentication.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
tenantId | string | No | Only on api.molted.email, where it is required; mailbox.molted.email fills it in from your key. |
templateId | string | Yes | Template ID to simulate against. |
recipientEmails | string[] | Yes | Array 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 -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" }
]
}| Field | Type | Description |
|---|---|---|
total | number | Total recipients evaluated. |
wouldAllow | number | Number that would pass policy (including those that would be held or deferred). |
wouldBlock | number | Number that would be blocked. |
wouldDefer | number | Number that would be deferred to a later warmup slot. |
results[].wouldAllow | boolean | Whether this recipient would pass policy checks. |
results[].reason | string | Why the recipient would be blocked (only present when wouldAllow is false). |
results[].cooldownExpiresAt | string | When the cooldown expires, if applicable. |
results[].wouldRequireApproval, results[].approvalReason | boolean, string | Present when the send would wait for a human (autonomy_level, thread_risk_flagged or content_check). |
results[].wouldDefer, results[].scheduledFor | boolean, string | Present 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.comIn MCP the tool is next_best_action (contact_email).
POST https://mailbox.molted.email/v1/agent/next-best-actionRequires Bearer authentication.
| Field | Type | Required | Description |
|---|---|---|---|
tenantId | string | No | Only on api.molted.email, where it is required; mailbox.molted.email fills it in from your key. |
contactEmail | string | Yes | The contact's email address. |
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" }'{
"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
}
}recommendation | When |
|---|---|
stop | The contact is suppressed, or one of their latest messages was an opt-out (platformLabel: "opt_out"). Do not send. |
escalate | One of their latest messages was flagged by the safety check (any verdict except clean). A person should look first. |
wait | You 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. |
nudge | Nothing 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 --stdinIn MCP the tool is batch_next_action (contact_emails).
POST https://mailbox.molted.email/v1/agent/batch/next-best-actionRequires Bearer authentication.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
tenantId | string | No | Only on api.molted.email, where it is required; mailbox.molted.email fills it in from your key. |
contactEmails | string[] | Yes | Array of contact email addresses (max 500). |
Example
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 --stdinIn 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-intentRequires Bearer authentication.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
tenantId | string | No | Only on api.molted.email, where it is required; mailbox.molted.email fills it in from your key. |
messages | array | Yes | Array of message objects to classify (max 500). |
messages[].subject | string | No | The email subject line (default empty). |
messages[].bodyText | string | No | The plain text body of the email (default empty). |
messages[].bodyHtml | string | No | HTML body -- feeds the excessive-links safety signal. |
messages[].fromEmail | string | No | Sender address -- loads that sender's tenant safety settings, spam feedback, reputation and allow-list status. Omit when there's no real sender. |
messages[].fromName | string | No | From display name, if known. |
messages[].replyTo | string | No | Reply-To address, if known. |
Example
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.
Related
- 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