# Migrate from MailerSend

This page maps MailerSend's send payload, suppression lists, and webhooks to Bird. Follow the [main migration guide](/docs/guides/email/migrate) in order, and use these mappings for steps 1, 3, and 4.

MailerSend keeps five suppression lists and one of them is temporary by design. Read [Export suppressions](#export-suppressions) before step 3: importing that one turns a 72-hour hold into a permanent block.

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

```text
I am moving an email integration from MailerSend 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/mailersend.md for the payload, suppression and webhook mapping, and https://bird.com/docs/guides/email/migrate.md for the order the steps go in.
3. Find and list my MailerSend usage in this repository before you change anything: calls to /v1/email and any SDK wrappers around them, whether I send with template_id and personalization or with html, the webhook handler and the URL it is registered at, and every domain I send from. Tell me the list before you edit anything.
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 MailerSend 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 suppressions 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. MailerSend keeps five lists under /v1/suppressions: hard bounces, spam complaints, unsubscribes and the blocklist are the four to carry over. Do NOT import the On Hold list: it is a temporary 72-hour hold that MailerSend clears by itself, and importing it would suppress those addresses permanently. 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. Both providers sign with HMAC-SHA256, so this is a re-implementation rather than new code: MailerSend sends one Signature header, Bird follows the Standard Webhooks scheme with webhook-id, webhook-timestamp and webhook-signature, and the signed string is constructed differently. Keep the constant-time comparison. 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, and do not retire the MailerSend path, until I have seen the sandbox results and told you to go ahead. Finish by telling me what is left that only a person can do.
```

## Map the send call

MailerSend's `POST /v1/email` and our [`POST /v1/email/messages`](/docs/api/reference/create-email-message) share a shape. The differences worth code are the tag cap and how per-recipient data is carried.

| What it does      | MailerSend                                                 | Bird                                                 |
| ----------------- | ---------------------------------------------------------- | ---------------------------------------------------- |
| Sender            | `from` (`{email, name}`)                                   | `from`                                               |
| Recipients        | `to` (max 50), `cc` / `bcc` (max 10 each)                  | `to` / `cc` / `bcc` (arrays)                         |
| Subject           | `subject` (max 998 characters)                             | `subject`                                            |
| Body              | `html` / `text`                                            | `html` / `text` (at least one)                       |
| Reply-to          | `reply_to` (`{email, name}`)                               | `reply_to` (array)                                   |
| Custom headers    | `headers` (`{name, value}`, higher plans)                  | `headers` (string → string object)                   |
| Filterable labels | `tags` (array of strings, max 5)                           | `tags`: `{name, value}` pairs                        |
| Stored template   | `template_id` + `personalization`                          | `template` + `template.parameters` (see below)       |
| Attachments       | `attachments` (`content`, `filename`, `disposition`, `id`) | `attachments`                                        |
| Scheduling        | `send_at` (up to 72 hours ahead)                           | `scheduled_at`                                       |
| Bulk precedence   | `precedence_bulk`                                          | (no counterpart, see below)                          |
| Category          | (none)                                                     | `category`: `marketing` (default) or `transactional` |
| Threading         | `in_reply_to`                                              | `headers`                                            |

Our field caps and defaults (recipient counts, tag and metadata limits) live in [Sending email](/docs/guides/email/sending-email).

Porting notes:

- **Addresses are objects there and strings here.** `{"email": "a@x.com", "name": "A"}` becomes `"A <a@x.com>"` or just `"a@x.com"`, for `from`, `to`, `cc`, `bcc` and `reply_to` alike.
- **`tags` are bare strings capped at five. Our tags are pairs.** A tag like `"welcome"` becomes `{"name": "category", "value": "welcome"}`. Pick a stable `name` so your dashboards filter the way your MailerSend tag stats did.
- **`personalization` is per-recipient template data.** It is an array keyed by recipient email. Our `template.parameters` applies to the send, so a message whose content genuinely differs per recipient becomes one send per recipient or a [batch](/docs/guides/email/sending-bulk) entry each. If your `personalization` array carries the same values for every recipient, it collapses into one `template.parameters` object.
- **Round-trip context has no MailerSend equivalent to copy.** If you were reconstructing context from `tags`, use [`metadata`](/docs/guides/email/sending-email) instead: we echo it on every webhook event alongside `email_id`/`recipient_id`, and it is not capped at five entries.
- **`precedence_bulk` has no counterpart, and `category` is not one.** `precedence_bulk` sets a `Precedence: bulk` header, which asks autoresponders and out-of-office agents to stay quiet. Our `category` decides which suppression records and unsubscribe preferences may block a message, and nothing else. Setting `category` in its place changes suppression behaviour and does nothing about autoresponders. If you need the header, note that `headers` rejects addressing and platform names but not this one.
- **Custom headers and `list_unsubscribe` are plan-gated at MailerSend.** If your plan did not include them, they are available here; see [sending email](/docs/guides/email/sending-email) and [categories](/docs/guides/email/categories) for how we handle list-unsubscribe on marketing mail.

## Export suppressions

MailerSend keeps five lists under `/v1/suppressions`, one endpoint per list. Four are permanent and carry over; the fifth must not.

Export and run through the [import loop](/docs/guides/email/migrate#3-import-suppressions):

- **Hard bounces**, for example `GET https://api.mailersend.com/v1/suppressions/hard-bounces`
- **Spam complaints**
- **Unsubscribes**
- **Blocklist**, the addresses and patterns you added by hand

**Do not import the On Hold list.** MailerSend's own description is that it holds addresses that "have soft bounced 5 times within 30 days", that "these emails will be blocked for 72 hours", and that they are then "automatically removed from the list". It is a cooling-off period the provider clears by itself, so importing it converts a temporary hold into a permanent suppression and silently stops mailing addresses that were about to be released. Our equivalent of that behaviour is [deferral](/docs/guides/email/events), which we handle from live delivery outcomes rather than from an imported list.

MailerSend's list types map onto our `hard_bounce`, `complaint`, and `manual` reasons; [Suppressions](/docs/guides/email/suppressions) has the full taxonomy.

## Translate webhook events

| Outcome            | MailerSend                                     | Bird                                             |
| ------------------ | ---------------------------------------------- | ------------------------------------------------ |
| Accepted/processed | `activity.sent`                                | `email.accepted` → `email.processed`             |
| Delivered          | `activity.delivered`                           | `email.delivered`                                |
| Temporary failure  | `activity.soft_bounced` / `activity.deferred`  | `email.deferred`                                 |
| Permanent bounce   | `activity.hard_bounced`                        | `email.bounced` / `email.out_of_band_bounce`     |
| Spam complaint     | `activity.spam_complaint`                      | `email.complained`                               |
| Open               | `activity.opened` / `activity.opened_unique`   | `email.opened`                                   |
| Click              | `activity.clicked` / `activity.clicked_unique` | `email.clicked`                                  |
| Unsubscribe        | `activity.unsubscribed`                        | `email.unsubscribed` / `email.list_unsubscribed` |
| Temporarily held   | `recipient.on_hold_added` / `..._removed`      | (no equivalent; see above)                       |

Two differences decide how much of your handler changes.

**Both sides sign with HMAC-SHA256, so this is a re-implementation rather than new code.** MailerSend sends a single `Signature` header holding a hash of the payload computed with that webhook's signing secret. We follow the [Standard Webhooks](https://www.standardwebhooks.com) scheme, which uses `webhook-id`, `webhook-timestamp` and `webhook-signature` and signs a string built from the id, the timestamp and the body, so the timestamp also gives you replay protection. Keep the constant-time comparison you already have and swap the construction; the recipe is in [Webhooks and events](/docs/guides/webhooks).

**MailerSend distinguishes opens and clicks from their unique variants. We do not.** `activity.opened` and `activity.opened_unique` both arrive as `email.opened`, so a handler that counted only the unique variant needs to deduplicate on `recipient_id` itself. Our delivery events are recipient-scoped, so a send to three recipients produces three delivery outcomes rather than one.

## Cut over

Work through [domains and DNS](/docs/guides/email/migrate#2-re-point-domains-and-dns) and the [sandbox smoke test](/docs/guides/email/migrate#5-verify-in-the-sandbox-before-cutover) in the main guide. Both are provider-independent.

## Next steps

- [Sending domains](/docs/guides/email/sending-domains): registration, verification lifecycle, and the DNS records you are publishing
- [Webhooks and events](/docs/guides/webhooks): endpoint setup and Standard Webhooks verification
- [Testing sandbox](/docs/guides/email/testing-sandbox): smoke-test the new integration before cutover
- [Suppressions](/docs/guides/email/suppressions): confirm your imported list and how we maintain it from here