Migrate from Mandrill
The provider-specific half of the migration guide: how Mandrill's (Mailchimp Transactional) messages/send payload, rejection blacklist, and webhooks map onto Bird. Do the steps in the main guide in order — this page is the lookup table for steps 1, 3, and 4.
Two shape changes dominate the port. Mandrill nests everything under a message object and authenticates with a key in the request body; Bird takes a flat top-level payload and a standard Authorization: Bearer header. And Mandrill's recipient type (to/cc/bcc as a field on each address) becomes Bird's separate to/cc/bcc arrays.
Map the send call
| What it does | Mandrill (messages/send) | Bird |
|---|---|---|
| Auth | key in request body | Authorization: Bearer bk_... header |
| Sender | message.from_email / from_name | from — string or { "email", "name" } |
| Recipients | message.to — [{ email, name, type }] | to / cc / bcc — split by the type field |
| Subject | message.subject | subject |
| Body | message.html / message.text | html / text (at least one) |
| Reply-to | message.headers["Reply-To"] | reply_to — array, 1–25 |
| Custom headers | message.headers | headers — string → string object |
| Filterable labels | message.tags — bare strings | tags — { name, value } pairs, max 20 |
| Round-trip context | message.metadata | metadata — arbitrary JSON, max 2 KB |
| Open/click tracking | message.track_opens / track_clicks | track_opens / track_clicks (default true) |
| Attachments | message.attachments — { type, name, content } | attachments — { content_type, filename, content } |
| Inline images | message.images — { type, name, content } | attachments with content_id |
| Category | subaccount / tags convention | category: transactional (default) or marketing |
Porting notes:
- Flatten and re-auth. Drop the message wrapper — its fields move to the top level — and move the API key out of the body into the Authorization header. The key field has no Bird equivalent.
- Split recipients by type. Mandrill marks each recipient to, cc, or bcc on the address object; Bird uses three separate arrays. Bucket the to list by its type field as you port.
- Tags become name/value pairs. Mandrill tags are bare strings ("welcome"); Bird tags are { name, value } pairs. Pick a stable name — { "name": "category", "value": "welcome" } — so your filters and analytics group the way your Mandrill stats did. metadata ports across directly as JSON.
- merge_vars / templates render in your app. Mandrill's handlebars-style merge tags and stored templates (messages/send-template) have no Bird equivalent yet — template_id is reserved. Render the final HTML in your application and send it as html.
- send_at is reserved. Mandrill's scheduled-send send_at maps to Bird's scheduled_at, which is in preview and returns 422 unsupported_feature today — plan around the gap before cutover.
- Attachments port directly — Mandrill's base64 content is Bird's content, and inline images (referenced as cid: in the HTML) become attachments entries with content_id. See attachments.
Export suppressions
Mandrill keeps unwanted addresses on its rejection blacklist — pull it with the rejects/list API (or export from the Rejection Blacklist view). Each entry carries a reason (hard-bounce, soft-bounce, spam, unsub, custom); skip the soft-bounce rows (transient, not a true suppression) and run the rest through the import loop. Bird sorts them into its own reasons as they're imported.
Translate webhook events
Mandrill posts batched event arrays; map the event value to Bird's event vocabulary:
| Outcome | Mandrill | Bird |
|---|---|---|
| Sent / accepted | send | email.accepted → email.processed |
| Delivered | — | email.delivered |
| Temporary failure | deferral | email.deferred |
| Permanent bounce | hard_bounce | email.bounced / email.out_of_band_bounce |
| Soft bounce | soft_bounce | email.deferred (then email.bounced if it gives up) |
| Spam complaint | spam | email.complained |
| Unsubscribe | unsub | email.unsubscribed / email.list_unsubscribed |
| Rejected/blocked | reject | email.rejected |
| Open | open | email.opened |
| Click | click | email.clicked |
Two differences to code for:
- Bird reports delivery explicitly. Mandrill's send event means the message was injected; Bird splits acceptance (email.accepted/email.processed) from the recipient mail server taking it (email.delivered).
- Events are recipient-scoped and signed differently. Bird's delivery events carry recipient_id alongside email_id (one stream per recipient), and Bird signs per the Standard Webhooks spec rather than Mandrill's X-Mandrill-Signature HMAC. See Webhooks & events for verification.
Cut over
Work through domains & DNS and the sandbox smoke test in the main guide — both are provider-independent.