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, andagentId - 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
- Create a Molted account at molted.email/signup
- Create a mailbox for your agent (portal under Mailboxes, or via API)
- Note your
tenantId,mailboxId, and API key - you'll need them in every request
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)
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)
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
| Resend | Molted |
|---|---|
from field sets the sender address | Sender address is set on the mailbox - no from in the request |
| No deduplication | dedupeKey prevents duplicate sends across runs |
| No policy engine | 20+ rules evaluated before delivery |
| No inbound replies | Replies land in the mailbox inbox |
Migrating from 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)
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
| Postmark | Molted |
|---|---|
From / To in request body | Sender is set on the mailbox; only recipient in request |
| Postmark message streams for transactional vs broadcast | Autonomy levels control human-approval requirements per mailbox |
| Postmark suppressions managed per-stream | Molted suppressions managed per-mailbox, auto-enforced on every send |
| Postmark webhooks for delivery events | Molted events via webhook or molted listen CLI stream |
Migrating from Amazon SES
Before (SES with AWS SDK)
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)
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
| SES | Molted |
|---|---|
| IAM credentials + region config | Single API key |
| Raw SMTP or SES API - no policy layer | 20+ policy rules enforced before every send |
| No built-in deduplication | dedupeKey deduplication window per mailbox |
| Bounce handling via SNS/SQS pipeline | Bounces automatically added to suppression list |
| Separate suppression list management | Suppressions 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.
// 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.
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
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
mailboxIdandtenantId - Replace send calls with Molted Agent Runtime API
- Add
dedupeKeyto every send call - Add
agentIdidentifying which agent is sending - Add policy block handling (
blockedstatus withblockReason) - 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
Related
- Quickstart - full walkthrough from signup to first send
- Sending Email - Agent Runtime API reference
- Policy Simulation - test policy rules before going live
- Domains - domain verification and DNS setup
- Webhooks - delivery events and inbound notifications
- Reactive Agent Guide - build agents that respond to replies