PayPal 如何傳遞 Webhook

PayPal 用 RSA-SHA256 簽署請求主體的 CRC32,並連到一張託管的憑證。說明為什麼走驗證 API 才合理,並附手動驗證的格式。

傳遞行為

取自服務商自己的文件,最後檢查於 2026-08-06。服務商沒寫數字的地方,這張表也不填。

回應時限服務商未公開
重試服務商未公開
放棄服務商未公開

簽章

用一個經測試套件證明可運作的驗證程式核對過,不是照文件抄來的。最後驗證於 2026-08-20。

演算法RSA-SHA256
簽章內容{txId}|{txTime}|{webhookId}|{crc32(body)}
編碼base64
標頭paypal-transmission-sig + 4 more
容許時間差不強制

驗證簽章

要用原始請求主體(raw body),逐位元組原封不動,並在任何 JSON 解析之前取得。框架只要重新序列化 payload,這頁上的每一種機制就會立刻失效。

// 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,否則攻擊者只要自己架一張憑證就行了。

容易踩的坑

這是這頁上唯一一種驗證時可能需要網路呼叫的機制。每個事件都帶著五個簽章標頭,而簽章內容嵌入的是請求主體的 CRC32,而不是主體本身。

延伸閱讀

AnyHook 架在接收 PayPal Webhook 的端點前面:大約 1 秒就回應,伺服器當機時依自己的排程重試,並讓每個事件都能重播。 換一個網址,程式碼不用動。

運作方式 →