Paddle の Webhook 配信の仕組み

本番環境では約 2 日間で最大 60 回の配信試行があり、そのうち約 10 回は最初の 1 時間に行われます。Paddle の ts/h1 署名形式と Node の検証コードを掲載しています。

配信の挙動

プロバイダーの公式ドキュメントから読み取っています(最終確認 2026-08-06)。プロバイダーが数値を示していない項目には、この表も数値を載せていません。

応答期限5 秒

ハンドラーが期限内に応答するか確認する

リトライ本番環境:約 2 日間で最大 60 回試行(最初の 1 時間に約 10 回、初日に約 45 回)。サンドボックス:約 2 分以内に 3 回
打ち切りプロバイダーは未公表
出典https://developer.paddle.com/webhooks/about/respond-to-webhooks

署名

ドキュメントを読んだだけではなく、テストスイートで裏付けた動作する検証コードで確認しています。最終検証日は 2026-08-20 です。

アルゴリズムHMAC-SHA256
署名対象{timestamp}:{body}
エンコードhex
ヘッダーpaddle-signature = ts=X;h1=hex
許容範囲5 秒

検証する

JSON としてパースする前の生のリクエストボディを、バイト単位でそのまま使います。フレームワークがペイロードを再シリアライズした時点で、このページのどの方式も検証に失敗します。

const crypto = require("node:crypto");

// paddle-signature: "ts=1700000000;h1=abc..." — HMAC-SHA256 over "{ts}:{body}"
const pairs = Object.fromEntries(
  req.headers["paddle-signature"].split(";").map((p) => p.split("=")),
);
const expected = crypto
  .createHmac("sha256", secretKey) // per destination: pdl_ntfset_...
  .update(`${pairs.ts}:${rawBody}`)
  .digest("hex");
const received = Buffer.from(pairs.h1 ?? "");
const wanted = Buffer.from(expected);
// timingSafeEqual throws on a length mismatch, so check that first.
const valid =
  received.length === wanted.length && crypto.timingSafeEqual(received, wanted);

Paddle の SDK が適用するタイムスタンプの許容範囲は 5 秒で、ここに挙げた送信元の中で最も厳しい値です。中継サービスやキューを挟む場合は、ワーカーが処理を始めた時刻ではなく、イベントがエッジに到着した時刻とタイムスタンプを比べてください。

落とし穴

Stripe のリトライ間隔がまばらな時期に、Paddle は密に送ってきます。最初の 1 時間に約 10 回、初日に約 45 回、約 2 日間で最大 60 回です。遅いだけのハンドラーは、最初のうち数分おきに再び実行されます。冪等性がなければ、実行のたびに副作用が繰り返されます。

サンドボックスの挙動は本番環境とまったく違い、約 2 分以内に 3 回しか試行しません。テストで問題なく見えるリトライ処理も、本番の 5% の負荷しか経験していないことになります。

あわせて読む

AnyHook は、Paddle から受信するエンドポイントの手前に入ります。約 1 秒で応答し、サーバーがダウンしていれば独自のスケジュールでリトライします。すべてのイベントは再送できる状態で保存されます。 変えるのは URL 1 つだけで、コードはそのままです。ただし Paddle 自身の署名は 5 秒で失効するため、リトライと再送を受け付けるには AnyHook-Signature を検証してください。

仕組みを見る →