Svix (Clerk, Resend) 如何投递 Webhook

约一天内尝试 8 次,用尽后发出通知,连续失败 5 天后停用。Clerk 和 Resend 的 Webhook 也由 Svix 签名;附签名格式和验证器。

投递行为

取自该平台的官方文档,最近核对于 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 分钟后就会过期。

工作原理 →