配信の挙動
プロバイダーの公式ドキュメントから読み取っています(最終確認 2026-08-06)。プロバイダーが数値を示していない項目には、この表も数値を載せていません。
| 応答期限 | プロバイダーは未公表 |
|---|---|
| リトライ | プロバイダーは未公表 |
| 打ち切り | プロバイダーは未公表 |
署名
ドキュメントを読んだだけではなく、テストスイートで裏付けた動作する検証コードで確認しています。最終検証日は 2026-08-20 です。
| アルゴリズム | RSA-SHA256 |
|---|---|
| 署名対象 | {txId}|{txTime}|{webhookId}|{crc32(body)} |
| エンコード | base64 |
| ヘッダー | paypal-transmission-sig + 4 more |
| 許容範囲 | 適用なし |
検証する
JSON としてパースする前の生のリクエストボディを、バイト単位でそのまま使います。フレームワークがペイロードを再シリアライズした時点で、このページのどの方式も検証に失敗します。
// Manual verification means: fetch the cert from paypal-cert-url, extract
// its public key, then RSA-SHA256-verify paypal-transmission-sig against
// "{transmission-id}|{transmission-time}|{webhook-id}|{crc32(rawBody)}"
// Most integrations skip that and call the verify endpoint instead:
const res = await fetch(
"https://api-m.paypal.com/v1/notifications/verify-webhook-signature",
{
method: "POST",
headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
body: JSON.stringify({
transmission_id: req.headers["paypal-transmission-id"],
transmission_time: req.headers["paypal-transmission-time"],
cert_url: req.headers["paypal-cert-url"],
auth_algo: req.headers["paypal-auth-algo"],
transmission_sig: req.headers["paypal-transmission-sig"],
webhook_id: webhookId, // yours, from the developer dashboard
webhook_event: JSON.parse(rawBody),
}),
},
);
const valid = (await res.json()).verification_status === "SUCCESS";手動で検証する場合は、cert_url を取得する前に、それが paypal.com を指していることを確認してください。確認しなければ、攻撃者は自前の証明書をホストするだけで済みます。
落とし穴
このページで、検証にネットワーク呼び出しが必要になりうる方式はこれだけです。5 つの署名ヘッダーが各イベントに付き、署名対象にはボディそのものではなく、ボディの CRC32 が埋め込まれます。
あわせて読む
AnyHook は、PayPal から受信するエンドポイントの手前に入ります。約 1 秒で応答し、サーバーがダウンしていれば独自のスケジュールでリトライします。すべてのイベントは再送できる状態で保存されます。 変えるのは URL 1 つだけで、コードはそのままです。