从 Resend 迁移
本页将 Resend 的 POST /emails 载荷、屏蔽处理和 Svix 签名的 webhook 映射到 Bird。按顺序遵循主迁移指南,并在步骤 1、3 和 4 中使用这些映射。发送载荷的字段相似,但跟踪、元数据和 webhook 验证需要做出更改。
交给你的 agent
将以下内容粘贴到 Claude Code、Cursor 或 Codex 中。Agent 会根据你的代码仓库逐步执行本页操作,使用它已有的任何 Bird 接口:已连接的 MCP 服务器,或已安装并登录的 CLI。
代码示例
I am moving an email integration from Resend 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/resend.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 Resend usage in this repository before you change anything: the POST /emails and batch call sites and any SDK wrappers around them, the webhook handler and the URL it is registered at, and every domain I send from.
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. Rebuild my suppression list and import it into Bird before any production traffic goes through Bird, so my first sends do not reach addresses that already bounced or complained. Resend publishes no suppression export, so there is no endpoint that returns this list: derive it from whatever bounce and complaint events I have stored from my webhook, from the dashboard's Emails view, and from contacts marked unsubscribed in any Audience I use. That means the list can be incomplete without either of us noticing, so show me the list you built and tell me which of those sources each address came from before you import anything. 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. Verification is a header change rather than a rewrite here: Resend signs with Svix and Bird signs per Standard Webhooks, which is the same HMAC construction with the svix-* headers renamed to webhook-*, so keep my verifier and rename what it reads. The event shape does change: Resend's events are scoped to a message and Bird's are scoped to a recipient, so a send to three recipients yields three outcomes rather than one. See 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 Resend path is a separate step that comes later: ask me again and wait for me to reply with the words retire the Resend 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.映射发送调用
| 功能 | Resend | Bird |
|---|---|---|
| 发件人 | from | from |
| 收件人 | to / cc / bcc | to / cc / bcc(数组) |
| 主题 | subject | subject |
| 正文 | html / text | html / text(至少提供一个) |
| 回复地址 | reply_to | reply_to(数组) |
| 自定义请求头 | headers | headers(string → string 对象) |
| 可筛选标签 | tags: {name, value} 键值对 | tags: {name, value} 键值对 |
| 往返上下文 | (无;标签兼作上下文) | metadata: 任意 JSON |
| 定时发送 | scheduled_at | scheduled_at |
| 打开/点击跟踪 | 按域名在面板中设置 | track_opens / track_clicks(默认 true) |
| 类别 | (无) | category: marketing(默认)或 transactional |
字段上限和默认值(收件人数量、标签和元数据限制)详见发送邮件。
迁移注意事项:
- 标签保持原有结构,metadata 是一个升级。 Resend 的标签与我们使用的 {name, value} 键值对相同,但其值的限制迫使关联数据被放入标签值中。在这里,请将关联上下文移入 metadata(任意 JSON,会在每个 webhook 事件中回传,也会在 API 读取时返回),标签仅用于筛选。参见标签与元数据的区别。
- 跟踪移入载荷中。 Resend 在面板中按域名切换打开/点击跟踪。我们按消息设置 track_opens/track_clicks(默认均为 true)。
- scheduled_at 直接映射,字段名也一样。参见定时发送。对于 react,请在应用中将 React Email 模板渲染为 HTML(@react-email/render 的 render 函数可直接使用),然后将结果作为 html 发送。
- 附件直接迁移。 Resend 的 attachments(base64 content)映射到我们的附件数组。为内联图片设置 content_id。
- 批量发送直接迁移。 Resend 的 POST /emails/batch 对应我们的批量端点,两者都返回逐条结果。
导出屏蔽列表
Resend 没有提供专门的屏蔽列表导出功能。请提取最后一个事件为 bounced 或 complained 的地址。使用面板中的 Emails 视图或已存储的 webhook 事件,然后通过导入流程处理该列表。如果你使用 Audiences 发送营销邮件,也请迁移标记为已退订的联系人。
转换 webhook 事件
| 结果 | Resend | Bird |
|---|---|---|
| 已接受/已处理 | email.sent | email.accepted → email.processed |
| 已投递 | email.delivered | email.delivered |
| 临时失败 | email.delivery_delayed | email.deferred |
| 永久退回 | email.bounced | email.bounced / email.out_of_band_bounce |
| 垃圾邮件投诉 | email.complained | email.complained |
| 已拦截/已屏蔽 | email.failed | email.rejected |
| 打开 | email.opened | email.opened |
| 点击 | email.clicked | email.clicked |
| 退订 | (无) | email.unsubscribed / email.list_unsubscribed |
Webhook 验证使用了相关的 HMAC 构造,但请求头不同。Resend 使用 svix-id、svix-timestamp 和 svix-signature。Bird 遵循 Standard Webhooks 规范,使用 webhook-* 请求头。请更新你的验证器,使用 Bird 的签名密钥和 Webhooks 与事件中的流程。
一个行为差异:Resend 的事件以消息为维度。我们的投递事件以收件人为维度(recipient_id 与 email_id 并列),因此一次发送给三个收件人会产生三个投递结果,每个收件人一个。
正式切换
后续步骤
- 发送域名:注册、验证生命周期,以及需要重新指向的 DNS 记录
- Webhooks 与事件:端点设置和 Standard Webhooks 验证
- 测试沙箱:在正式切换前对新集成进行冒烟测试
- 屏蔽列表:确认导入的列表以及后续的维护方式