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
时间容差无强制要求

验证签名

要用原始请求体,逐字节一致,并且在任何 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,否则攻击者只要托管一个自己的证书就行了。

容易踩的坑

这是本页唯一一种验证时可能需要发起网络请求的方案。每个事件都带着五个签名请求头,待签名串里嵌入的是请求体的 CRC32,而不是请求体本身。

延伸阅读

AnyHook 挡在接收 PayPal Webhook 的端点前面:约 1 秒应答,你的服务器宕机时按自己的计划重试,并且每个事件都可以重放。 只改一个 URL,代码不用动。

工作原理 →