PayPal の Webhook 配信の仕組み

PayPal はボディの CRC32 を含むデータに RSA-SHA256 で署名し、PayPal がホストする証明書に連なります。検証 API を使うのが堅実な理由と、手動検証の構成を解説します。

配信の挙動

プロバイダーの公式ドキュメントから読み取っています(最終確認 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 つだけで、コードはそのままです。

仕組みを見る →