案例 PayPal IPN 退役迁移 REST Webhooks:四种丢单都返回 HTTP 200

2026-09-30 21:01:43

PayPal IPN 退役迁移 REST Webhooks:四种丢单都返回 HTTP 200

PayPal 正在淘汰传统即时支付通知 IPN 与 Website Payments Standard(WPS)。时间线是:

  • 2025 年底:停止发放新的 IPN / WPS 凭据
  • 2026 年 1 月:Website Payments Standard 标记为 deprecated
  • 2027 年 1 月:彻底停用(EOL)

替代方案是 REST Webhooks。

真正危险的不是 2027-01 这个截止日,而是迁移窗口期。IPN → Webhooks 不是改个 endpoint URL,而是四层同时变化。每一层都有一种失败模式会对 PayPal 返回 HTTP 200,于是 PayPal 停止重试,而你的系统静默地从未记录这笔款。

四层变化对照

面IPN(旧)Webhooks(REST)
内容类型application/x-www-form-urlencodedapplication/json
负载结构扁平 key/value嵌套 resource 对象
金额字段mc_gross="19.95"resource.amount.value + resource.amount.currency_code
状态字段payment_status="Completed"event_type 与 resource.status="COMPLETED"
买家邮箱payer_email(顶层)capture 资源里没有对应字段
自定义引用customcustom_id
交易号txn_idresource.id(capture id)/ id(event id,形如 WH-…)
验签POST 回传 cmd=_notify-validate,期待 VERIFIED对原始 body + PAYPAL-TRANSMISSION-* 头 + 证书链做 RSA-SHA256 验签

失败模式 1:用 urlencoded 解析器收 JSON

旧代码里的 urlencoded 解析器遇到 JSON body,所有字段变 undefined;handler 照样返回 200,PayPal 视为已投递并停止重试,订单永远停在「未支付」。日志里没有任何报错,等发现时早已过了重试窗口,只能去 PayPal 后台手动重放。

失败模式 2:继续读 mc_gross

新结构里金额在 resource.amount.value,而且它仍然是字符串,不是数字。custom 变成 custom_id。payer_email 在 capture 资源里根本不存在,需要二次 order 查询才能拿到。

失败模式 3:只订阅 COMPLETED

状态词从 Completed 变成 COMPLETED,大小写不同。更关键的是,退款是独立事件类型 PAYMENT.CAPTURE.REFUNDED,拒付/争议也是独立事件。只订阅 COMPLETED,退款和争议永远不会到达你的系统。

失败模式 4:验签方式整体换掉

IPN 是回调式验签:把原样 payload 用 cmd=_notify-validate POST 回 PayPal,只有回 VERIFIED 才可信。

Webhooks 改成离线 RSA-SHA256:用原始请求 body、PAYPAL-TRANSMISSION-ID、PAYPAL-TRANSMISSION-TIME、你的 webhook id,以及 body 的 CRC32 重建签名串,再拿 PAYPAL-TRANSMISSION-SIG 对 PAYPAL-CERT-URL 的公钥验签;或者调用 /v1/notifications/verify-webhook-signature 并检查 verification_status === "SUCCESS"。

弃用期更隐蔽的坑:_notify-validate 端点还没消失

弃用期间 _notify-validate 端点还在,但把 JSON webhook body 回传给它不会返回 VERIFIED。保留旧回环的团队会看到每个 webhook 都验签失败 → 返回 4xx → PayPal 重试后放弃 → 丢单,而且这看起来像「安全检查在正常工作」。

反过来,如果验签写错,比如留了一条「没签名就放过」的开发分支,任何知道 webhook URL 的人都能手写一个 PAYMENT.CAPTURE.COMPLETED,带上任意 custom_id 和 amount,不带签名头,直接走到 fulfillOrder。这种伪造请求同样返回 200,日志里看不到任何错误。

自检

在代码库里搜:

grep -rn "payment_status\|mc_gross\|payer_email\|_notify-validate" .

命中就说明还是 IPN 时代的代码。金额要显式转成数字再比较。

事件名与字段定义见 PayPal 官方事件名文档。

复制全文 生成海报 支付 PayPal Webhook

推荐文章

程序员茄子在线接单