投递行为
取自该平台的官方文档,最近核对于 2026-08-06。平台没有给出数字的地方,这张表也不填。
| 响应时限 | 平台未公布 |
|---|---|
| 重试 | 8 次尝试:立即、5 秒、5 分钟、30 分钟、2 小时、5 小时、10 小时、10 小时。之后发送一条 message.attempt.exhausted 运维 Webhook |
| 放弃 | 所有尝试连续失败 5 天后,端点会被停用 |
| 出处 | https://docs.svix.com/retries |
签名
用一个经测试套件证明的可运行验证器核对,不是照着文档读来的。最近核对于 2026-08-20。
| 算法 | HMAC-SHA256 |
|---|---|
| 待签名串 | {id}.{timestamp}.{body} |
| 编码 | base64 |
| 请求头 | svix-signature = v1,base64 |
| 时间容差 | 5 分钟 |
验证签名
要用原始请求体,逐字节一致,并且在任何 JSON 解析之前。框架一旦重新序列化请求体,本页的每一种方案都会失效。
const crypto = require("node:crypto");
// base64(HMAC-SHA256(base64-decoded secret, "{id}.{timestamp}.{body}"))
const id = req.headers["svix-id"];
const ts = req.headers["svix-timestamp"];
const key = Buffer.from(whsecSecret.replace("whsec_", ""), "base64");
const expected = crypto
.createHmac("sha256", key)
.update(`${id}.${ts}.${rawBody}`)
.digest("base64");
// svix-signature holds space-separated candidates like "v1,<base64>"
const valid = req.headers["svix-signature"]
.split(" ")
.some((part) => part.split(",")[1] === expected);也可以用官方 SDK:new Webhook(secret).verify(rawBody, headers),它还会替你强制执行 5 分钟的时间容差。计算 HMAC 之前,要去掉 whsec_ 前缀,再把剩下的部分做 base64 解码;跳过这一步是最典型的错误。
容易踩的坑
Clerk 和 Resend 的 Webhook 底层都是 Svix,所以一个验证器就能覆盖这三家:请求头和格式完全相同,只是存放密钥的控制台不同。
重试阶梯共 8 次尝试(立即、5 秒、5 分钟、30 分钟、2 小时、5 小时、10 小时、10 小时),之后一条 message.attempt.exhausted 运维 Webhook 会告诉你它放弃了。连续五天全部失败,端点就会被停用。
延伸阅读
AnyHook 挡在接收 Svix Webhook 的端点前面:约 1 秒应答,你的服务器宕机时按自己的计划重试,并且每个事件都可以重放。 只改一个 URL,代码不用动;要接受重试和重放,就验证 AnyHook-Signature 请求头,因为 Svix 自己的签名 5 分钟后就会过期。