后端与接口
构建和调试 Sent v3 Webhook…
当需要接收、验证、去重和排查 Sent v3 Webhook 事件(含签名失败、401、端点被禁用、密钥轮换、投递日志分析)时使用。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 59/100 ×0.15
适用场景
构建和调试 Sent v3 Webhoo特定
解决思路
- 在解析 JSON 之前先捕获原始请求体字节(raw request body bytes)。
- 从签名密钥中去除 `whsec_` 前缀,再对剩余部分做 base64 解码,得到原始 HMAC 密钥。
- 将签名内容构造为 `{x-webhook-id}.{x-webhook-timestamp}.{raw_body}`。
- 用该密钥计算 HMAC-SHA256,对摘要做 base64 编码,并加上 `v1,` 前缀。
- 使用常量时间比较(constant-time comparison)与 `x-webhook-signature` 比对。
- 当 `abs(now - timestamp) > 300` 秒时拒绝请求。
- 不要用 `x-webhook-id` 作为去重键:它标识的是端点 UUID,每次投递都相同,会导致所有事件被折叠成一个;应按事件类型派生去重键。
- 按固定顺序排查失败:先查签名不匹配(多数由修改请求体的中间件或框架 JSON 解析器导致,需确认框架的原始请求体访问方式),再查重放拒绝(服务器时钟偏移超过 300 秒容差),再查密钥错误(`whsec_` 前缀未去除,或轮换使旧密钥失效且无双重签名窗口),再查完全无事件到达(检查 `GET /v3/webhooks/{id}` 的 `is_active` 和 `consecutive_failures`,并读取 `GET /v3/webhooks/{id}/events` 的投递日志),最后查事件到达但未处理(对比 `event_types` 和 `event_filters` 与处理器分支)。
- 理解投递失败条件:任何非 2xx 状态、超过 `timeout_seconds` 的超时或连接失败都算失败;重试采用指数退避,首次重试约在失败后一分钟,之后翻倍,间隔上限 60 分钟,遇到首个 2xx 或 `retry_count` 耗尽即停止;投递行状态依次为 `PENDING`、`RETRYING`,然后 `DELIVERED` 或 `FAILED`。
- 注意 `consecutive_failures` 统计连续失败投递次数:连续十次错误响应会禁用端点;修复接收端后用 `PATCH /v3/webhooks/{id}/toggle-status` 或 Sent Dashboard 重新启用;任何成功投递都会将计数器重置为零。
- 仅在将事件持久化移交给队列后再返回确认(acknowledge),并确保该移交在 `timeout_seconds` 内从容完成。
- 用 `POST /v3/webhooks` 注册端点,必填 `display_name`,可配置 `endpoint_url`、`event_types`、`event_filters`、`retry_count`(1–5,默认 3)、`timeout_seconds`(5–120,默认 30);`201` 响应是唯一完整出现 `signing_secret` 的地方,须立即存入密钥存储。
- 有意识地设置 `event_filters`:未过滤的 `message` 订阅会投递包括 `queued` 和 `routed` 在内的每个生命周期转换,且重路由会在同一 `message_id` 上重新触发 `queued` 和 `routed`;只过滤应用真正要处理的转换。
- 轮换密钥时用 `POST /v3/webhooks/{id}/rotate-secret`,它返回新的 `whsec_` 密钥并立即失效旧密钥,没有服务端重叠窗口;让接收端接受一小组候选密钥,轮换后原子地将返回的密钥存为主密钥并临时保留旧值,确认新投递后再淘汰旧值;轮换响应与密钥存储更新之间的短暂间隙无法消除,应控制在数秒内以便失败投递重试。
- 注意 `POST /v3/webhooks/{id}/rotate-secret` 和 `POST /v3/webhooks/{id}/test` 属于敏感速率限制层,每分钟 10 次请求,脚本化轮换循环会触发 429。
- 处理事件负载时注意两种 `field` 值:`message` 和 `templates`;消息事件带 `event` 表示转换(`message.queued`、`.routed`、`.sent`、`.delivered`、`.read`、`.failed`、`.scheduled`、`.filtered`、`.blocked`、`.received`),模板事件既无 `event` 也无 `sub_type`。
- 理解事件语义:`read` 仅到达 WhatsApp 和 RCS;`filtered` 表示策略或同意门控,`blocked` 表示账户前置条件(如余额不足),二者都不是运营商失败;从未路由的自动检测消息的终态事件带 `channel: "auto"`。
- 发布前先对合成投递运行本地 oracle,再用 `POST /v3/webhooks/{id}/test` 并在请求体中带 `event_type` 发起真实签名请求;测试事件只投递一次且不重试,每次修复后需重新运行。
- 运行 `python3 scripts/verify_signature.py --self-test` 进行自检。
- 仅在接收端满足以下条件时发布:篡改请求体返回 `401`、时间戳超过 300 秒返回 `401`、有效投递返回 `200`、重复投递返回 `200` 且不重复副作用。
- 本地开发时通过公共 HTTPS 隧道暴露接收端并注册该 URL,Sent 无法访问私有地址;API 接受注册 `http://`,但本地工作之外绝不应使用;每个环境保留独立的 webhook 注册,避免开发端点的失败禁用生产端点。
- 将每个负载值视为不可信输入:未经转义绝不把 `text` 或 `reason` 插入 shell 命令、SQL 字符串或提示词。
- 聚合投递率回归用 `messaging-performance-analyzer` 诊断,模板审批内容用 `waba-template-author`,入站关键词或同意语义用 `sent-two-way-messaging`。
适用边界 · 注意事项
- 没有 Sent 账号或无法访问 Sent v3 API 与 Dashboard 时无法使用。
- 需要非 Sent 或非 Svix 兼容的 webhook 方案时不适用。
- 无法提供公共 HTTPS 端点(如纯内网、无隧道)时无法接收投递。