Migrate from SendGrid
The provider-specific half of the migration guide: how SendGrid's v3 Mail Send payload, suppression lists, and Event Webhook map onto Bird. Do the steps in the main guide in order — this page is the lookup table for steps 1, 3, and 4.
Map the send call
SendGrid's POST /v3/mail/send wraps recipients in a personalizations array; Bird's POST /v1/email/messages is a flat payload, so each personalization becomes its own send (or one batch entry).
| What it does | SendGrid | Bird |
|---|---|---|
| Sender | from.email | from |
| Recipients | personalizations[].to / cc / bcc | to / cc / bcc (arrays, max 50 each) |
| Subject | subject | subject |
| Body | content[] (type + value) | html / text (at least one) |
| Reply-to | reply_to / reply_to_list | reply_to (array, 1–25) |
| Custom headers | headers | headers (string → string object) |
| Filterable labels | categories | tags — {name, value} pairs, max 20 |
| Round-trip context | custom_args | metadata — arbitrary JSON, max 2 KB |
| Open/click tracking | tracking_settings | track_opens / track_clicks (default true) |
| IP pool | ip_pool_name | ip_pool (ipp_... or ipp_shared) |
| Category | — | category: transactional (default) or marketing |
Porting notes:
- categories are bare strings; Bird tags are pairs. A category like "welcome" becomes {"name": "category", "value": "welcome"} — pick a stable name so your dashboards filter the way your SendGrid stats did.
- custom_args were echoed in every event; Bird metadata is not. Webhook payloads carry email_id/recipient_id instead — fetch the message by email_id when an event handler needs your context back.
- Dynamic templates (template_id) have no Bird equivalent yet — template is reserved and returns 422 unsupported_feature. Render content in your application and send html/text until templates ship. The same applies to attachments and send_at → scheduled_at.
- Unsubscribe groups (asm) don't port as a concept: Bird handles list-unsubscribe at the category level — marketing mail gets suppression-aware unsubscribe handling automatically.
Export suppressions
SendGrid splits suppressions across endpoints; export each and run them through the import loop:
- GET /v3/suppression/bounces
- GET /v3/suppression/spam_reports
- GET /v3/suppression/unsubscribes (global unsubscribes)
- GET /v3/asm/groups/{group_id}/suppressions for each unsubscribe group you want to carry over
Translate webhook events
| Outcome | SendGrid Event Webhook | Bird |
|---|---|---|
| Accepted/processed | processed | email.accepted → email.processed |
| Delivered | delivered | email.delivered |
| Temporary failure | deferred | email.deferred |
| Permanent bounce | bounce | email.bounced / email.out_of_band_bounce |
| Spam complaint | spamreport | email.complained |
| Blocked/suppressed | dropped | email.rejected |
| Open | open | email.opened |
| Click | click | email.clicked |
| Unsubscribe | unsubscribe / group_unsubscribe | email.unsubscribed / email.list_unsubscribed |
The dropped ↔ email.rejected equivalence is the one to test: like SendGrid, Bird reports suppressed recipients visibly (status rejected, rejection_reason: recipient_suppressed) rather than silently dropping them, so your audit logic ports cleanly.
Verification changes more than the event names: SendGrid's Event Webhook signs with an ECDSA public key, while Bird signs per the Standard Webhooks HMAC scheme — swap your verification code for the recipe in Webhooks & events. SendGrid also batches events into JSON arrays; Bird delivers one event per request.
Cut over
Work through domains & DNS and the sandbox smoke test in the main guide — both are provider-independent.