Svix (Clerk, Resend) 如何傳遞 Webhook

大約一天內嘗試 8 次,用盡時發出通知,連續失敗 5 天後停用。Clerk 和 Resend 也由 Svix 簽章;附格式與驗證程式。

傳遞行為

取自服務商自己的文件,最後檢查於 2026-08-06。服務商沒寫數字的地方,這張表也不填。

回應時限服務商未公開
重試嘗試 8 次:立即、5 秒、5 分鐘、30 分鐘、2 小時、5 小時、10 小時、10 小時。之後發出 message.attempt.exhausted 的 Operational Webhook
放棄所有嘗試持續失敗 5 天後,停用端點
出處https://docs.svix.com/retries

簽章

用一個經測試套件證明可運作的驗證程式核對過,不是照文件抄來的。最後驗證於 2026-08-20。

演算法HMAC-SHA256
簽章內容{id}.{timestamp}.{body}
編碼base64
標頭svix-signature = v1,base64
容許時間差5 分鐘

驗證簽章

要用原始請求主體(raw body),逐位元組原封不動,並在任何 JSON 解析之前取得。框架只要重新序列化 payload,這頁上的每一種機制就會立刻失效。

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 這個 Operational Webhook 通知你它已放棄。全部失敗持續五天,端點就會被停用。

延伸閱讀

AnyHook 架在接收 Svix Webhook 的端點前面:大約 1 秒就回應,伺服器當機時依自己的排程重試,並讓每個事件都能重播。 換一個網址,程式碼不用動;要接受重試與重播,改為檢查 AnyHook-Signature,因為 Svix 自己的簽章在 5 分鐘後就會過期。

運作方式 →