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 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

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)
Subjectsubjectsubject
Bodyhtml / texthtml / text (at least one)
Reply-toh:Reply-Toreply_to (array)
Custom headersh:X-*headers (string → string object)
Filterable labelso:tagtags: {name, value} pairs
Round-trip contextv:* / X-Mailgun-Variablesmetadata: arbitrary JSON
Stored templatetemplate + t:variablestemplate + template.parameters
Schedulingo:deliverytimescheduled_at
Open/click trackingo:tracking-opens / o:tracking-clickstrack_opens / track_clicks (default true)
Category(none)category: marketing (default) or transactional
Our 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. We take a JSON body with Content-Type: application/json. This is usually the biggest mechanical change in the port.
  • v: variables were echoed in events. 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 them back without an extra lookup.
  • Recipient variables don't port one-to-one. Mailgun's recipient-variables personalize many recipients in one call. Here, that job belongs to the batch endpoint, one entry per recipient, each with its own content or its own parameters values for {{ token }} substitution.
  • Stored templates port directly. Mailgun's template parameter maps to our template field with values in template.parameters. See sending with a template.
  • Attachments port directly. Mailgun multipart attachment / inline files become our 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 our 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. We split 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/suppressed(none)email.rejected
Openopenedemail.opened
Clickclickedemail.clicked
Unsubscribeunsubscribedemail.list_unsubscribed
email.rejected has no Mailgun counterpart: we report 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 we sign 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 we maintain it from here