傳遞行為
取自服務商自己的文件,最後檢查於 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 秒就回應,伺服器當機時依自己的排程重試,並讓每個事件都能重播。 換一個網址,程式碼不用動。