投递行为
取自该平台的官方文档,最近核对于 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,代码不用动。