MOLTED EMAIL

Human-in-the-Loop Email Approvals

Keep humans in the loop for sensitive or high-stakes email sends. Use autonomy levels and the approval API to build review workflows around your agent.

Fully autonomous agents are the goal for routine email - transactional confirmations, onboarding sequences, follow-up replies. But for outbound prospecting, legal communications, or any email your agent hasn't proven itself on yet, you want a human to review the draft before it goes out.

Molted's autonomy levels and approval API give you fine-grained control over when humans review agent-proposed sends, without changing your agent code.

How it works

Every send request flows through two gates:

  1. Policy engine - checks deduplication, rate limits, suppression, consent, and risk budgets
  2. Autonomy level - decides whether a passing send goes straight to delivery or waits for human approval

Your agent always calls the same endpoint. The approval requirement is a mailbox setting - the agent proposes, the infrastructure decides whether to deliver immediately or route to the approval queue.

agent calls /v1/agent/send/request


  policy engine runs 20+ rules

        ├── BLOCKED → returns blockReason to agent

        └── PASSED → autonomy level check

                ├── L1/L2 trigger → held for approval → human reviews
                │                         │
                │                   approved → delivered
                │                   rejected → not delivered

                └── L3 → delivered immediately

Autonomy levels

Set per mailbox. Default is L3 (fully autonomous).

LevelNameWhen to use
L1AssistedEvery send waits for approval. For new agents, high-stakes mailboxes, or any context where you want full visibility.
L2Guarded AutoFirst message to a new contact waits for approval. Replies and follow-ups to known contacts auto-send. Good for outbound prospecting where new contacts need review but ongoing threads do not.
L3Fully AutonomousNo approval required. Right for proven transactional agents with established patterns.

Set up a supervised mailbox

Set a mailbox to L1 or L2 via the portal, CLI, or API:

CLI
molted mailboxes autonomy set YOUR_MAILBOX_ID --level 1
API
curl -X PATCH "https://api.molted.email/v1/agent/mailboxes/YOUR_MAILBOX_ID/autonomy?tenantId=YOUR_TENANT_ID" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"autonomyLevel": 1}'

Your agent code does not change. The next send from this mailbox will be held for approval instead of delivered.

Check the approval queue

When a send is held, it appears in the approval queue. Poll it or integrate it into your internal tools:

List pending approvals
curl "https://api.molted.email/v1/send/approvals?tenantId=YOUR_TENANT_ID&status=pending" \
  -H "Authorization: Bearer YOUR_API_KEY"
Response
{
  "approvals": [
    {
      "id": "appr_abc123",
      "requestId": "req_xyz789",
      "status": "pending",
      "recipientEmail": "prospect@example.com",
      "subject": "Following up on your trial",
      "htmlPreview": "<p>Hi Sarah, just wanted to check in...</p>",
      "agentId": "outreach-agent",
      "createdAt": "2026-04-01T09:00:00Z",
      "expiresAt": "2026-04-02T09:00:00Z"
    }
  ]
}

Approve or reject

Approve
curl -X PATCH "https://api.molted.email/v1/send/approvals/appr_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "approved"}'
Reject
curl -X PATCH "https://api.molted.email/v1/send/approvals/appr_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"status": "rejected", "rejectionReason": "Tone too aggressive - rephrase"}'

Approved sends are delivered immediately. Rejected sends are not delivered. Pending approvals expire after 24 hours by default.

Build an approval workflow with your agent

A practical pattern: your agent submits the send, then polls for the approval decision and acts on it.

agent.ts (TypeScript)
import { MailClient } from "@molted/mail";

const client = new MailClient({
  baseUrl: "https://api.molted.email",
  apiKey: process.env.MOLTED_API_KEY!,
});

async function sendWithApproval(
  to: string,
  subject: string,
  body: string
): Promise<{ delivered: boolean; reason?: string }> {
  // Submit the send
  const result = await client.requestSend({
    tenantId: process.env.MOLTED_TENANT_ID!,
    recipientEmail: to,
    templateId: "_default",
    dedupeKey: `${to}-${subject}`,
    payload: { subject, html: body, text: body },
  });

  if (result.status === "blocked") {
    return { delivered: false, reason: `Policy block: ${result.blockReason}` };
  }

  if (result.status === "queued") {
    // Immediately queued - L3 mailbox, no approval needed
    return { delivered: true };
  }

  if (result.status === "pending_approval") {
    // L1 or L2 mailbox - wait for human review
    console.log(`Send held for approval. requestId: ${result.requestId}`);
    console.log(`Review in portal or via API: /v1/send/approvals`);

    // Optionally: poll for the decision (or use webhooks instead)
    return { delivered: false, reason: "Pending human approval" };
  }

  return { delivered: false, reason: "Unknown status" };
}
agent.py (Python)
import os
import time
import requests


def send_with_approval(to: str, subject: str, body: str) -> dict:
    """Submit a send and return immediately. Check approval queue for decision."""
    response = requests.post(
        "https://api.molted.email/v1/agent/send/request",
        headers={
            "Authorization": f"Bearer {os.environ['MOLTED_API_KEY']}",
            "Content-Type": "application/json",
        },
        json={
            "tenantId": os.environ["MOLTED_TENANT_ID"],
            "recipientEmail": to,
            "templateId": "_default",
            "dedupeKey": f"{to}-{subject}",
            "payload": {"subject": subject, "html": body, "text": body},
        },
        timeout=10,
    )
    data = response.json()

    if data["status"] == "blocked":
        return {"delivered": False, "reason": f"Policy block: {data['blockReason']}"}

    if data["status"] == "queued":
        return {"delivered": True, "requestId": data["requestId"]}

    if data["status"] == "pending_approval":
        print(f"Send held for approval. requestId: {data['requestId']}")
        print("Review in portal: Dashboard > Approvals")
        return {"delivered": False, "reason": "pending_approval", "requestId": data["requestId"]}

    return {"delivered": False, "reason": "unknown"}

Use webhooks to react to approval decisions

Instead of polling, set up a webhook to notify your agent or internal system when a human approves or rejects:

Register a webhook
curl -X POST "https://api.molted.email/v1/webhooks" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "tenantId": "YOUR_TENANT_ID",
    "url": "https://your-app.example.com/webhooks/email-approval",
    "events": ["approval.approved", "approval.rejected", "approval.expired"]
  }'

Your webhook handler receives the decision:

webhook-handler.ts
import type { NextRequest } from "next/server";

export async function POST(req: NextRequest) {
  const event = await req.json();

  if (event.type === "approval.approved") {
    const { requestId, recipientEmail } = event.data;
    console.log(`Email to ${recipientEmail} approved and delivered. requestId: ${requestId}`);
    // Notify your agent, update CRM, etc.
  }

  if (event.type === "approval.rejected") {
    const { requestId, rejectionReason } = event.data;
    console.log(`Email rejected. Reason: ${rejectionReason}`);
    // Alert agent, log for review, potentially regenerate with feedback
  }

  if (event.type === "approval.expired") {
    console.log(`Approval expired. requestId: ${event.data.requestId}`);
    // Handle timeout - re-queue or abandon
  }

  return new Response("ok");
}

See Webhooks for full event reference and signature verification.

Progressive trust: start supervised, graduate to autonomous

A reliable pattern for deploying new agents safely:

Phase 1 - L1 (Assisted): Every send is reviewed. Build confidence in the agent's output quality and volume. Review for tone, targeting accuracy, and edge cases.

Phase 2 - L2 (Guarded Auto): Once first-contact quality is proven, graduate to L2. New prospects are still reviewed, ongoing threads auto-send. This is the right steady state for most outbound agents.

Phase 3 - L3 (Fully Autonomous): After L2 is stable with no rejection patterns, graduate to L3 for high-confidence mailboxes (transactional, support replies, internal notifications).

Switch levels at any time via API or portal - no code changes needed:

# Graduate from L1 to L2 after reviewing 100 sends
molted mailboxes autonomy set YOUR_MAILBOX_ID --level 2

Separate mailboxes for different risk profiles

A single tenant can have multiple mailboxes with different autonomy levels. This lets you apply the right level of oversight per use case without a blanket policy:

MailboxAutonomyUse case
outreach@yourdomain.comL2Outbound prospecting - new contacts reviewed
support@yourdomain.comL3Support replies - proven patterns, auto-send
enterprise@yourdomain.comL1Enterprise outreach - always reviewed
billing@yourdomain.comL3Transactional dunning - fully automated