Liqpay checkout API 如何处理支付结果回跳
明确结论
不要通过浏览器回跳 URL 判断支付结果。LiqPay 的 result_url(旧版资料中可能称为 return_url)只负责把用户带回商户页面,不能作为可信的支付通知渠道。
正确的处理流程是:
- 创建支付时设置服务器通知地址
server_url。 - 通过
server_url接收 LiqPay 的 POST 回调。 - 使用商户私钥验证回调中的
signature。 - 解码
data,核对order_id、金额、币种和status。 - 如果回调没有到达,或者需要主动确认,再调用 LiqPay 的
statusAPI 查询。 - 回跳页面只显示“正在确认支付”,并从商户服务器读取订单状态。
为什么不能根据回跳地址判断支付结果
浏览器回跳本身存在几个问题:
- 用户可能直接关闭页面,回跳不会发生。
- 回跳 URL 可以被手工访问或伪造。
- 即使支付已经成功,银行侧状态也可能仍在异步确认。
- 用户取消支付和支付失败可能返回同一个页面。
- 即使 URL 中带有状态参数,未经签名验证也不能信任。
支付结果必须由 LiqPay 发给商户服务器的签名回调确认,不能由浏览器通知服务器“支付成功”。
推荐的整体流程
用户点击订阅
↓
商户服务器创建 order_id,并将订单保存为 pending
↓
用户进入 LiqPay Checkout
↓
LiqPay 向 server_url 发送签名回调
↓
商户服务器验证签名和订单数据,更新本地订单
↓
用户跳转到 result_url
↓
结果页从商户服务器查询订单的最终状态
创建 Checkout 参数时,应同时传入类似下面的地址:
$params = [
'version' => '3',
'public_key' => $publicKey,
'action' => 'subscribe',
'amount' => '10.00',
'currency' => 'USD',
'description'=> 'Monthly subscription',
'order_id' => $orderId,
'server_url' => 'https://example.com/payments/liqpay/callback',
'result_url' => 'https://example.com/payments/result?order_id='
. rawurlencode($orderId),
];
参数名称应以当前使用的 LiqPay Checkout API 文档为准。如果 SDK 或旧版文档使用 return_url,处理原则不变:该地址只负责前端跳转。
接收并验证服务器回调
LiqPay 通常通过表单 POST 发送两个字段:
data:Base64 编码的支付结果 JSONsignature:用于验证data是否被修改的签名
签名通常按以下规则计算:
base64_encode(sha1(private_key + data + private_key, binary))
PHP 示例:
<?php
$privateKey = getenv('LIQPAY_PRIVATE_KEY');
$data = $_POST['data'] ?? '';
$receivedSignature = $_POST['signature'] ?? '';
if ($data === '' || $receivedSignature === '') {
http_response_code(400);
exit('Missing callback fields');
}
$expectedSignature = base64_encode(
sha1($privateKey . $data . $privateKey, true)
);
if (!hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(403);
exit('Invalid signature');
}
$decoded = base64_decode($data, true);
if ($decoded === false) {
http_response_code(400);
exit('Invalid data encoding');
}
$payload = json_decode($decoded, true);
if (!is_array($payload)) {
http_response_code(400);
exit('Invalid JSON');
}
$orderId = (string) ($payload['order_id'] ?? '');
$status = (string) ($payload['status'] ?? '');
$amount = (string) ($payload['amount'] ?? '');
$currency = (string) ($payload['currency'] ?? '');
// 必须从数据库读取原订单,不能直接信任回调中的业务数据。
$order = findOrderByOrderId($orderId);
if ($order === null) {
http_response_code(404);
exit('Unknown order');
}
if (
bccomp($amount, $order['amount'], 2) !== 0 ||
$currency !== $order['currency']
) {
http_response_code(409);
exit('Payment details mismatch');
}
switch ($status) {
case 'success':
markOrderPaidOnce($orderId, $payload);
break;
case 'failure':
case 'error':
markOrderFailed($orderId, $payload);
break;
case 'reversed':
markOrderReversed($orderId, $payload);
break;
default:
// waiting、processing 等非终态,以及当前 API 可能返回的其他状态。
markOrderPending($orderId, $status, $payload);
break;
}
http_response_code(200);
echo 'OK';
success、failure、error、reversed 是常见状态示例,但完整的状态集合可能因支付动作和 LiqPay API 版本而异。生产代码应根据当前官方状态表建立映射。遇到未知状态时,一律按“尚未确认”处理,不能默认支付成功。
处理订阅支付时,还要分别记录:
- 首次扣款结果;
- 订阅是否已经激活;
- 后续周期扣款;
- 订阅取消;
- 成功交易之后发生的退款或撤销。
这些状态不能全部压缩成一个布尔值。
主动调用状态查询 API
如果服务器回调延迟、丢失,或者需要对账,可以通过 LiqPay API 主动查询订单状态。常见做法是向以下端点发送签名请求:
POST https://www.liqpay.ua/api/request
请求内容包括 action=status、version、public_key 和 order_id。PHP 示例:
<?php
function liqpaySignature(string $privateKey, string $data): string
{
return base64_encode(
sha1($privateKey . $data . $privateKey, true)
);
}
$params = [
'action' => 'status',
'version' => '3',
'public_key' => $publicKey,
'order_id' => $orderId,
];
$data = base64_encode(
json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)
);
$signature = liqpaySignature($privateKey, $data);
$ch = curl_init('https://www.liqpay.ua/api/request');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
'data' => $data,
'signature' => $signature,
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
]);
$responseBody = curl_exec($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($responseBody === false || $httpStatus < 200 || $httpStatus >= 300) {
throw new RuntimeException(
'LiqPay status request failed: ' . $error
);
}
$result = json_decode($responseBody, true);
if (!is_array($result)) {
throw new RuntimeException('Invalid LiqPay response');
}
$status = (string) ($result['status'] ?? '');
具体端点、API 版本和响应字段应以当前 LiqPay 文档及所用 SDK 为准。状态查询是服务器回调的补偿机制,不应在结果页每次刷新时无限调用 LiqPay。
回跳页面应该如何展示
result_url 页面可以携带自己的 order_id,但这个值不能直接作为支付证明。页面应查询商户服务器中保存的订单状态:
async function waitForPayment(orderId) {
for (let attempt = 0; attempt < 10; attempt += 1) {
const response = await fetch(
`/api/orders/${encodeURIComponent(orderId)}/payment-status`,
{ credentials: 'same-origin' }
);
const result = await response.json();
if (result.status === 'paid') {
return showSuccess();
}
if (['failed', 'cancelled', 'reversed'].includes(result.status)) {
return showFailure(result.status);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
showPending(
'支付结果仍在确认中,请稍后在订单页面查看。'
);
}
浏览器回跳可能早于服务器通知。此时短暂显示“正在确认”是正常的。不能因为用户已经返回站点,就把订单标记为已付款或已取消。
如何识别“用户取消”
是否有独立的取消状态,以及该状态的具体名称,应以当前 LiqPay Checkout 状态定义为准。实际处理时需要注意:
- 收到明确的终态失败或取消状态后,可以更新本地订单。
- 如果只有浏览器回跳而没有签名回调,不能认定用户已经取消。
- 用户关闭 Checkout 页面后,可能不会发生可用于业务确认的回跳。
- 订单长时间停留在非终态时,可以通过状态 API 查询,并按业务规则将其标记为过期。
- 订单过期属于商户自己的业务状态,不一定等同于 LiqPay 返回的取消状态。
实现时需要注意的事项
private_key只能保存在服务器端,不能出现在 HTML 或 JavaScript 中。- 每次支付都应使用唯一且不可预测的
order_id。 - 回调处理必须支持幂等,因为同一笔交易的通知可能重复到达。
- 发放会员、开通订阅等操作必须采用原子更新,确保只执行一次。
- 除了验证签名,还要核对
order_id、金额、币种、商户公钥以及适用的交易标识。 - 保存原始回调和状态变更记录,以便处理退款、撤销和对账。
- 不能只处理
success。支付成功后仍可能发生退款、撤销或订阅状态变化。 server_url必须能从公网访问,并使用有效的 HTTPS 证书。- 回调接口应尽快返回成功响应。耗时业务可以交给队列异步执行。
- 状态查询失败或遇到未知状态时,应保持
pending并等待重试,不能猜测支付结果。
原则很简单:浏览器回跳用于处理用户体验,签名服务器回调和服务端状态查询用于确认资金状态。
备注:内容仅供参考。
