配信の挙動
プロバイダーの公式ドキュメントから読み取っています(最終確認 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 分 |
検証する
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 分のタイムスタンプ許容範囲も SDK が適用します。HMAC の鍵にする前に whsec_ プレフィックスを取り除き、残りを base64 デコードします。この手順を飛ばすのが典型的な間違いです。
落とし穴
Clerk と Resend の Webhook は中身が Svix なので、1 つの検証コードで 3 つすべてに対応できます。ヘッダーも形式も同じで、違うのはシークレットを管理するダッシュボードだけです。
リトライ間隔は 8 回の試行(即時、5 秒、5 分、30 分、2 時間、5 時間、10 時間、10 時間)で、その後 message.attempt.exhausted の Operational Webhook で打ち切りが通知されます。5 日間すべてが失敗し続けると、エンドポイントは無効化されます。
あわせて読む
AnyHook は、Svix から受信するエンドポイントの手前に入ります。約 1 秒で応答し、サーバーがダウンしていれば独自のスケジュールでリトライします。すべてのイベントは再送できる状態で保存されます。 変えるのは URL 1 つだけで、コードはそのままです。ただし Svix 自身の署名は 5 分で失効するため、リトライと再送を受け付けるには AnyHook-Signature を検証してください。