MOLTED EMAIL

Migrating from Resend, Postmark, or SES

Switch your AI agent's email sending from Resend, Postmark, or Amazon SES to Molted. Step-by-step migration with side-by-side code examples.

If you're already sending email from an AI agent using Resend, Postmark, or Amazon SES, migrating to Molted takes under an hour. Your existing provider keeps working - Molted routes through it with automatic failover. What changes is that every send now goes through a policy engine before delivery.

This guide walks through the migration for each provider with side-by-side code examples.

What changes and what stays the same

Stays the same:

  • Your sending domain and DNS records
  • Your existing provider account (Molted can use it as a delivery backend)
  • Your email templates and content

What changes:

  • Send calls go to Molted's Agent Runtime API instead of your provider's API directly
  • The request shape adds mailboxId, tenantId, dedupeKey, and agentId
  • Your agent gets a reply inbox, policy enforcement, and decision traces

What you gain:

  • Automatic deduplication across runs (no more re-sending because your agent forgot)
  • Rate limits enforced at the infrastructure layer, not in your code
  • Suppression list management - unsubscribes and bounces handled automatically
  • Inbound reply handling - replies land in a classified inbox your agent can read
  • Immutable decision traces - every send records which rules fired and why
  • Multi-provider failover - if your primary provider goes down, sends route to a backup automatically

Before you start

  1. Create a Molted account at molted.email/signup
  2. Create a mailbox for your agent (portal under Mailboxes, or via API)
  3. Note your tenantId, mailboxId, and API key - you'll need them in every request
Create a mailbox
curl -X POST https://api.molted.email/v1/me/mailboxes \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Outbound Agent",
    "emailAddress": "agent@yourdomain.com"
  }'

Migrating from Resend

Before (Resend)

Before — Resend
import { Resend } from "resend";

const resend = new Resend(process.env.RESEND_API_KEY);

await resend.emails.send({
  from: "agent@yourdomain.com",
  to: "alice@example.com",
  subject: "Welcome to the trial",
  html: "<p>Thanks for signing up.</p>",
});

After (Molted)

After — Molted
const response = await fetch("https://api.molted.email/v1/agent/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MOLTED_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    tenantId: process.env.MOLTED_TENANT_ID,
    mailboxId: process.env.MOLTED_MAILBOX_ID,
    recipientEmail: "alice@example.com",
    templateId: "_default",
    dedupeKey: "alice@example.com-trial-welcome",
    agentId: "my-agent",
    payload: {
      subject: "Welcome to the trial",
      html: "<p>Thanks for signing up.</p>",
      text: "Thanks for signing up.",
    },
  }),
});

const result = await response.json();

if (result.status === "blocked") {
  console.error("Send blocked:", result.blockReason);
  return;
}

console.log("Sent:", result.requestId);

Key differences

ResendMolted
from field sets the sender addressSender address is set on the mailbox - no from in the request
No deduplicationdedupeKey prevents duplicate sends across runs
No policy engine20+ rules evaluated before delivery
No inbound repliesReplies land in the mailbox inbox

Migrating from Postmark

Before (Postmark)

Before — Postmark
import postmarker
from postmarker.core import PostmarkClient

client = PostmarkClient(server_token=os.environ["POSTMARK_API_KEY"])

client.emails.send(
    From="agent@yourdomain.com",
    To="alice@example.com",
    Subject="Welcome to the trial",
    HtmlBody="<p>Thanks for signing up.</p>",
    TextBody="Thanks for signing up.",
)

After (Molted)

After — Molted
import os
import requests

resp = requests.post(
    "https://api.molted.email/v1/agent/send",
    json={
        "tenantId": os.environ["MOLTED_TENANT_ID"],
        "mailboxId": os.environ["MOLTED_MAILBOX_ID"],
        "recipientEmail": "alice@example.com",
        "templateId": "_default",
        "dedupeKey": "alice@example.com-trial-welcome",
        "agentId": "my-agent",
        "payload": {
            "subject": "Welcome to the trial",
            "html": "<p>Thanks for signing up.</p>",
            "text": "Thanks for signing up.",
        },
    },
    headers={"Authorization": f"Bearer {os.environ['MOLTED_API_KEY']}"},
    timeout=10,
)
resp.raise_for_status()
result = resp.json()

if result["status"] == "blocked":
    print(f"Send blocked: {result['blockReason']}")
else:
    print(f"Sent: {result['requestId']}")

Key differences

PostmarkMolted
From / To in request bodySender is set on the mailbox; only recipient in request
Postmark message streams for transactional vs broadcastAutonomy levels control human-approval requirements per mailbox
Postmark suppressions managed per-streamMolted suppressions managed per-mailbox, auto-enforced on every send
Postmark webhooks for delivery eventsMolted events via webhook or molted listen CLI stream

Migrating from Amazon SES

Before (SES with AWS SDK)

Before — SES
import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";

const ses = new SESClient({ region: "us-east-1" });

await ses.send(
  new SendEmailCommand({
    Source: "agent@yourdomain.com",
    Destination: { ToAddresses: ["alice@example.com"] },
    Message: {
      Subject: { Data: "Welcome to the trial" },
      Body: {
        Html: { Data: "<p>Thanks for signing up.</p>" },
        Text: { Data: "Thanks for signing up." },
      },
    },
  })
);

After (Molted)

After — Molted
const response = await fetch("https://api.molted.email/v1/agent/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.MOLTED_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    tenantId: process.env.MOLTED_TENANT_ID,
    mailboxId: process.env.MOLTED_MAILBOX_ID,
    recipientEmail: "alice@example.com",
    templateId: "_default",
    dedupeKey: "alice@example.com-trial-welcome",
    agentId: "my-agent",
    payload: {
      subject: "Welcome to the trial",
      html: "<p>Thanks for signing up.</p>",
      text: "Thanks for signing up.",
    },
  }),
});

const result = await response.json();

Key differences

SESMolted
IAM credentials + region configSingle API key
Raw SMTP or SES API - no policy layer20+ policy rules enforced before every send
No built-in deduplicationdedupeKey deduplication window per mailbox
Bounce handling via SNS/SQS pipelineBounces automatically added to suppression list
Separate suppression list managementSuppressions enforced automatically

Handling the dedupeKey

The dedupeKey is the most important new concept in Molted. It prevents your agent from sending the same email twice if it runs multiple times, retries a failed operation, or encounters a bug.

Rule: Make the key stable and specific to the intent.

Good dedupeKey patterns
// Onboarding email: one per user signup
dedupeKey: `${userId}-onboarding-welcome`

// Trial expiry reminder: one per trial period
dedupeKey: `${userId}-trial-expiry-2026-04`

// Outbound prospecting: one per prospect per campaign
dedupeKey: `${prospectId}-outbound-q2-2026`

// Support follow-up: one per ticket
dedupeKey: `ticket-${ticketId}-follow-up`

Avoid using timestamps or random values - these defeat the purpose and allow duplicates on retry.

Handling policy blocks

Your code needs to handle the case where a send is blocked by policy. This replaces the silent-drop or exception behavior you might have in existing integrations.

Policy block handling
const result = await response.json();

switch (result.status) {
  case "queued":
  case "sent":
    // Success - log the requestId for tracking
    logger.info("Email sent", { requestId: result.requestId });
    break;

  case "blocked":
    switch (result.blockReason) {
      case "duplicate_send":
        // Already sent - this is expected behavior, not an error
        logger.info("Duplicate send prevented", { dedupeKey: result.dedupeKey });
        break;
      case "suppressed_recipient":
        // Contact has unsubscribed - remove from your outreach list
        logger.warn("Recipient suppressed", { email: recipientEmail });
        await markContactSuppressed(recipientEmail);
        break;
      case "rate_limit_exceeded":
        // Too many sends - back off and retry later
        logger.warn("Rate limit hit", { requestId: result.requestId });
        await scheduleRetry(payload, delayMs(60_000));
        break;
      case "risk_budget_exceeded":
        // Agent's send budget exhausted for this period
        logger.warn("Risk budget exceeded");
        break;
      default:
        logger.error("Send blocked", { reason: result.blockReason });
    }
    break;
}

Keep your existing provider as a delivery backend

You don't need to change your Resend, Postmark, or SES account. Molted can route outbound sends through your existing provider. Configure it in the portal under Settings > Providers.

This means:

  • No DNS changes or domain re-verification needed
  • Your existing sending reputation carries over
  • Molted adds a failover provider automatically if your primary goes down

Checking delivery status

Check delivery status
const statusResp = await fetch(
  `https://api.molted.email/v1/agent/send/${result.requestId}/status`,
  {
    headers: { Authorization: `Bearer ${process.env.MOLTED_API_KEY}` },
  }
);
const status = await statusResp.json();
// { status: "delivered", provider: "resend", lastEvent: "delivered" }

Reading inbound replies

Once your agent has a mailbox, replies from recipients come back to that address. Use the CLI to stream them:

molted listen --mailbox agent@yourdomain.com --events "inbound.*"

Or subscribe to webhook events - see Webhooks and Reactive Agent Guide.

Migration checklist

  • Create Molted account and add API key to environment
  • Create a mailbox for your agent, note mailboxId and tenantId
  • Replace send calls with Molted Agent Runtime API
  • Add dedupeKey to every send call
  • Add agentId identifying which agent is sending
  • Add policy block handling (blocked status with blockReason)
  • Test with a send to your own address
  • Configure your existing provider as a delivery backend in the portal (optional)
  • Set up inbound reply handling if your agent needs to read replies