跳转到内容

事件类型

创建 Webhook 时,可以任意组合订阅这些事件类型。

事件 触发时机 将 last_event 设为
email.scheduled 带有 scheduled_at 的邮件已被接受。 scheduled
email.sent 邮件已交给 Amazon SES。 sent
email.delivered 收件人的邮件服务器已接受该邮件。 delivered
email.delivery_delayed 投递被延迟或暂时失败;SES 会持续重试。 delivery_delayed
email.bounced 永久性失败(地址不存在、域名拒收)。 bounced
email.complained 收件人将邮件标记为垃圾邮件。 complained
email.opened 收件人打开了邮件(需要开启打开追踪)。 opened
email.clicked 收件人点击了链接(需要开启点击追踪)。 clicked
email.failed SpacePost 或 SES 无法发送该邮件(被拒绝、渲染失败、团队或租户被暂停)。 failed
email.suppressed 由于收件人在你的抑制列表中,已跳过该收件人。 suppressed

Webhook 请求体与 Resend 的完全一致,因此为 Resend 编写的处理程序可继续使用。

{
"type": "email.delivered",
"created_at": "2026-10-23T14:02:10.000Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme",
"created_at": "2026-10-23T14:02:09.512Z",
"broadcast_id": "0192e1c4-0000-7000-8000-0000000000bb",
"tenant_id": "0192e1c4-5555-7000-8000-0000000000cc",
"tags": { "category": "welcome" }
}
}
  • type 是事件类型;created_at 是事件发生的时间。
  • data.email_id、from、to(该邮件的所有收件人,而不仅是受影响的那一个)、subject 和 created_at(邮件的创建时间)始终存在。
  • data.broadcast_id 仅在群发邮件中出现。
  • data.tenant_id 仅在邮件是代表某个租户发送时(平台模式)出现。它是我们的租户 id,而不是你的 external_id。
  • data.tags 仅在邮件带有标签时出现,形式为对象({ "name": "value" })。
  • 部分事件会在 data 中再增加一个以事件命名的对象:bounce、click、open、failed 或 suppressed。它们列在下方。除此之外不会添加任何内容:没有顶层的 bounceType、delayType 或 reason 键。
{
"type": "email.scheduled",
"created_at": "2026-10-23T14:02:09.700Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Your trial ends tomorrow",
"created_at": "2026-10-23T14:02:09.512Z"
}
}
{
"type": "email.sent",
"created_at": "2026-10-23T14:02:10.100Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme",
"created_at": "2026-10-23T14:02:09.512Z"
}
}
{
"type": "email.delivered",
"created_at": "2026-10-23T14:02:11.350Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme",
"created_at": "2026-10-23T14:02:09.512Z"
}
}

在 SES 投递延迟和临时性退信时触发。单纯的延迟没有额外字段。临时性退信还会携带 bounce 对象(见下文,其中 type: "Transient")。

{
"type": "email.delivery_delayed",
"created_at": "2026-10-23T14:07:00.000Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme",
"created_at": "2026-10-23T14:02:09.512Z"
}
}

只有永久性退信才会触发该事件,并且会将该地址加入你的抑制列表。data.bounce 包含 type(Permanent)、subType 以及接收服务器返回的 message。

{
"type": "email.bounced",
"created_at": "2026-10-23T14:02:12.000Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["nobody@example.com"],
"subject": "Welcome to Acme",
"created_at": "2026-10-23T14:02:09.512Z",
"bounce": {
"type": "Permanent",
"subType": "General",
"message": "smtp; 550 5.1.1 The email account that you tried to reach does not exist."
}
}
}

退信子类型包括 General、NoEmail、Suppressed、OnAccountSuppressionList、MailboxFull、MessageTooLarge、ContentRejected 和 AttachmentRejected。

没有额外字段。该地址会被加入你的抑制列表。

{
"type": "email.complained",
"created_at": "2026-10-23T15:30:00.000Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Our monthly newsletter",
"created_at": "2026-10-23T14:02:09.512Z"
}
}
{
"type": "email.opened",
"created_at": "2026-10-23T14:05:44.000Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme",
"created_at": "2026-10-23T14:02:09.512Z",
"open": { "ipAddress": "203.0.113.7", "userAgent": "Mozilla/5.0 (Macintosh)" }
}
}
{
"type": "email.clicked",
"created_at": "2026-10-23T14:06:10.000Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme",
"created_at": "2026-10-23T14:02:09.512Z",
"click": {
"link": "https://acme.com/get-started",
"ipAddress": "203.0.113.7",
"userAgent": "Mozilla/5.0 (Macintosh)",
"timestamp": "2026-10-23T14:06:10.000Z"
}
}
}
{
"type": "email.failed",
"created_at": "2026-10-23T14:02:10.000Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com"],
"subject": "Welcome to Acme",
"created_at": "2026-10-23T14:02:09.512Z",
"failed": { "reason": "Attachment https://files.acme.com/report.pdf returned HTTP 404" }
}
}

suppressed.reason 为 suppression_list(收件人在你的抑制列表中)或 recipient_domain_does_not_accept_mail(收件人的域名没有邮件服务器)。

{
"type": "email.suppressed",
"created_at": "2026-10-23T14:02:10.000Z",
"data": {
"email_id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10",
"from": "Acme <hello@acme.com>",
"to": ["user@example.com", "other@example.com"],
"subject": "Welcome to Acme",
"created_at": "2026-10-23T14:02:09.512Z",
"suppressed": { "recipients": ["other@example.com"], "reason": "suppression_list" }
}
}

Node SDK 导出了以 type 为判别字段的联合类型 WebhookPayload,spacepost.webhooks.verify() 返回的就是它,因此根据 event.type 进行类型收窄,即可得到正确的 data 字段:

import type { WebhookPayload } from "@spacepost/node";
function handle(event: WebhookPayload) {
switch (event.type) {
case "email.bounced":
return event.data.bounce?.subType;
case "email.clicked":
return event.data.click?.link;
case "email.suppressed":
return event.data.suppressed.recipients;
case "email.failed":
return event.data.failed.reason;
default:
return event.data.tenant_id; // present when the email was sent for a tenant
}
}