Migrate from Resend
The provider-specific half of the migration guide: how Resend's POST /emails payload, suppression handling, and Svix-signed webhooks map onto Bird. Do the steps in the main guide in order; this page is the lookup table for steps 1, 3, and 4. Of the providers covered, this is the shortest port: the payloads are nearly field-for-field identical.
Map the send call
| What it does | Resend | Bird |
|---|---|---|
| Sender | from | from |
| Recipients | to / cc / bcc (max 50) | to / cc / bcc (arrays) |
| Subject | subject | subject |
| Body | html / text | html / text (at least one) |
| Reply-to | reply_to | reply_to (array) |
| Custom headers | headers | headers (string → string object) |
| Filterable labels | tags: {name, value} pairs | tags: {name, value} pairs |
| Round-trip context | (none; tags double as context) | metadata: arbitrary JSON |
| Scheduling | scheduled_at | scheduled_at |
| Open/click tracking | per-domain dashboard setting | track_opens / track_clicks (default true) |
| Category | (none) | category: marketing (default) or transactional |
Bird-side field caps and defaults (recipient counts, tag and metadata limits) live in Sending email.
Porting notes:
- Tags keep their shape, and metadata is an upgrade. Resend tags are the same {name, value} pairs Bird uses, but their value constraints pushed correlation data into tag values. On Bird, move correlation context into metadata (arbitrary JSON, echoed back on every webhook event and returned on API reads) and keep tags for filtering. See tags vs metadata.
- Tracking moves into the payload. Resend toggles open/click tracking per domain in the dashboard; Bird sets track_opens/track_clicks per message (both default true).
- scheduled_at maps directly, name and all; see scheduled sending. For react, render your React Email templates to HTML in your application (the render function from @react-email/render works unchanged) and send the result as html.
- Attachments port directly. Resend's attachments (base64 content) map to Bird's attachments array; set content_id for inline images.
- Batch sending ports directly. Resend's POST /emails/batch becomes Bird's batch endpoint, with per-entry results in both cases.
Export suppressions
Resend doesn't expose a dedicated suppression-list export. Pull the addresses whose last event is bounced or complained, from the Emails view in the dashboard or by walking your stored webhook events if you've been recording them, and run the list through the import loop. If you use Audiences for marketing mail, also carry over contacts marked unsubscribed.
Translate webhook events
| Outcome | Resend | Bird |
|---|---|---|
| Accepted/processed | email.sent | email.accepted → email.processed |
| Delivered | email.delivered | email.delivered |
| Temporary failure | email.delivery_delayed | email.deferred |
| Permanent bounce | email.bounced | email.bounced / email.out_of_band_bounce |
| Spam complaint | email.complained | email.complained |
| Blocked/suppressed | email.failed | email.rejected |
| Open | email.opened | email.opened |
| Click | email.clicked | email.clicked |
| Unsubscribe | (none) | email.list_unsubscribed |
The signing scheme barely changes: Resend delivers through Svix (svix-id, svix-timestamp, svix-signature headers), and Bird signs per the Standard Webhooks specification, which uses the same HMAC construction with webhook-* header names. If you verify Resend deliveries today, the same code verifies Bird's after renaming the three headers; or use the recipe in Webhooks & events.
One behavioral difference: Resend's events are message-scoped; Bird's delivery events are recipient-scoped (recipient_id alongside email_id), so a three-recipient send produces three delivery outcomes, not one.
Cut over
Work through domains & DNS and the sandbox smoke test in the main guide; both are provider-independent.
Next steps
- Sending domains: registration, verification lifecycle, and the DNS records you're re-pointing
- Webhooks & events: endpoint setup and Standard Webhooks verification
- Testing sandbox: smoke-test the new integration before cutover
- Suppressions: confirm your imported list and how Bird maintains it from here