Migrate from Mandrill
The provider-specific half of the migration guide: how Mandrill's (Mailchimp Transactional) messages/send payload, rejection blacklist, and webhooks map here. 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. We take 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 our 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 |
| Custom headers | message.headers | headers: string → string object |
| Filterable labels | message.tags: bare strings | tags: { name, value } pairs |
| Round-trip context | message.metadata | metadata: arbitrary JSON |
| Stored template | messages/send-template + merge_vars | template + template.parameters |
| Scheduling | send_at | scheduled_at |
| 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: marketing (default) or transactional |
Our field caps and defaults (recipient counts, tag and metadata limits) live in Sending email.
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 equivalent here.
- Split recipients by type. Mandrill marks each recipient to, cc, or bcc on the address object. We use 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"). Our tags are { name, value } pairs. Pick a stable name, for example { "name": "category", "value": "welcome" }, so your filters and analytics group the way your Mandrill stats did. metadata ports across directly as JSON.
- Stored templates port, with one caveat. Mandrill's messages/send-template becomes our template field (referenced by ID or slug) with values in template.parameters. See sending with a template. Our template variables are per message rather than per recipient, so per-recipient merge_vars become one batch entry per recipient, each with its own parameters.
- Attachments port directly. Mandrill's base64 content is our 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 has 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.
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 | (none) | 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:
- We report delivery explicitly. Mandrill's send event means the message was injected. We split acceptance (email.accepted/email.processed) from the recipient mail server taking it (email.delivered).
- Events are recipient-scoped and signed differently. Our delivery events have recipient_id alongside email_id (one stream per recipient), and we deliver one event per request, signed per the Standard Webhooks spec rather than Mandrill's X-Mandrill-Signature HMAC over batched arrays. See Webhooks & events for verification.
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