Documentation
Sign inGet started

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 doesMailgunBird
Senderfromfrom
Recipientsto / cc / bccto / cc / bcc (arrays, max 50 each)
Subjectsubjectsubject
Bodyhtml / texthtml / text (at least one)
Reply-toh:Reply-Toreply_to (array, 1–25)
Custom headersh:X-*headers (string → string object)
Filterable labelso:tagtags{name, value} pairs, max 20
Round-trip contextv:* / X-Mailgun-Variablesmetadata — arbitrary JSON, max 2 KB
Open/click trackingo:tracking-opens / o:tracking-clickstrack_opens / track_clicks (default true)
Categorycategory: transactional (default) or marketing
Porting notes:
  • The request becomes JSON. Mailgun accepts multipart form data; Bird takes a JSON body with Content-Type: application/json — usually the biggest mechanical change in the port.
  • v: variables were echoed in events; Bird metadata is not. Webhook payloads carry email_id/recipient_id instead — fetch the message by email_id when a handler needs your variables back.
  • Recipient variables (recipient-variables batch sending) have no direct equivalent: Bird's batch endpoint takes fully-rendered per-recipient entries rather than a template plus substitutions.
  • o:deliverytimescheduled_at is reserved and returns 422 unsupported_feature today, as do attachments — plan around both before cutover. The full reserved list is in Sending email.

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:
OutcomeMailgunBird
Accepted/processedacceptedemail.acceptedemail.processed
Delivereddeliveredemail.delivered
Temporary failurefailed (temporary)email.deferred
Permanent bouncefailed (permanent)email.bounced / email.out_of_band_bounce
Spam complaintcomplainedemail.complained
Blocked/suppressedemail.rejected
Openopenedemail.opened
Clickclickedemail.clicked
Unsubscribeunsubscribedemail.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 — headers, not 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.