PayPal WebHook
即paypal异步通知功能,订阅了相应的事件后,当发生了订阅的事件,paypal会post一个json格式的数据到我们配置的地址上。
配置WebHook地址和订阅事件
选择查看应用详情后拉到最下面的Webhook配置,输入通知地址和订阅事件,点击保存后,会生成一个Webhook ID(在验证Webhook信息的时候用到)。
checkout order相关的事件:https://developer.paypal.com/docs/checkout/apm/reference/subscribe-to-webhooks/
所有事件:https://developer.paypal.com/api/rest/webhooks/event-names/
需要响应200状态码表示处理成功,否则会在3天内最多发送25次。
Webhook事件
- CHECKOUT.ORDER.APPROVED 客户授权支付,此时可收款。
- PAYMENT.CAPTURE.COMPLETED 收款完成,即交易完成。
- PAYMENT.CAPTURE.PENDING 交易需要审核。
- PAYMENT.CAPTURE.DENIED 拒绝了收款。
- PAYMENT.CAPTURE.REFUNDED 退款。
- CUSTOMER.DISPUTE.CREATED 客户发起争议,此时也会收到RISK.DISPUTE.CREATED消息,内容一致。
- CUSTOMER.DISPUTE.RESOLVED 争议解决。
接收并验证Webhook消息
为了保证webhook消息的真实性,我们需要对消息进行验证,避免处理假消息。
请注意系统需要判断此事件消息是否处理过,避免重复处理事件。
地址:https://developer.paypal.com/api/rest/webhooks/
需要用到的header:
PAYPAL-TRANSMISSION-ID: Http传输的唯一ID
PAYPAL-TRANSMISSION-TIME:时间
PAYPAL-CERT-URL:Public key地址
PAYPAL-TRANSMISSION-SIG:签名
PAYPAL-AUTH-ALGO:签名算法
验证方式一:调接口获取webhook详情
通过Webhook的消息ID调用接口去获取webhook消息详情
GET https://api.paypal.com/v1/notifications/webhooks-events/{EVENT_ID}
验证方式二:调用验签API
调用paypal的验签API
POST https://api.paypal.com/v1/notifications/verify-webhook-signature
参数
{
/**
* 签名方式,可从header信息的PAYPAL-AUTH-ALGO获取
*/
"auth_algo": string;
/**
* 公钥地址,可从header信息的PAYPAL-CERT-URL获取
*/
"cert_url": string;
/**
* HTTP传输ID,可从header信息的PAYPAL-TRANSMISSION-ID获取
*/
"transmission_id": string;
/**
* 签名,可从header信息的PAYPAL-TRANSMISSION-SIG获取
*/
"transmission_sig": string;
/**
* 时间,可从header信息的PAYPAL-TRANSMISSION-TIME获取
*/
"transmission_time": string;
/**
* Webhook ID,即webhook配置的ID
*/
"webhook_id": string;
/**
* webhook事件
*/
"webhook_event": string;
}
响应
{
/**
* 验证结果
* SUCCESS - 成功
* FAILURE - 失败
*/
"verification_status": string;
}
验证方式三:自己手动验证
验证签名,输入的字符串为:
<transmissionId>|<timeStamp>|<webhookId>|<crc32>
Field | Description |
---|---|
transmissionId | The unique ID of the HTTP transmission from the PAYPAL-TRANSMISSION-ID header. |
timeStamp | The date and time when the HTTP message was transmitted from the PAYPAL-TRANSMISSION-TIME header. |
webhookId | The ID of the webhook resource for the destination URL to which PayPal delivers the event notification. |
crc32 | The Cyclic Redundancy Check (CRC32) checksum for the body of the HTTP payload. |
使用crc32算法对post的数据进行多项式计算。
PHP代码
<?php
$dataUrlString = file_get_contents('php://input');
$headers = self::getAllHeaders();// 获取所有header
$paypalTransmissionId = $headers['PAYPAL-TRANSMISSION-ID'] ?? '';
$timeStamp = $headers['PAYPAL-TRANSMISSION-TIME'] ?? '';
$webhookId = 'WEBHOOK ID';
$crc32 = crc32($dataUrlString);
if (!$paypalTransmissionId || !$timeStamp || !$webhookId || !$crc32) {
return false;
}
$sigString = "{$paypalTransmissionId}|{$timeStamp}|{$webhookId}|{$crc32}";
// 通过PAYPAL-CERT-URL头信息去拿公钥
$publicKey = openssl_pkey_get_public(self::httpCurl($headers['PAYPAL-CERT-URL']));
$details = openssl_pkey_get_details($publicKey);
$verifyResult = openssl_verify($sigString, base64_decode($headers['PAYPAL-TRANSMISSION-SIG']), $details['key'], 'SHA256');
if ($verifyResult === 1) {
// 验证成功
} else {
// 验证失败
}
<?php
public static function getAllHeaders(): array
{
$headers = [];
foreach ($_SERVER as $name => $value) {
if (substr($name, 0, 5) == 'HTTP_') {
$headers[str_replace(' ', '-', str_replace('_', ' ', substr($name, 5)))] = $value;
}
}
return $headers;
}
<?php
public static function httpCurl($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
return $output;
}
感谢作者,非常详细,这里补充一点,如果使用 paypal/Checkout-PHP-SDK 这个新sdk支付的话, webhook的通知是不会带自定义order_no的,需要下单的时候记录paypal返回的order_id
貌似v1 版本的包 paypal/rest-api-sdk-php webhook对应的事件是 PAYMENT.SALE.COMPLETED 里面会携带自定义order_no
再次感谢作者,webhook写的很详细,忍不住吐槽一句,paypal的支付文档像坨狗屎,尤其是 paypal/Checkout-PHP-SDK 几乎没有任何说明文件
我们在最开始的时候使用paypal的SDK,但是后来我们都没有用,自己按照paypal的文档封装了一套逻辑(包括下单,收款,退款,发货等)。
v1接口不推荐使用了,我们用的都是v2接口。
下单的时候,建议传invoice_id给paypal,用你们自己内部的唯一订单ID即可,webhook里面是有返回这个字段的,可用来判断是哪个订单的webhook。
下单成功,建议都保留order_id字段,方便以后查询。
Webhook可能出现延迟,PayPal回复:
所以我们可以在收款的时候,如果返回的状态是completed,则直接进行收款完成操作,webhook也保留处理completed。谁先到就谁先处理,处理时会对此订单加锁,避免重复处理。
延迟到账原因可能是:
– 电子支票付款(ECheck)
– PayPal审核
– 买家账户被限制(此时需要买家联系paypal才能处理,否则一直pending)
感谢~
RISK.DISPUTE.CREATED事件已被CUSTOMER.DISPUTE.CREATED事件替代,所以只需要订阅CUSTOMER.DISPUTE.CREATED事件即可。
每个事件对应PayPal页面的事件:
Checkout order approved:CHECKOUT.ORDER.APPROVED,客户授权支付,此时可收款。
Payment capture completed:PAYMENT.CAPTURE.COMPLETED,收款完成,即交易完成。
Payment capture pending:PAYMENT.CAPTURE.PENDING,交易需要审核。
Payment capture denied:PAYMENT.CAPTURE.DENIED,拒绝了收款。
Payment capture refunded:PAYMENT.CAPTURE.REFUNDED,退款。
Disputes created:CUSTOMER.DISPUTE.CREATED,客户发起争议。
Disputes resolved:CUSTOMER.DISPUTE.RESOLVED,争议解决。
头信息的PAYPAL-AUTH-ALGO是SHA256WithRSA,而PHP 要使用SHA256