Migrate from Mandrill
This page maps Mandrill's (Mailchimp Transactional) messages/send payload, rejection blacklist, and webhooks to Bird. Follow the main migration guide in order, and use these mappings 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.
Hand this to your agent
Paste this into Claude Code, Cursor, or Codex. The agent works through this page against your own repository, using whichever Bird surface it already has: the MCP server if one is connected, the CLI if it is installed and signed in.
Exemple de code
I am moving an email integration from Mandrill to Bird. Route through it with me.
1. Check what you already have before setting anything up. If Bird's MCP server is connected, use its tools. If the Bird CLI is installed and signed in, use that. Either one is enough, and every step below is an action you take with whichever you have. Only if neither is present, follow https://bird.com/docs/ai/set-up-your-agent.md to set one up and sign me in. Every Bird docs page serves Markdown at its own URL with `.md` appended, so fetch that rather than the HTML.
2. Read https://bird.com/docs/guides/email/migrate/mandrill.md for the payload, suppression and event mapping, and https://bird.com/docs/guides/email/migrate.md for the order the steps go in.
3. Find and list my Mandrill usage in this repository before you change anything: the messages/send and messages/send-template call sites and any SDK wrappers around them, the webhook handler and the URL it is registered at, and every domain I send from. Mandrill authenticates with an API key passed in the request body rather than a header, so tell me every place that key appears in my code: the port changes how I authenticate, not just what I send, and that key is a secret currently sitting in a payload.
4. Register each of those sending domains with Bird and give me the DNS records to publish, following https://bird.com/docs/guides/email/sending-domains.md. Leave every DNS record my current provider uses exactly as it is: Bird's records are published alongside them and both providers authenticate side by side until I switch traffic. Publishing DNS affects mail for the whole domain, so show me the records and let me publish them.
5. Export my rejects from Mandrill and import them into Bird before any production traffic goes through Bird, so my first sends do not reach addresses that already bounced or complained. Read the list through rejects/list, and skip the soft-bounce rows: those are transient failures rather than suppressions, and importing them would suppress addresses that are fine. Show me how many rows you skipped. The Bird import takes one address per request and is idempotent, so a partial re-run is safe. https://bird.com/docs/guides/email/suppressions.md has the reason taxonomy.
6. Port the send call and the webhook handler using the mapping tables on the provider page. Move the API key out of the request body and into the Authorization header as a bearer token while you are there. Bird signs deliveries per Standard Webhooks rather than Mandrill's scheme, so treat verification as a rewrite rather than a URL change: https://bird.com/docs/guides/webhooks.md and https://bird.com/docs/guides/email/events.md.
7. Run my whole integration against Bird's mail sandbox before any production traffic, following https://bird.com/docs/guides/email/testing-sandbox.md. Sandbox sends run the real pipeline without reaching an inbox or touching my sending reputation.
8. Stop and ask me wherever a step needs a decision. Do not point production traffic at Bird until I have seen the sandbox results and replied with the words cut over to Bird. Retiring the Mandrill path is a separate step that comes later: ask me again and wait for me to reply with the words retire the Mandrill path. A reply that agrees without naming what it is authorising is not authorisation. Finish by telling me what is left that only a person can do.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 because they describe transient failures rather than true suppressions. 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, with one stream per recipient. We deliver one event per request and sign it according to the Standard Webhooks specification. Mandrill instead signs batched arrays with an 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.
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