Svix (Clerk, Resend) の Webhook 配信の仕組み

約 1 日で 8 回試行し、使い切ると通知を送り、5 日間失敗が続くと無効化します。Svix は Clerk と Resend の署名も担っています。署名形式と検証コードを掲載しています。

配信の挙動

プロバイダーの公式ドキュメントから読み取っています(最終確認 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 を検証してください。

仕組みを見る →