Sign inGet started

从 Brevo 迁移

本页将 Brevo 的事务性发送载荷、屏蔽列表和 webhook 映射到 Bird。按顺序完成主迁移指南,并在步骤 1、3 和 4 中使用这些映射。
Brevo 将屏蔽记录保存在两个互不相关的位置,一个用于事务性邮件,一个用于营销邮件。在规划步骤 3 之前,请先阅读导出屏蔽记录:只导出其中一个而遗漏另一个,正是这次迁移最容易犯的错误。

交给你的 agent

将以下内容粘贴到 Claude Code、Cursor 或 Codex 中。agent 会基于你的代码仓库逐步处理本页内容,使用它已有的任意 Bird 接口:已连接的 MCP 服务器,或已安装并登录的 CLI。
代码示例
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 until I have seen the sandbox results and replied with the words cut over to Bird. Retiring the Brevo path is a separate step that comes later: ask me again and wait for me to reply with the words retire the Brevo 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.

映射发送调用

Brevo 的 POST /v3/smtp/email 与我们的 POST /v1/email/messages 结构相近。主要工作是将 Brevo 的地址对象展开为纯字符串。
功能BrevoBird
认证api-key 请求头Authorization: Bearer
发件人sender{email, name}from
收件人to / cc / bcc{email, name} 数组)to / cc / bcc(地址数组)
主题subjectsubject
正文htmlContent / textContenthtml / text(至少提供一个)
回复地址replyTo{email, name}reply_to(数组)
自定义请求头headers(Title-Case 键名)headers(string → string 对象)
可筛选标签tags(字符串数组)tags: {name, value} 键值对
已存模板templateId + paramstemplate + template.parameters
附件attachmenturl 或 base64 contentattachments(仅支持 base64,见下文)
定时发送scheduledAtscheduled_at
批次句柄batchId(无对应字段,见下文)
按收件人分版messageVersions每个版本单独发送,或使用 batch
类别(无)category: marketing(默认)或 transactional
字段上限和默认值(收件人数量、标签和元数据限制)详见发送邮件
迁移说明:
  • 地址在 Brevo 中是对象,在这里是字符串。 {"email": "a@x.com", "name": "A"} 变为 "A <a@x.com>" 或仅 "a@x.com"。同样的展开方式适用于 senderreplyTo
  • tags 是纯字符串。我们的标签是键值对。 类似 "welcome" 的标签变为 {"name": "category", "value": "welcome"}。选择一个稳定的 name,以便仪表盘的筛选方式与 Brevo 标签统计保持一致。
  • params 是模板数据,不是往返上下文。 它对应为 template.parameters。如果你之前还用它将自有标识符传递到事件中,请将这些标识符移到 metadata,我们会在每个 webhook 事件中连同 email_id/recipient_id 一起回传。
  • messageVersions 没有单次调用的等价方式。 每个版本都是独立的收件人集合和载荷,因此它要么拆成单独的发送请求,要么作为 batch 中的一个条目。
  • batchId 没有对应字段。 Brevo 的 batchId 将定时消息分组,以便你可以作为整组取消或重新安排。这里的定时发送通过消息 id 逐条寻址;没有可传递或用于取消的分组句柄。
  • 不支持通过 URL 添加附件。 Brevo 接受 attachment 条目作为 URL 并自行拉取文件。你需要自己获取文件并以 base64 编码发送;参见附件

导出屏蔽记录

Brevo 将屏蔽记录分散在两个系统中,它们不共享端点或分页方式。如果迁移时只拉取第一个,会在不知不觉中丢失所有营销退订:
  • 事务性屏蔽和退订GET /v3/smtp/blockedContacts,分页返回(默认每页 50 条,最多 100 条),每条记录附带被屏蔽的原因。
  • 营销屏蔽列表:它根本不是屏蔽端点,而是保存在联系人记录的 emailBlacklisted 字段上,因此需要分页遍历 GET /v3/contacts(通过 offset 每页最多 1000 条),保留该标志为 true 的联系人。
将两者都通过导入循环处理。Brevo 的屏蔽原因对应我们的 hard_bouncecomplaintmanual 原因;屏蔽记录中有完整分类。

转换 webhook 事件

结果BrevoBird
已接受/已处理requestemail.acceptedemail.processed
已投递deliveredemail.delivered
临时失败deferred / soft_bounceemail.deferred
永久退信hard_bounceemail.bounced / email.out_of_band_bounce
垃圾邮件投诉spamemail.complained
已屏蔽/已抑制blocked / invalid_emailemail.rejected
打开opened / unique_openedemail.opened
点击clickemail.clicked
退订unsubscribedemail.unsubscribed / email.list_unsubscribed
两个差异决定了你的处理程序需要改动多少。
在迁移之前,先检查你的端点当前依赖了什么。 Brevo 的 webhook 安全页面记录了你在端点上配置的凭据:以 https://username:password@example.com/ 形式附加在 URL 上的用户名和密码、Bearer token、自定义请求头,以及它的 IP 范围。我们改为按照 Standard Webhooks HMAC 方案对每次投递进行签名,因此验证从调用方携带凭据转变为你的处理程序计算签名。如果你当前的端点将凭据保存在 URL 中,请将其移除并轮换该凭据对,而不是复用它:一个曾出现在 URL 中的凭据已经进入了访问日志、配置导出和供应商控制台。具体做法见 Webhooks and events
Brevo 区分打开/点击与其去重变体,我们不区分。 openedunique_opened 都以 email.opened 形式到达,因此之前只计数去重变体的处理程序需要自行基于 recipient_id 去重。我们的投递事件以收件人为粒度,因此发送给三个收件人会产生三个投递结果,而非一个。

切换

按照主指南完成域名和 DNS 以及沙盒冒烟测试。两者都与邮件服务商无关。

后续步骤