Migrate/Resend
Package + key swap

Switch from Resend to Anypost

If you're coming from Resend, Anypost will feel familiar: both are JSON REST APIs with Bearer auth and a similar set of core send fields. For most apps, migrating means changing your package name, swapping your API key, and adjusting a field or two.

~15 mintypical migration time
2things to adjust (tags, webhooks)
8official SDK languages

What's the same, what changed

Both platforms send authenticated email over a JSON REST API and report the same delivery lifecycle. The core request looks similar; the differences are a handful of details, listed below.

ConceptResendAnypost
Base URLapi.resend.comapi.anypost.com/v1
Auth headerAuthorization: Bearer re_…Authorization: Bearer ap_…
Key prefixre_ap_
Send endpointPOST /emailsPOST /v1/email
Batch endpointPOST /emails/batch, a JSON arrayPOST /v1/email/batch, { emails: [...] }, up to 100
Response ididid
from, to, cc, bccSame namesSame names
reply_toSame nameSame name
html / textSame namesSame names
AttachmentsBase64 in JSONBase64 in JSON
IdempotencyIdempotency-Key headerIdempotency-Key header
Tags[{name, value}] objects["string"] (array of strings)
Templatesreact-email / HTML stringMarkdown or HTML, stored server-side
Webhooksemail.* events, one per requestemail.* events, batched array
SMTP hostsmtp.resend.comsmtp.anypost.com
SMTP port465 (implicit TLS)587 (STARTTLS)

The one field to watch: Resend's tags take an array of { name, value } objects. Anypost's tags take plain strings, like ["welcome", "onboarding"]: up to 10, each up to 64 letters, numbers, underscores, or hyphens. Flatten each tag object into one string, such as flow_welcome (a colon is rejected), and the rest of the body carries over. See Tags, topics & campaigns.

Six steps to switch

You can run Resend and Anypost in parallel during cutover; the two domains are independent. Migrate one email type at a time and verify delivery before cutting over completely.

Create an Anypost account and verify your domain

Sign up at anypost.com and add the same sending domain you use with Resend. You'll publish three CNAME records (one for verification, two for DKIM), then click verify; Anypost hosts the SPF and DKIM records they point to, and on Cloudflare one click creates them for you. Propagation takes a few minutes to a few hours. Your Resend records don't conflict with Anypost's, so both can be live at once. During your evaluation, your account sends to your own verified domains, so you can build and test everything first. See Domains.

Create an API key

In the dashboard, create a key (it starts with ap_). Use a send-only key for your application server, just as you would for a Resend key. Set it as ANYPOST_API_KEY alongside your existing RESEND_API_KEY during cutover. See Authentication.

Swap the package and key

Install the Anypost SDK, replace your Resend import, and point the key env var at your new ap_ key. The send call keeps a similar shape.

Before (Resend)
import { Resend } from "resend";
 
const resend = new Resend(process.env.RESEND_API_KEY);
 
await resend.emails.send({
  from: "[email protected]",
  to: ["[email protected]"],
  subject: "Hello",
  html: "<p>Hi there.</p>",
});
After (Anypost)
import { Anypost } from "anypost";
 
const anypost = new Anypost(process.env.ANYPOST_API_KEY);
 
await anypost.email.send({
  from: "[email protected]",
  to: ["[email protected]"],
  subject: "Hello",
  html: "<p>Hi there.</p>",
});

Update your webhook endpoint (if you use one)

Register your existing webhook URL in the Anypost dashboard. Most email.* event names carry over, so your switch statement needs only minor changes: rename the two the table below lists, and wrap it in a loop because Anypost batches events into an array. See the Webhooks section below.

Bring your suppression list

Export the addresses Resend has stopped sending to, and your unsubscribed contacts, and add each address with POST /v1/suppressions, so your first Anypost sends skip addresses that have already hard-bounced, complained, or opted out. Give each unsubscribe the topic it opted out of, such as marketing, so receipts and password resets still reach that person. See Suppressions.

for (const { email, topic } of exported) {
  await anypost.suppressions.create({ email, topic });
}

Move your volume over with us

Our deliverability experts plan your warm-up from what you told us about your sending. Move one email type at a time, starting with the mail your recipients expect most, while we raise your volume at a pace mailbox providers accept, so they see a steady sender, not a sudden new one. Questions about timing? Talk to us.

The same call in every SDK

The change is consistent across languages: swap the package, construct the client with your ap_ key, and call email.send. Pick your language.

Resend
curl https://api.resend.com/emails \
  -H "Authorization: Bearer re_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Welcome",
    "html": "<p>Hi.</p>"
  }'
Anypost
curl https://api.anypost.com/v1/email \
  -H "Authorization: Bearer ap_…" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "[email protected]",
    "to": ["[email protected]"],
    "subject": "Welcome",
    "html": "<p>Hi.</p>"
  }'

Webhooks

Most of the email.* event names you handle today carry over unchanged; the table marks the two that don't. Beyond those, Anypost delivers events in a batched events array, and the recipient address is data.recipient rather than data.to.

Resend eventAnypost eventNotes
email.sentemail.sentsameAccepted for delivery
email.deliveredemail.deliveredsameAccepted by the recipient's server
email.delivery_delayedemail.delayedrenameTemporary failure; will be retried
email.bouncedemail.bouncedsameCheck bounce.type: permanent, transient, or expired
email.complainedemail.complainedsameFeedback-loop complaint
email.openedemail.openedsameTracked open
email.clickedemail.clickedsameTracked click
email.failed(none)no equiv.A send Anypost can't accept is an API error at send time

Anypost also sends email.suppressed when a send skips an address on your suppression list, and email.unsubscribed when a recipient uses a one-click unsubscribe link.

Before (Resend)
// Resend posts one event per request
app.post("/webhook/email", (req, res) => {
  const { type, data } = req.body;
  switch (type) {
    case "email.delivered":  markDelivered(data.email_id); break;
    case "email.bounced":    suppressAddress(data.to); break;
    case "email.complained": unsubscribe(data.to); break;
    case "email.opened":
    case "email.clicked":    recordEngagement(data); break;
  }
  res.sendStatus(200);
});
After (Anypost)
// Anypost batches events; the recipient is data.recipient
app.post("/webhook/email", (req, res) => {
  for (const { type, data } of req.body.events) {
    switch (type) {
      case "email.delivered":  markDelivered(data.email_id); break;
      case "email.bounced":
        if (data.bounce.type === "permanent") suppressAddress(data.recipient);
        break;
      case "email.complained": unsubscribe(data.recipient); break;
      case "email.opened":
      case "email.clicked":    recordEngagement(data); break;
    }
  }
  res.sendStatus(200);
});

Suppress on permanent bounces only. email.bounced also reports temporary failures and messages that expired after 72 hours of retries. Anypost already suppresses permanent bounces for you, so if you mirror the list, check data.bounce.type first. See Webhooks.

Signature verification differs. Resend signs webhooks with Svix headers; Anypost signs with an Anypost-Signature header (t=<unix>,v1=<hmac>) over the timestamp and raw body. Swap your verification step for Anypost's before trusting a delivery. See Webhooks.

Register your webhook URL once in the Anypost dashboard and select which events to receive. One URL covers every event for your account.

Using SMTP instead of the API?

Point your SMTP client at Anypost's host with the username anypost and your ap_ key as the password. The one real change is the port: Resend uses implicit TLS on 465; Anypost uses STARTTLS on 587.

Resend SMTP
SMTP_HOST=smtp.resend.com
SMTP_PORT=465
SMTP_USER=resend
SMTP_PASS=re_…
SMTP_TLS=true
Anypost SMTP
SMTP_HOST=smtp.anypost.com
SMTP_PORT=587
SMTP_USER=anypost
SMTP_PASS=ap_…
SMTP_STARTTLS=true

Anypost supports ports 587 (recommended), 2587, and 25. STARTTLS is required on every port, and there's no implicit-TLS port, so move off Resend's 465 to 587. See Sending over SMTP.

Ready to switch?

Start with a free evaluation and build against the real API, then bring your volume over while our deliverability experts warm up your sending.