新用户新用户即享 DNS 与 CDN 免费套餐。立即注册

事件与 Webhook

在发送记录中跟踪邮件状态,并在你自己的端点上接收带签名的送达、退信、投诉、打开和点击事件。

4 分钟阅读 更新于 2026年10月20日

你发送的每封邮件都会获得一个邮件 ID,并在排队、投递、重试或退信的过程中依次经历一系列状态。你可以在控制台的发送记录中跟踪,用 API 查询,也可以通过 Webhook 把每个事件推送到你自己的服务器。

邮件状态

每份收件人副本都有一个状态。状态只会向前推进:例如已送达的邮件之后仍可能变为退信或投诉,但不会回到排队中。

状态控制台显示含义
queued排队中已接收,等待投递
deferred延迟重试收件服务器返回临时错误,会在最长 72 小时内重试投递。
delivered已送达收件服务器已接收邮件
bounced退信投递永久失败,可能发生在投递过程中,也可能来自之后收到的退信报告。退信类型见下文。
complained投诉收件人将邮件标记为垃圾邮件
rejected已拒绝发送前即被拒绝,例如收件人在抑制名单中、不在沙箱允许范围内,或内容被拒
failed失败重试 72 小时后仍无法投递,或未能入队

打开、点击和退订不属于状态。它们会记录在邮件上(opened_at、clicked_at、unsubscribed_at),并作为 Webhook 事件发送。在发送记录中,已打开的邮件会多一个 已打开 标签。

退信类型

类型何时发生影响
硬退信永久性的 5xx 拒收,例如用户不存在地址加入全局抑制名单
软退信邮箱已满(x.2.2),或因系统、网络、策略原因拒收(x.3.x 到 x.7.x,例如 IP 信誉拦截)地址不会被抑制

发送记录

发信流的 发送记录 标签页列出最近 30 天的邮件,包括时间、收件人、主题、分类、来源(API 或 SMTP)和状态。你可以按 收件人、状态、来源、类别 和日期范围筛选。

点击某封邮件可打开 邮件详情:状态、发件人、主题、分类、标签、收件服务器的最后一次响应,以及事件列表,其中包含每次投递尝试的 SMTP 响应码、响应内容、退信类型、MX 主机和 IP。

概览 标签页会按 7 天或 30 天汇总同样的数据:发送量、送达率、退信率、投诉率、打开和点击。

Webhook

Webhook 会在事件发生时把它发送到你的 HTTPS 端点,这样你的应用就能做出响应,例如在硬退信后把用户的地址标记为无效。

配置 Webhook

  1. 打开发信流,进入 Webhook 标签页。
  2. 在 接收地址 中填写一个公网可访问的 https:// URL。不接受内网和本地地址。
  3. 在 事件 中选择需要的事件,默认全选。
  4. 点击 保存。首次保存 URL 时,会显示一次 签名密钥,以 whsec_ 开头。请把它保存在你的服务器上。
  5. 点击 发送测试,发送一个 test 事件,确认你的端点能正常响应。

如需更换密钥,点击 重置签名密钥,旧密钥会立即失效。如需停止接收事件,清空 URL 并保存。

事件

事件控制台显示触发时机
delivered送达收件服务器已接收邮件
deferred延迟某次投递尝试临时失败,每次尝试发送一次。
bounced退信永久失败。data.bounce_type 为 hard 或 soft。
complained投诉收件人将邮件举报为垃圾邮件
failed失败72 小时后放弃投递
rejected拒绝发信服务器拒绝了某个收件人
opened打开邮件首次被打开(需开启打开跟踪)
clicked点击每次点击被跟踪的链接(需开启点击跟踪)
unsubscribed退订收件人退订

被 HTTP API 直接拒绝的收件人会在 API 响应的 rejected 字段中返回,不会触发 Webhook。

请求格式

每个事件以 POST 请求发送,请求体为 JSON:

http
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: Yeap-TM-Webhook/1
X-Yeap-Signature: t=1760955304,v1=5f2b9c...e81a

{
  "id": "evt_01JAQ40B2C3D4E5F6G7H8J9K0M",
  "type": "bounced",
  "stream_id": 12,
  "message_id": "01JAQ3Z9V8K4M2N6P7R5S1T0XW",
  "created_at": "2026-10-20T10:15:04+00:00",
  "data": {
    "rcpt": "alice@example.org",
    "smtp_code": 550,
    "smtp_response": "5.1.1 The email account that you tried to reach does not exist",
    "bounce_type": "hard",
    "mx": "mx.example.org"
  }
}

id 对每个事件唯一,可用来忽略重复事件。data 的内容取决于事件类型:

事件data 字段
delivered、deferred、bounced、complained、failed、rejectedrcpt,以及在有值时提供的 smtp_code、smtp_response、bounce_type、mx、at
openedrcpt、ip、user_agent
clickedrcpt、url、ip、user_agent
unsubscribedrcpt、category、via
testmessage

验证签名

X-Yeap-Signature 邮件头的格式为 t=<timestamp>,v1=<signature>。签名是对 <timestamp>.<raw request body> 计算的十六进制 HMAC-SHA256,密钥为完整的签名密钥(包括 whsec_)。验证请求的步骤:

  1. 在解析 JSON 之前,按收到的原样读取原始请求体。
  2. 对 t + "." + body 计算 HMAC,并用恒定时间比较与 v1 对比。
  3. 拒绝时间戳超过 5 分钟的请求,以防重放攻击。

Python:

python
import hashlib, hmac, time

def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    ts, sig = parts.get("t", ""), parts.get("v1", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), ts.encode() + b"." + body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)

Node.js:

javascript
const crypto = require('crypto');

function verify(secret, header, rawBody, tolerance = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const ts = Number(parts.t);
  if (!ts || Math.abs(Date.now() / 1000 - ts) > tolerance) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

响应与重试

  • 请在 10 秒内返回任意 2xx 状态码,其他情况均视为失败。
  • 投递失败会以递增的间隔重试,从 1 分钟开始,最长间隔 4 小时。事件发生 24 小时后停止重试。
  • 事件可能重复送达,也可能乱序到达。请用事件 id 去重,并以邮件状态判断当前情况。

Webhook 标签页中的 最近投递 表格显示最近 100 次投递,包括事件、状态(待投递、成功、重试中、已放弃)、尝试次数、最后一次错误和下次尝试时间。

相关文档

本页内容是否有帮助?