Migrate from SendGrid
The provider-specific half of the migration guide: how SendGrid's v3 Mail Send payload, suppression lists, and Event Webhook map here. 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. Our 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) |
| Subject | subject | subject |
| Body | content[] (type + value) | html / text (at least one) |
| Reply-to | reply_to / reply_to_list | reply_to (array) |
| Custom headers | headers | headers (string → string object) |
| Filterable labels | categories | tags: {name, value} pairs |
| Round-trip context | custom_args | metadata: arbitrary JSON |
| Stored template | template_id + dynamic_template_data | template + template.parameters |
| Scheduling | send_at | scheduled_at |
| Open/click tracking | tracking_settings | track_opens / track_clicks (default true) |
| IP pool | ip_pool_name | ip_pool_id (ipp_... or ipp_shared) |
| Category | (none) | category: marketing (default) or transactional |
Our field caps and defaults (recipient counts, tag and metadata limits) live in Sending email.
Porting notes:
- categories are bare strings. Our 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. Our metadata works the same way. We echo your metadata (and tags) on every webhook event alongside email_id/recipient_id, so your handlers get your context back without an extra lookup.
- Dynamic templates port to stored templates. template_id plus dynamic_template_data become template (referenced by ID or slug) plus template.parameters on the same send call. See sending with a template. send_at maps to scheduled_at directly.
- Attachments port directly. SendGrid's attachments (base64 content, type, filename, content_id for inline) map to our attachments array field-for-field.
- Unsubscribe groups (asm) don't port as a concept: we handle list-unsubscribe at the category level, so 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, we report 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 we sign per the Standard Webhooks HMAC scheme. Swap your verification code for the recipe in Webhooks & events. SendGrid also batches events into JSON arrays. We deliver one event per request.
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 we maintain it from here