New: campaigns, automations and product emails in the same account. Meet Postal
Sign in Create account

Migrate from Resend to Postal

Postal's API uses the same resources, field names and response formats as Resend's API. In most cases, migrating means changing the base URL and the key, and the Resend SDK keeps working.

Changes in code
URL and key
You can run both together
Yes
Estimated time, plus DNS
A few hours
Type of sending
API and SMTP
1

Verify the domain on Postal

Add the domain in the dashboard and create the DNS records it shows. You can keep the same domain on Resend and Postal at the same time: the records are different, because Postal uses the postal selector in DKIM.

  • Postal checks the records on its own for up to 72 hours and notifies you by webhook (domain.updated)
  • If the domain doesn't have DMARC yet, the large providers penalize senders of volume: start with p=none and tighten it later
  • Create an API key under API keys. For systems that only send, use a sending-only key, restricted to the domain
2

Point the SDK at Postal

Resend's official SDKs work when pointed at Postal. In Node.js, pass baseUrl when creating the client or set RESEND_BASE_URL. The email body, the response ({"id"}) and the error format (statusCode, name, message) stay the same.

Through the SDK

Node.js
- const resend = new Resend(process.env.RESEND_API_KEY);+ const resend = new Resend(process.env.POSTAL_API_KEY, { baseUrl: 'https://api.postal.so' });

Or just through the environment

.env
# No changes to code that already uses new Resend(key)
RESEND_BASE_URL=https://api.postal.so
RESEND_API_KEY=ps_your_key

Over HTTP

cURL
- curl -X POST 'https://api.resend.com/emails' -H 'Authorization: Bearer re_...'+ curl -X POST 'https://api.postal.so/emails' -H 'Authorization: Bearer ps_...'

Over SMTP

SMTP
- Server: smtp.resend.com- User: resend- Password: re_...+ Server: smtp.postal.so+ User: postal (resend is also accepted)+ Password: ps_...

Tested with the resend 6.34 package (Node.js): simple, scheduled, batch and idempotent sending, plus lookup, editing and cancellation, attachments, domains, API keys, webhooks and webhooks.verify. Other SDKs that let you change the base URL speak the same format, but they were not tested one by one: if you find a difference, let us know through the contact form.

3

Import what you already have

Before switching traffic, bring over the suppression list, so you don't write to people who already asked to opt out, and the contacts, if you use the audience. Recreate the templates too: the {{{VAR}}} variables are the same as Resend's.

  • Suppression list: POST /suppressions/batch/add, with 100 addresses per request
  • Contacts: CSV import, through the dashboard or the API
  • Templates: create and publish with the same alias and the same variables; sends always use the latest published version
POST /suppressions/batch/add
curl -X POST https://api.postal.so/suppressions/batch/add \
  -H "Authorization: Bearer $POSTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"emails": ["[email protected]", "[email protected]"]}'
4

Recreate the webhook and check the signature

Event names are the same (email.delivered, email.bounced…), in the format {type, created_at, data}. What changes is the secret: in Postal, each endpoint has its own signing_secret (whsec_…). The signature follows the Standard Webhooks standard, and resend.webhooks.verify accepts both the webhook-* and svix-* headers.

  • Create the endpoint under Webhooks (dashboard or POST /webhooks) and choose the events
  • Send a test event from the dashboard before switching traffic
  • There are 8 attempts over about 27 hours, always with the same webhook-id; manual resending goes out through the dashboard or POST /webhooks/{id}/events/{event_id}/replay
  • Other languages: see the signature verification
Node.js, Resend SDK
import { Resend } from 'resend';

const resend = new Resend(process.env.POSTAL_API_KEY, { baseUrl: 'https://api.postal.so' });

// Read the raw request body, without reformatting the JSON
const event = resend.webhooks.verify({
  payload: rawBody,
  headers: {
    id: request.headers['webhook-id'],
    timestamp: request.headers['webhook-timestamp'],
    signature: request.headers['webhook-signature'],
  },
  webhookSecret: process.env.POSTAL_WEBHOOK_SECRET, // whsec_...
});
5

Test and switch, with both close at hand

Change the base URL and the key in the test environment, send an email to yourself and follow it in Logs and in the webhooks. Test addresses accept +rótulo, don't use quota, don't affect reputation and generate real webhooks (depending on the plan). Then switch in production and keep Resend around for a few days.

  • Different IDs. Resend's email and webhook IDs don't exist in Postal, and the reverse is also true. Record which provider each email went out through and keep receiving events from Resend for what went out through it
  • Mark the origin. Use tags (for example, provider: postal) and your own Idempotency-Key on each send
  • One provider at a time per flow. Invitations, password resets and codes should go out through a single path. Watch the bounces in the first few days, because the reputation limit applies to the whole team
  • An address that works again. A permanent bounce puts the address on the suppression list; remove it with DELETE /suppressions/{id} or POST /suppressions/batch/remove

What changes from one to the other

What stays the same and what changes
ItemWhat changes in Postal
Base URL and keyhttps://api.postal.so and Authorization: Bearer ps_…. Full-access or sending-only keys, optionally restricted to a domain.
Sending and responsePOST /emails, returns {"id"}. Same.
Errors{"statusCode", "name", "message"}. Same. See Errors.
Webhooks and signatureStandard Webhooks standard: webhook-id, webhook-timestamp and webhook-signature, and the same values in svix-*. The secret is each endpoint's signing_secret.
Event namesThe same ones, plus email.replied, contact.*, form.submitted, broadcast.sent and sending.paused / sending.resumed.
Bounce and failure reasondata.bounce.message and data.failed.reason, with the same fields.
Attachmentsfilename and content in base64, plus path (URL) and content_id for inline images. Up to 40 MB per email; your plan may allow less.
IdempotencyIdempotency-Key header (1 to 256 characters, kept for 24 hours) on POST /emails, /emails/batch and /events.
Schedulingscheduled_at in ISO 8601, in the future and at most 30 days ahead. You can reschedule and cancel.
Limits and quotasEach plan has its own requests per second and sending quotas. See Limits and Pricing.
Reputation pausePermanent bounces above the limit of the account reputation level (3% to 5%), complaints above 0.3% or rejected content above 5% pause the team's sending: 403 sending_paused. New accounts start at level 1, with a ceiling of 500 emails a day. Before the pause, owners and admins get an email warning.
Sending regionsSão Paulo (sa-east-1, the default), us-east-1, us-east-2, eu-west-1 and ap-northeast-1, per domain.
BillingIn reais, dollars or euros. The public list of plans is at GET /plans.
RetentionEmails, webhook events and logs are kept for 30 days; events sent through POST /events, for 90.

What Postal adds

  • Processing region of your choice, with São Paulo as the default. How we protect your data
  • Inboxes to receive and reply to emails, with conversations, folders, tags and drafts. See Inboxes
  • Audience and marketing: contacts, segments, topics, forms, broadcasts and automations, with the same key. See email marketing
  • Integrations with Stripe, Supabase, Clerk, Auth0 and Firebase, and the postal CLI. Integrate your app

The full reference is in Migrate from Resend, in the documentation.

Before you flip the key

  • Domain verified in Postal and API key stored in the environment
  • Suppression list and contacts imported
  • Templates recreated and published, with the same variables
  • Webhook created and signature checked with a test event
  • A test email sent and seen in Logs
  • Your code records which provider each email went out through
  • Bounces checked in the first days after the switch

Frequently asked questions

Do I need to change the code that uses the Resend SDK?

In Node.js, just pass baseUrl when creating the client or set RESEND_BASE_URL, besides changing the key. Other SDKs that let you change the base URL speak the same format, but they were not tested one by one.

Can I keep the same domain on both platforms?

Yes. The DNS records are different (Postal uses the postal selector), so both stay verified at the same time during the switch.

Does the code that verifies webhook signatures change?

resend.webhooks.verify keeps working, because Postal accepts both the webhook-* and svix-* headers. What changes is the secret: use the whsec_… of the endpoint created in Postal.

Do the IDs of my emails stay valid?

No. IDs belong to each platform. Also record which provider each email went out through, so you check the status in the right place.

Ready to change the base URL?

Create your account and start on the Free plan. To plan your quota and the order of the migration, talk to us through the contact form.