# Migrate from Brevo

This page maps Brevo's transactional send payload, blocklists, 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.

Brevo keeps suppressions in two unrelated places, one for transactional mail and one for marketing. Read [Export suppressions](#export-suppressions) before you plan step 3: exporting one and not the other is the mistake this migration invites.

## 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 Brevo 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/brevo.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 Brevo usage in this repository before you change anything: calls to /v3/smtp/email and any SDK wrappers around them, whether I send with templateId or with htmlContent, 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 Brevo 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. Brevo holds these in two separate places and I need both: GET /v3/smtp/blockedContacts for transactional blocks and unsubscribes, and the marketing blocklist, which lives on the contact records as emailBlacklisted rather than on a suppression endpoint. Page through both. 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. Brevo's webhook security page documents credentials I configure on the endpoint rather than a payload signature, so check what my endpoint actually relies on today: if it is a username and password in the webhook URL, take them out and tell me to rotate that pair rather than reusing it, because a credential that has lived in a URL should be treated as exposed. Bird signs every delivery instead. 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 Brevo 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

Brevo's `POST /v3/smtp/email` and our [`POST /v1/email/messages`](/docs/api/reference/create-email-message) are close in shape. Most of the work is unwrapping Brevo's address objects into plain strings.

| What it does       | Brevo                                           | Bird                                                                |
| ------------------ | ----------------------------------------------- | ------------------------------------------------------------------- |
| Auth               | `api-key` header                                | `Authorization: Bearer`                                             |
| Sender             | `sender` (`{email, name}`)                      | `from`                                                              |
| Recipients         | `to` / `cc` / `bcc` (arrays of `{email, name}`) | `to` / `cc` / `bcc` (arrays of addresses)                           |
| Subject            | `subject`                                       | `subject`                                                           |
| Body               | `htmlContent` / `textContent`                   | `html` / `text` (at least one)                                      |
| Reply-to           | `replyTo` (`{email, name}`)                     | `reply_to` (array)                                                  |
| Custom headers     | `headers` (Title-Case keys)                     | `headers` (string → string object)                                  |
| Filterable labels  | `tags` (array of strings)                       | `tags`: `{name, value}` pairs                                       |
| Stored template    | `templateId` + `params`                         | `template` + `template.parameters`                                  |
| Attachments        | `attachment` (`url` or base64 `content`)        | `attachments` (base64 only, see below)                              |
| Scheduling         | `scheduledAt`                                   | `scheduled_at`                                                      |
| Batch handle       | `batchId`                                       | (no counterpart, see below)                                         |
| Per-recipient copy | `messageVersions`                               | one send per version, or a [batch](/docs/guides/email/sending-bulk) |
| Category           | (none)                                          | `category`: `marketing` (default) or `transactional`                |

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"`. The same unwrapping applies to `sender` and `replyTo`.
- **`tags` are bare strings. Our tags are pairs.** A tag like `"welcome"` becomes `{"name": "category", "value": "welcome"}`. Pick a stable `name` so your dashboards filter the way your Brevo tag stats did.
- **`params` is template data, not round-trip context.** It becomes `template.parameters`. If you were also using it to carry your own identifiers through to events, move those to [`metadata`](/docs/guides/email/sending-email), which we echo back on every webhook event alongside `email_id`/`recipient_id`.
- **`messageVersions` has no single-call equivalent.** Each version is a distinct recipient set and payload, so it becomes either its own send or one entry in a [batch](/docs/guides/email/sending-bulk).
- **`batchId` has no counterpart.** Brevo's `batchId` groups scheduled messages so you can cancel or reschedule them as a set. Scheduled sends here are addressed individually by their message id; there is no group handle to pass or cancel against.
- **Attachment by URL is not supported.** Brevo accepts an `attachment` entry as a URL for it to fetch. Fetch the file yourself and send it base64-encoded; see [attachments](/docs/guides/email/attachments).

## Export suppressions

Brevo splits suppressions across two systems that do not share an endpoint or a pagination scheme, and a migration that pulls only the first silently loses every marketing unsubscribe:

- **Transactional blocks and unsubscribes**: `GET /v3/smtp/blockedContacts`, paginated (50 per page by default, 100 maximum), each entry carrying the reason it was blocked.
- **Marketing blocklist**: not a suppression endpoint at all. It lives on the contact record as `emailBlacklisted`, so page `GET /v3/contacts` (up to 1000 per page with `offset`) and keep the contacts where that flag is true.

Run both through the [import loop](/docs/guides/email/migrate#3-import-suppressions). Brevo's block reasons map onto our `hard_bounce`, `complaint`, and `manual` reasons; [Suppressions](/docs/guides/email/suppressions) has the full taxonomy.

## Translate webhook events

| Outcome            | Brevo                       | Bird                                             |
| ------------------ | --------------------------- | ------------------------------------------------ |
| Accepted/processed | `request`                   | `email.accepted` → `email.processed`             |
| Delivered          | `delivered`                 | `email.delivered`                                |
| Temporary failure  | `deferred` / `soft_bounce`  | `email.deferred`                                 |
| Permanent bounce   | `hard_bounce`               | `email.bounced` / `email.out_of_band_bounce`     |
| Spam complaint     | `spam`                      | `email.complained`                               |
| Blocked/suppressed | `blocked` / `invalid_email` | `email.rejected`                                 |
| Open               | `opened` / `unique_opened`  | `email.opened`                                   |
| Click              | `click`                     | `email.clicked`                                  |
| Unsubscribe        | `unsubscribed`              | `email.unsubscribed` / `email.list_unsubscribed` |

Two differences decide how much of your handler changes.

**Check what your endpoint relies on today before you port it.** Brevo's webhook security page documents credentials you configure on the endpoint: a username and password appended to the URL as `https://username:password@example.com/`, a bearer token, custom request headers, and its IP ranges. We sign every delivery per the [Standard Webhooks](https://www.standardwebhooks.com) HMAC scheme instead, so verification moves from something the caller carries to something your handler computes. If your current endpoint holds credentials in its URL, take them out and rotate that pair rather than reusing it: a credential that has lived in a URL has reached access logs, config exports, and a vendor console. The recipe is in [Webhooks and events](/docs/guides/webhooks).

**Brevo distinguishes opens and clicks from their unique variants. We do not.** `opened` and `unique_opened` 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