想让客户把截图和发票发到一个地址,再自动建工单,可以用 Resend 收信。接入时要分开三件事:收到通知、读取正文、下载附件。email.received 只报告邮件到了,正文和文件要另调 API,不能把事件里的附件数组直接当文件保存。

第一次接入,需要改企业邮箱的MX吗?

先用默认地址就能开始。在 Resend 的 Emails → Receiving 页取得团队的 .resend.app 地址,向它发邮件。这个阶段不需要动 DNS,适合先验证程序能否拿到内容。收信入门文档列出了默认域名入口。

要使用品牌地址时,建议选择独立子域名。例如下面的地址是虚构示例:客户发给 [email protected],员工仍在原来的 example.com 邮箱办公。

在 Domains 中打开已验证域名的详情,启用 Receiving,复制弹窗里的 MX 到 DNS 服务,点击“I’ve added the record”,等收信记录显示 verified。若是新域名,先完成添加和验证。官方域名指南明确建议已有邮箱的用户采用子域名;同优先级的两组 MX 并不会让双方各收一份邮件。

Webhook究竟会给程序什么?

应用要提供一个公网可访问的 POST 路由。在 Webhooks 页面点 Add Webhook,填写该路由 URL,选择 email.received,再点 Add。本地开发可用公开隧道地址,不能把 localhost 直接填给远端服务。步骤依据创建收信Webhook文档。

下面这张表决定接到事件后该读哪一层,避免拿错接口。

要完成的动作从哪里取关键字段或结果
确认哪封邮件到达Webhook事件data.email_id、主题、收件人
读取客户描述Receiving API邮件的 text、html
获取实际PDF文件Attachments API及下载请求download_url、expires_at

收信域名可以收到不同用户名的邮件,因此应用仍要设定自己接受哪些地址。工单入口只处理工单邮箱,其余地址进入忽略或人工队列,别让任意前缀都新建业务记录。

为什么要保留原始请求再验签?

路由先读取原始请求文本,再交给 SDK 验证。不要先解析 JSON 又序列化回来:空格等变化也会使签名不匹配。验证时提供 svix-id、svix-timestamp、svix-signature 三个请求头,以及 Webhook 详情页的 signing secret;Resend SDK 或 Svix 库都能处理。验签文档说明了这一要求。

签名密钥与调用邮件 API 的密钥用途不同,均留在服务端。验签失败时拒绝请求;通过后再检查事件类型。这里证明的是通知来源,客户邮件内容仍是外部输入,不能仅凭发件人字段授权修改订单。

用email_id把正文和附件接起来

下面是 Node.js 服务端处理片段,前提是已经初始化 Resend SDK,并得到验签通过且类型为 email.received 的 event。它展示单页附件的读取,不是完整路由;注释处还需接入自己的文件存储:

const emailId = event.data.email_id;
const { data: email, error: emailError } =
  await resend.emails.receiving.get(emailId);
if (emailError) throw new Error(emailError.message);
if (!email) throw new Error("Email response is empty");

const { data: files, error: filesError } =
  await resend.emails.receiving.attachments.list({ emailId });
if (filesError) throw new Error(filesError.message);
if (!files) throw new Error("Attachment response is empty");

// email.text / email.html:正文;files.data:本页附件列表
for (const file of files.data) {
  const response = await fetch(file.download_url);
  if (!response.ok) throw new Error(`Download failed: ${response.status}`);
  const bytes = await response.arrayBuffer();
  // 按业务规则保存 bytes,使用 emailId 和 file.id 建立关联
}

样本里只有一个 PDF,正式邮件还可能夹带签名图片。业务若只接受发票,应在下载处理前按文件类型和大小筛选,再核对文件实际格式;文件名只是展示信息。保存路径用应用生成的标识,不直接拼接来信文件名。两个客户都叫“发票.pdf”,也应成为两个独立文件。

正文接口是 receiving.get,不要误调发信查询接口。具体返回结构见读取已收邮件API。工单描述可先使用纯文本;如要显示 HTML,需要在自己的界面处理不可信内容。

附件指南注明下载链接有效期为一小时。保存邮件ID、附件ID和实际文件,不要只存临时链接。任务排队较久时,在下载前重新请求链接;若响应 has_more 为真,说明还有附件未取完,不能将当前结果标记为整封邮件处理完成;字段见附件列表API。附件存到对象存储后,也要单独安排备份,可参考数据库与Storage分开备份。

发一封什么邮件才能检查完整链路?

发送一个自己制作的小 PDF,正文写“收信验证-001”,主题写“附件入库测试”;这些都是示例数据。先在 Emails → Receiving 打开该邮件,确认正文和附件齐全,再对照应用拿到的邮件ID、正文标记和下载文件。后台还可直接点击附件下载,作为文件内容的参照。

检查不能停在 Webhook 返回成功。应用应出现一张工单,描述包含唯一标记,关联文件可以打开,文件大小与样本一致。任何一环缺失,都能由这封样本定位到通知、正文读取或下载阶段。

接着从 Webhooks 进入端点和具体消息,点 Replay。Resend允许重放成功及失败事件,持续失败的端点可能自动停用,恢复服务后先到 Webhooks 页重新启用。保存业务记录时应给邮件ID建立唯一约束。重放后仍只有一张工单,才符合这里的目标。

收到HTTP成功以后,附件处理算完成了吗?

如果后端把任务可靠写入队列便返回成功,Resend看到的是通知已交付。之后下载失败,需要你自己的任务重试。建议分别记录“已入队、正文已存、文件已存”,不要用一个成功状态覆盖全程。

去重也要区分“邮件见过”和“任务做完”:首次通知创建记录后下载失败,下一次处理应补做缺失附件,而不是发现邮件ID存在就全部跳过。附件可用邮件ID与附件ID的组合识别,成功保存一个就记下一个;人工重放时沿用原记录继续处理。

本文依据截至2026年9月15日的官方接口说明编写,未部署测试,也未做大附件压力测试。生产环境的队列任务可接入定时任务心跳监控,相关服务的接入笔记放在工具栈目录。