Migrate from Resend

Drop-in compatible. Change one import. Keep your code, your webhooks, your retry logic, your idempotency keys, your error-handling patterns. Most projects migrate in under 5 minutes; the rest of this page covers the edge cases.

The 5-minute migration

1. Install the SDK

terminal
npm install @mailstorm/resend
# or pip install mailstorm-resend

2. Run the codemod

terminal
npx mailstorm migrate           # dry run, prints what would change
npx mailstorm migrate --apply   # actually rewrites imports

What it does:

3. Manual step: API key

The codemod won't change your API key — that's intentional. You decide between two paths:

RESEND_API_KEY=re_xxxxxxxxxxxMAILSTORM_API_KEY=ms_live_xxxxxxxxxxx

Or keep both during cutover and use a different env var name in code:

typescript
const resend = new Resend(process.env.MAILSTORM_API_KEY ?? process.env.RESEND_API_KEY!);

Generate a Mailstorm key at /api-keys. Test mode (ms_test_*) captures without sending — use it to verify your integration before flipping to live.

4. Verify your sending domain

Mailstorm requires DNS verification before live sends. Add the domain at /domains. The DKIM, SPF, and DMARC records are familiar — paste them in your DNS host (Vercel DNS, Cloudflare, Route 53, etc.). Verification polls automatically; typically green within 5 minutes.

If your existing Resend domain has DMARC at p=quarantine or p=reject: our DKIM signs with a different selector (mailstorm), so you can keep both Resend and Mailstorm DKIM records active during cutover. Both pass DMARC, no domain reputation lost.

5. Reconfigure webhooks

Create webhooks at /webhooks pointing at the same endpoint URL you used with Resend. Same event names. Same JSON shape. Only header differs:

Resend-SignatureX-Mailstorm-Signature

Both are HMAC-SHA256 of the raw body using the secret you configured. Verification logic is byte-identical.

Compatibility matrix (v0.1)

Email API

Resend methodStatusNotes
resend.emails.send()✅ Drop-inIdentical request & response shapes
resend.emails.create()✅ Drop-inAlias of send
resend.emails.get(id)✅ Drop-inReturns object, last_event, to as array (Resend shape) plus our native fields
resend.emails.update(id)○ StubSchedule-send not yet implemented; returns clear error
resend.emails.cancel(id)○ StubSame — schedule-send is post-Phase 7
resend.batch.send()✅ Drop-inUp to 100 emails; {data: {data: [{id}]}} response
resend.batch.create()✅ Drop-inAlias

Domains

Resend methodStatus
resend.domains.create/list/get/verify/remove/update~ Use @mailstorm/sdk for now (or the dashboard)

Coverage in @mailstorm/resend is a roadmap item; for v0.1, manage domains via the dashboard or our native SDK.

Audiences / Contacts / Broadcasts / API Keys

Resend methodStatus
resend.audiences.*~ Native API at /api/contact-lists
resend.contacts.*~ Native API at /api/contacts
resend.broadcasts.*~ Native API at /api/broadcasts
resend.apiKeys.*Use the dashboard at /api-keys

Webhook events

Resend eventMailstorm eventStatus
email.sentemail.sent
email.deliveredemail.delivered
email.delivery_delayedemail.delivery_delayed~ Coming Phase 5
email.complainedemail.complained
email.bouncedemail.bounced
email.openedemail.opened
email.clickedemail.clicked
email.receivedemail.received✅ + AI fields included

Things that work different on purpose

Pricing model

Resend bills transactional and marketing as separate plans. Mailstorm bundles both into one tier — your monthly allowance covers all sends, no SKU split. See /pricing.

FROM-domain enforcement

Live sends require a verified FROM domain. Resend allows sending from onboarding@resend.dev without owning a domain. We mirror this with test mode — ms_test_* keys send from any domain — but live sends require ownership. This was added 2026-04-26 after a phishing operation exploited the gap.

Per-tier daily caps + new-account probation

A new account is capped at 100 sends/day for the first 7 days regardless of tier. After probation, the monthly cap takes over (free 5k/mo, hobby 50k/mo, pro 100k/mo). On Free there's also a 200/day hard cap; Hobby and Pro have no daily limit beyond your plan's monthly allotment. This is a brand-protection trade-off that makes our IP reputation more durable.

AI inbound fields

Mailstorm's email.received webhook payload includes structured AI fields (category, urgency, language, confidence) classified before the webhook fires. Resend's payload is metadata only and forces you to fetch the body via a second API call. The Mailstorm shape is a superset — your existing Resend handler reads only the fields it knows; the AI fields are additive.

Migration audit checklist

Before flipping production traffic, verify:

Rollback

If you need to roll back during the cutover window, the inverse migration is the same one-liner:

import { Resend } from "@mailstorm/resend"import { Resend } from "resend"

Flip your env var back to RESEND_API_KEY, swap the webhook URL back, and you're on Resend again. We design for migration to be reversible — that's the only way it's actually safe.

Get help

Stuck on something specific? Email support@mailstorm.dev with your migration question. We respond within 24 business hours (LATAM time zone). For urgent migration help during a cutover, mention "migration assist" in the subject and we prioritize.