Sign inGet started

Migrate from SendGrid

This page maps SendGrid's v3 Mail Send payload, suppression lists, and Event Webhook to Bird. Follow the main migration guide in order, and use these mappings for steps 1, 3, and 4.

Hand this to your agent

Paste this into Claude Code, Cursor, or Codex. The agent works through this page against your own repository, using whichever Bird surface it already has: the MCP server if one is connected, the CLI if it is installed and signed in.
Exemple de code
I am moving an email integration from SendGrid to Bird. Route through it with me.
1. Check what you already have before setting anything up. If Bird's MCP server is connected, use its tools. If the Bird CLI is installed and signed in, use that. Either one is enough, and every step below is an action you take with whichever you have. Only if neither is present, follow https://bird.com/docs/ai/set-up-your-agent.md to set one up and sign me in. Every Bird docs page serves Markdown at its own URL with `.md` appended, so fetch that rather than the HTML.
2. Read https://bird.com/docs/guides/email/migrate/sendgrid.md for the payload, suppression and event mapping, and https://bird.com/docs/guides/email/migrate.md for the order the steps go in.
3. Find and list my SendGrid usage in this repository before you change anything: the /v3/mail/send call sites and any SDK wrappers around them, the Event Webhook handler and the URL it is registered at, and every domain I send from.
4. Register each of those sending domains with Bird and give me the DNS records to publish, following https://bird.com/docs/guides/email/sending-domains.md. Leave every DNS record my current provider uses exactly as it is: Bird's records are published alongside them and both providers authenticate side by side until I switch traffic. Publishing DNS affects mail for the whole domain, so show me the records and let me publish them.
5. Export my suppressions from SendGrid and import them into Bird before any production traffic goes through Bird, so my first sends do not reach addresses that already bounced or complained. SendGrid splits these across GET /v3/suppression/bounces, GET /v3/suppression/spam_reports, GET /v3/suppression/unsubscribes, and GET /v3/asm/groups/{group_id}/suppressions for each unsubscribe group worth carrying over. The Bird import takes one address per request and is idempotent, so a partial re-run is safe. https://bird.com/docs/guides/email/suppressions.md has the reason taxonomy.
6. Port the send call and the webhook handler using the mapping tables on the provider page. Bird signs deliveries per Standard Webhooks rather than SendGrid's scheme, so treat verification as a rewrite rather than a URL change: https://bird.com/docs/guides/webhooks.md and https://bird.com/docs/guides/email/events.md.
7. Run my whole integration against Bird's mail sandbox before any production traffic, following https://bird.com/docs/guides/email/testing-sandbox.md. Sandbox sends run the real pipeline without reaching an inbox or touching my sending reputation.
8. Stop and ask me wherever a step needs a decision. Do not point production traffic at Bird until I have seen the sandbox results and replied with the words cut over to Bird. Retiring the SendGrid path is a separate step that comes later: ask me again and wait for me to reply with the words retire the SendGrid path. A reply that agrees without naming what it is authorising is not authorisation. Finish by telling me what is left that only a person can do.

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 doesSendGridBird
Senderfrom.emailfrom
Recipientspersonalizations[].to / cc / bccto / cc / bcc (arrays)
Subjectsubjectsubject
Bodycontent[] (type + value)html / text (at least one)
Reply-toreply_to / reply_to_listreply_to (array)
Custom headersheadersheaders (string → string object)
Filterable labelscategoriestags: {name, value} pairs
Round-trip contextcustom_argsmetadata: arbitrary JSON
Stored templatetemplate_id + dynamic_template_datatemplate + template.parameters
Schedulingsend_atscheduled_at
Open/click trackingtracking_settingstrack_opens / track_clicks (default true)
IP poolip_pool_nameip_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

OutcomeSendGrid Event WebhookBird
Accepted/processedprocessedemail.acceptedemail.processed
Delivereddeliveredemail.delivered
Temporary failuredeferredemail.deferred
Permanent bouncebounceemail.bounced / email.out_of_band_bounce
Spam complaintspamreportemail.complained
Blocked/suppresseddroppedemail.rejected
Openopenemail.opened
Clickclickemail.clicked
Unsubscribeunsubscribe / group_unsubscribeemail.unsubscribed / email.list_unsubscribed
The droppedemail.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