MOLTED EMAIL

Outbound

Send and reply to email from a specific mailbox using the mailbox-scoped outbound API.

The outbound API is the mailbox-scoped send surface used by agentic mailbox clients. It handles thread projection, canary token detection, deduplication, and autonomy level gating per mailbox.

Outbound vs. agent send

Molted provides two sending surfaces:

EndpointBest for
POST /v1/agent/request-sendTenant-level sends from any agent. Full policy trace and scheduled send support.
POST /v1/agent/outbound/sendMailbox-scoped sends. Thread projection, canary detection, per-mailbox autonomy enforcement.

Use /v1/agent/outbound/send when your agent acts on behalf of a specific mailbox and you want thread continuity and per-mailbox autonomy controls. Use /v1/agent/request-send for general-purpose tenant-level sends.

See Sending Email for full documentation on the agent send endpoint.

Endpoints

MethodPathDescription
POST/v1/agent/outbound/sendSend an email from a mailbox
POST/v1/agent/outbound/replyReply to an existing thread
POST/v1/agent/outbound/schedule-followupSchedule a followup if no reply is received

All endpoints require Bearer authentication and the automation API key scope.

Send

Send a new email from a mailbox. Creates a new thread if no matching thread exists.

POST https://api.molted.email/v1/agent/outbound/send
curl
curl -X POST "https://api.molted.email/v1/agent/outbound/send?tenantId=TENANT_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipientEmail": "alice@example.com",
    "templateId": "welcome",
    "dedupeKey": "welcome-alice-1",
    "payload": { "name": "Alice" },
    "mailboxId": "mbx_abc123"
  }'

Request body

FieldTypeRequiredDescription
recipientEmailstringYesRecipient email address
templateIdstringYesTemplate ID to use for the send
dedupeKeystringYesUnique key to prevent duplicate sends. Same key on a second call returns the original result.
payloadobjectYesTemplate variables
mailboxIdstringNoSend from a specific mailbox. Enables per-mailbox autonomy level enforcement.
threadIdstringNoAssociate with an existing thread
subjectstringNoSubject line for the email. Used by the _default template (or as a fallback if payload.subject is set); ignored by a named template, which renders its own subject.
sendReasonstringNoReason for sending, recorded in the decision trace
idempotencyKeystringNoAlternative deduplication key
replyTostringNoWhere the recipient's replies go: one plain address. See Reply-To.

Response

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

Response status values:

StatusMeaning
queuedAccepted, headed for delivery
scheduledAccepted but deferred (e.g. domain warmup); check scheduledAt
blockedBlocked by the policy engine; check reason (a same-dedupeKey retry comes back blocked with reason: "duplicate", and the original requestId)
pending_approvalHeld by the mailbox's autonomy level

A blocked or pending_approval status is not an error - it means the policy engine or autonomy level is working correctly. Always check the status field. See Errors & Policy Blocks for every reason value.

CLI

molted send
# The send CLI uses /v1/agent/request-send by default
# To use the outbound endpoint, see: molted threads reply

molted send --to alice@example.com --template welcome \
  --dedupe-key "welcome-alice-1" --payload '{"name": "Alice"}' \
  --mailbox mbx_abc123

Reply

Reply to an existing thread. The reply is sent from the mailbox that owns the thread.

POST https://api.molted.email/v1/agent/outbound/reply
curl
curl -X POST "https://api.molted.email/v1/agent/outbound/reply?tenantId=TENANT_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "threadId": "thr_abc123",
    "templateId": "_default",
    "dedupeKey": "reply-alice-1",
    "payload": { "body": "Thanks for reaching out - we will get back to you shortly." }
  }'

Request body

FieldTypeRequiredDescription
threadIdstringYesThread to reply to
templateIdstringYesTemplate ID. Use _default for plain-text replies.
dedupeKeystringYesUnique key to prevent duplicate replies
payloadobjectYesTemplate variables. Set payload.subject to override the reply subject; if omitted, it defaults to Re: <thread subject>.
sendReasonstringNoReason for replying
idempotencyKeystringNoAlternative deduplication key
replyTostringNoWhere the contact's answer goes, e.g. a person you hand the conversation to: one plain address. See Reply-To.

A reply goes to the Reply-To of the message it answers (inReplyToMessageId, else the latest inbound message) only when that is an address you already wrote to in the thread, or the same company's (non-shared) domain when the message was sent from that company and passed DMARC for it; otherwise to the thread's contact. The response's replyRecipient says which and why. See who a reply goes to.

A reply with no subject in payload defaults to Re: <thread subject> (never doubled if the thread's subject already starts with "Re:"). The reply also carries In-Reply-To/References headers, built from the contact's latest inbound message and your own earlier messages in the thread (when a Message-ID is on file for them), so mail clients (Gmail, Outlook, Apple Mail, ...) thread it into the same conversation instead of starting a new one.

CLI

molted threads reply
# Shorthand with plain text body -- subject defaults to "Re: <thread subject>"
molted threads reply thr_abc123 --body "Thanks for reaching out!"

# Override the default subject
molted threads reply thr_abc123 --body "Thanks for reaching out!" --subject "Re: Question"

# With a specific template and payload
molted threads reply thr_abc123 \
  --template-id follow-up \
  --payload '{"name": "Alice"}' \
  --dedupe-key "reply-alice-2"

# Introduce a colleague: their answer goes to Sam, not the mailbox
molted threads reply thr_abc123 --body "Sam will take it from here." --reply-to sam@acme.dev

Schedule followup

Schedule a followup email that sends automatically if the contact does not reply within a specified delay.

POST https://api.molted.email/v1/agent/outbound/schedule-followup
curl
curl -X POST "https://api.molted.email/v1/agent/outbound/schedule-followup?tenantId=TENANT_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contactEmail": "alice@example.com",
    "threadRequestId": "req_abc123",
    "delayMinutes": 4320,
    "cancelOnReply": true
  }'

Request body

FieldTypeRequiredDescription
contactEmailstringYesContact email address
threadRequestIdstringYesThe requestId from the initial send response
delayMinutesintegerYesDelay in minutes before the followup sends (1-43200, max 30 days)
cancelOnReplybooleanNoCancel the followup automatically if the contact replies first
triggerConditionsobjectNotemplateId and payload choose the followup's content -- see below

The threadRequestId is the requestId returned from the send or reply response. Save this value when sending if you plan to schedule a follow-up later.

Choosing the content

A followup has no content of its own at schedule time -- it's rendered when it fires, from triggerConditions:

  • triggerConditions.templateId names a template slug (or ID) to render. If you don't set one, it defaults to followup-default -- create a template with that slug in your tenant to give every followup that names no template its content.
  • triggerConditions.payload supplies that template's variables.
  • Set templateId to _default to skip template lookup and pass literal subject/html/text directly in payload instead.
curl
curl -X POST "https://api.molted.email/v1/agent/outbound/schedule-followup?tenantId=TENANT_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contactEmail": "alice@example.com",
    "threadRequestId": "req_abc123",
    "delayMinutes": 4320,
    "cancelOnReply": true,
    "triggerConditions": {
      "templateId": "followup-nudge",
      "payload": { "firstName": "Alice" }
    }
  }'

If no template resolves (no templateId given and no followup-default template exists, or an explicit templateId that doesn't exist) or the template fails to render, the followup is cancelled (reason template_not_found or render_failed) rather than sent with no content.

Approval hold

A followup goes through the same autonomy level gate as any other agent send. At level 3 it's queued and sent automatically; at level 1 (every send) or level 2 (first contact with this recipient) it's held for approval and shows up in the portal's approval queue like any other held send -- approving it sends the same content that was rendered when it fired.

At the scheduled time, the follow-up is also checked against suppressions and your billing plan before it sends. If the contact has since been suppressed, or your plan is trial (not yet activated) or expired, the follow-up is cancelled (reason suppressed, trial_not_activated, or subscription_expired, recorded in the audit trail) -- it will not silently disappear.

A follow-up sent at autonomy level 3 also counts against your send quota -- the same monthly/daily/hourly budget an API send draws from. Over the daily or hourly limit, the follow-up is deferred and retried automatically once that window resets, the same as a paused mailbox, rather than cancelled. Over the monthly limit (or its overage cap, on a plan with overage), the follow-up is cancelled with reason monthly_limit_exceeded or overage_cap_exceeded, recorded the same way as the other cancellation reasons above.

Errors

StatusCodeMeaning
404send_request_not_foundNo send request matches threadRequestId for this tenant. Use the requestId returned from the original send or reply call.

CLI

molted threads followup
# Schedule followup 72 hours after initial send
molted threads followup req_abc123 \
  --contact-email alice@example.com \
  --delay-hours 72 \
  --cancel-on-reply

Deduplication

Every outbound send and reply requires a dedupeKey. The same key on a second call returns the original result without sending again. Use deterministic keys based on your workflow, contact, and step:

{workflow}-{contact-id}-{step}
# e.g. "onboard-alice-day1", "renewal-alice-2026q1"

If you use an auto-generated key (like the CLI does with --dedupe-key auto), each call is treated as a new send.

Empty or whitespace-only values (e.g. --dedupe-key "") are rejected up front with 400 dedupe_key_required. Silently accepting them would collapse every such send into a single "duplicate" and block all subsequent sends after the first.

Autonomy levels

Outbound sends through a mailbox are subject to that mailbox's autonomy level:

  • L1 (Assisted) - Every send requires human approval. Response status: pending_approval.
  • L2 (Guarded Auto) - First message to a new contact requires approval. Subsequent messages send automatically.
  • L3 (Fully Autonomous) - All policy-approved sends execute immediately.

When a send is held for approval, use the approval queues API to review and release it.