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.
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.
| Concept | Resend | Anypost |
|---|---|---|
| Base URL | api.resend.com | api.anypost.com/v1 |
| Auth header | Authorization: Bearer re_… | Authorization: Bearer ap_… |
| Key prefix | re_ | ap_ |
| Send endpoint | POST /emails | POST /v1/email |
| Batch endpoint | POST /emails/batch | POST /v1/email/batch |
| Response id | id | id |
from, to, cc, bcc | Same names | Same names |
reply_to | Same name | Same name |
html / text | Same names | Same names |
| Attachments | Base64 in JSON | Base64 in JSON |
| Idempotency | Idempotency-Key header | Idempotency-Key header |
| Tags | [{name, value}] objects | ["string"] (array of strings) |
| Templates | react-email / HTML string | Markdown or HTML, stored server-side |
| Webhooks | email.* events, one per request | email.* events, batched array |
| SMTP host | smtp.resend.com | smtp.anypost.com |
| SMTP port | 465 (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).
Flatten your tag objects into strings and the rest of the body carries over.
See Tags, topics & campaigns.
Four 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 DNS records (an SPF, a DKIM, and a tracking CNAME), then click verify. 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. 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.
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>",
});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. The
email.* event names carry over, so your switch statement needs only
minor changes; you wrap it in a loop because Anypost batches events into
an array.
See the Webhooks section below.
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.
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>"
}'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
The email.* event names you're already handling carry over, so your case
labels are unchanged. Two things change: Anypost delivers
events in a batched events array, and the recipient address is
data.recipient rather than data.to.
// 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);
});// 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": suppressAddress(data.recipient); break;
case "email.complained": unsubscribe(data.recipient); break;
case "email.opened":
case "email.clicked": recordEngagement(data); break;
}
}
res.sendStatus(200);
});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.
SMTP_HOST=smtp.resend.com
SMTP_PORT=465
SMTP_USER=resend
SMTP_PASS=re_…
SMTP_TLS=trueSMTP_HOST=smtp.anypost.com
SMTP_PORT=587
SMTP_USER=anypost
SMTP_PASS=ap_…
SMTP_TLS=trueAnypost 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?
Create your account, verify your domain, and send your first email in minutes.