Migrate from Mailgun
The provider-specific half of the migration guide: how Mailgun's POST /v3/{domain}/messages parameters, suppression lists, and webhook events 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
Mailgun's form-encoded parameter prefixes (o: options, v: variables, h: headers) all become first-class JSON fields on POST /v1/email/messages:
| What it does | Mailgun | Bird |
|---|---|---|
| Sender | from | from |
| Recipients | to / cc / bcc | to / cc / bcc (arrays) |
| Subject | subject | subject |
| Body | html / text | html / text (at least one) |
| Reply-to | h:Reply-To | reply_to (array) |
| Custom headers | h:X-* | headers (string → string object) |
| Filterable labels | o:tag | tags: {name, value} pairs |
| Round-trip context | v:* / X-Mailgun-Variables | metadata: arbitrary JSON |
| Stored template | template + t:variables | template + template.parameters |
| Scheduling | o:deliverytime | scheduled_at |
| Open/click tracking | o:tracking-opens / o:tracking-clicks | 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:
- The request becomes JSON. Mailgun accepts multipart form data; Bird takes a JSON body with Content-Type: application/json. This is usually the biggest mechanical change in the port.
- v: variables were echoed in events; Bird metadata works the same way. Bird echoes your metadata (and tags) on every webhook event alongside email_id/recipient_id, so your handlers get them back without an extra lookup.
- Recipient variables don't port one-to-one. Mailgun's recipient-variables personalize many recipients in one call; on Bird that job belongs to the batch endpoint, one entry per recipient, each entry carrying its own content or its own parameters values for {{ token }} substitution.
- Stored templates port, with one caveat. Mailgun's template parameter maps to Bird's template field with values in template.parameters; see sending with a template. Template authoring on Bird is in preview and not yet open to every workspace, so until yours has it, render content in your application and send html/text.
- Attachments port directly. Mailgun multipart attachment / inline files become Bird's attachments array with base64 content (set content_id for inline images). See attachments.
Export suppressions
Mailgun keeps three per-domain lists; export each and run them through the import loop:
- GET /v3/{domain}/bounces
- GET /v3/{domain}/complaints
- GET /v3/{domain}/unsubscribes
Repeat per sending domain. Mailgun's lists are domain-scoped, while Bird suppressions are workspace-scoped, so the union of your domains' lists is what you import.
Translate webhook events
Mailgun signals temporary vs permanent failure with one failed event plus a severity field; Bird splits them:
| Outcome | Mailgun | Bird |
|---|---|---|
| Accepted/processed | accepted | email.accepted → email.processed |
| Delivered | delivered | email.delivered |
| Temporary failure | failed (temporary) | email.deferred |
| Permanent bounce | failed (permanent) | email.bounced / email.out_of_band_bounce |
| Spam complaint | complained | email.complained |
| Blocked/suppressed | (none) | email.rejected |
| Open | opened | email.opened |
| Click | clicked | email.clicked |
| Unsubscribe | unsubscribed | email.list_unsubscribed |
email.rejected has no Mailgun counterpart: Bird reports suppressed recipients visibly (status rejected, rejection_reason: recipient_suppressed) instead of silently skipping them. Add a handler for it rather than treating it as a bounce.
Verification also changes: Mailgun signs with an HMAC over timestamp + token inside the payload's signature object, while Bird signs per the Standard Webhooks specification, with headers rather than payload fields. Swap your verification code for the recipe in Webhooks & events.
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