API接入文档

聚合支付网关 - 商户接入指南

概述

本平台提供统一的支付API接口,商户只需对接一套API即可使用多个支付渠道。

支付流程

商户后端 --> 平台 API --> 支付渠道 --> 订单状态变化 --> 渠道通知平台 --> 平台通知商户

接入流程

  1. 管理员开通商户账号,分配 API Key 和 API Secret
  2. 管理员为商户开通可用的支付渠道
  3. 商户登录商户后台,填写各渠道的商户号、密钥等配置信息
  4. 商户在后台设置汇率(如需人民币结算)和IP白名单(推荐)
  5. 商户按本文档对接API,完成联调测试

基础信息

项目说明
API地址
请求方式POST(Content-Type: application/x-www-form-urlencoded
字符编码UTF-8
签名算法MD5
支持币种HKD(港币)、CNY(人民币),默认 HKD
返回格式JSON(code=0 表示成功,其他表示失败)

返回格式说明

所有API接口返回统一的JSON格式:

// 成功
{ "code": 0, "msg": "success", "data": { ... } }

// 失败
{ "code": -1, "msg": "错误信息" }
重要:所有平台 API 调用(下单、查询、退款)必须从商户后端服务器发起。请勿在前端(浏览器/App)中直接调用平台 API,原因:
1. API 签名需要 Secret,前端调用会暴露 Secret
2. IP 白名单仅允许固定服务器 IP 访问,前端用户 IP 无法通过白名单校验

签名认证

所有API请求都必须携带签名参数,用于验证请求的合法性和完整性。

签名步骤

  1. 将所有请求参数(除 sign 外)按参数名 ASCII 码升序排列
  2. 将参数按 key=value 格式用 & 拼接(空值不参与签名)
  3. 在末尾拼接 &secret=你的API_Secret
  4. 对拼接字符串做 MD5(32位小写)得到签名值

签名过程示例

假设参数为:api_key=abc123, timestamp=1709284800, channel=kpay, amount=100.00,Secret 为 my_secret_key

// 1. 按key排序: amount, api_key, channel, timestamp
// 2. 拼接: amount=100.00&api_key=abc123&channel=kpay×tamp=1709284800
// 3. 追加secret: amount=100.00&api_key=abc123&channel=kpay×tamp=1709284800&secret=my_secret_key
// 4. MD5 => "e10adc3949ba59abbe56e057f20f883e"

签名 + 下单完整示例

PHP Python Java Node.js
<?php
$api_key    = '你的API_KEY';
$api_secret = '你的API_SECRET';

$params = [
    'api_key'    => $api_key,
    'timestamp'  => time(),
    'channel'    => 'kpay',
    'order_no'   => 'ORD' . date('YmdHis') . rand(1000, 9999),
    'amount'     => '100.00',
    'currency'   => 'HKD',
    'pay_method' => 'alipay_scan',
    'subject'    => '商品购买',
    'notify_url' => 'https://www.example.com/notify.php',
    'return_url' => 'https://www.example.com/return.php',
];

// 签名
ksort($params);
$sign_str = '';
foreach ($params as $k => $v) {
    if ($v !== '') $sign_str .= "$k=$v&";
}
$sign_str .= "secret=$api_secret";
$params['sign'] = md5($sign_str);

// 发送请求
$ch = curl_init('https://平台地址/api/pay.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);

if ($result['code'] === 0) {
    $pay_url = $result['data']['pay_url'];
    // 将 pay_url 返回给前端处理(生成二维码或跳转)
}
import hashlib, time, random, requests

API_KEY    = "你的API_KEY"
API_SECRET = "你的API_SECRET"
API_URL    = "https://平台地址/api/"

def make_sign(params, secret):
    filtered = {k: v for k, v in params.items() if v != '' and k != 'sign'}
    sign_str = '&'.join(f'{k}={v}' for k, v in sorted(filtered.items()))
    sign_str += f'&secret={secret}'
    return hashlib.md5(sign_str.encode()).hexdigest()

params = {
    'api_key': API_KEY,
    'timestamp': str(int(time.time())),
    'channel': 'kpay',
    'order_no': f'ORD{int(time.time())}{random.randint(1000,9999)}',
    'amount': '100.00',
    'currency': 'HKD',
    'pay_method': 'alipay_scan',
    'subject': '商品购买',
    'notify_url': 'https://www.example.com/notify',
    'return_url': 'https://www.example.com/return',
}
params['sign'] = make_sign(params, API_SECRET)

resp = requests.post(API_URL + 'pay.php', data=params, verify=False)
result = resp.json()

if result['code'] == 0:
    pay_url = result['data']['pay_url']
    # 将 pay_url 返回给前端
import java.security.MessageDigest;
import java.util.*;
import java.net.http.*;
import java.net.URI;

public class PayClient {

    static String API_KEY    = "你的API_KEY";
    static String API_SECRET = "你的API_SECRET";
    static String API_URL    = "https://平台地址/api/";

    public static String sign(Map<String, String> params, String secret) {
        TreeMap<String, String> sorted = new TreeMap<>(params);
        sorted.remove("sign");
        StringBuilder sb = new StringBuilder();
        for (var e : sorted.entrySet()) {
            if (e.getValue() != null && !e.getValue().isEmpty())
                sb.append(e.getKey()).append("=").append(e.getValue()).append("&");
        }
        sb.append("secret=").append(secret);
        return md5(sb.toString());
    }

    static String md5(String input) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] hash = md.digest(input.getBytes("UTF-8"));
            StringBuilder hex = new StringBuilder();
            for (byte b : hash) hex.append(String.format("%02x", b));
            return hex.toString();
        } catch (Exception e) { throw new RuntimeException(e); }
    }

    public static void main(String[] args) throws Exception {
        Map<String, String> params = new HashMap<>();
        params.put("api_key", API_KEY);
        params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
        params.put("channel", "kpay");
        params.put("order_no", "ORD" + System.currentTimeMillis());
        params.put("amount", "100.00");
        params.put("currency", "HKD");
        params.put("pay_method", "alipay_scan");
        params.put("subject", "商品购买");
        params.put("notify_url", "https://www.example.com/notify");
        params.put("sign", sign(params, API_SECRET));

        // 拼接 form body 并发送 POST
        StringBuilder body = new StringBuilder();
        params.forEach((k, v) -> {
            if (body.length() > 0) body.append("&");
            body.append(k).append("=").append(java.net.URLEncoder.encode(v, java.nio.charset.StandardCharsets.UTF_8));
        });
        HttpRequest req = HttpRequest.newBuilder()
            .uri(URI.create(API_URL + "pay.php"))
            .header("Content-Type", "application/x-www-form-urlencoded")
            .POST(HttpRequest.BodyPublishers.ofString(body.toString()))
            .build();
        HttpResponse<String> resp = HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
        System.out.println(resp.body());
    }
}
const crypto = require('crypto');
const https = require('https');
const querystring = require('querystring');

const API_KEY    = '你的API_KEY';
const API_SECRET = '你的API_SECRET';
const API_URL    = 'https://平台地址/api/';

function makeSign(params, secret) {
    const keys = Object.keys(params).filter(k => k !== 'sign' && params[k] !== '').sort();
    let str = keys.map(k => k + '=' + params[k]).join('&');
    str += '&secret=' + secret;
    return crypto.createHash('md5').update(str).digest('hex');
}

const params = {
    api_key: API_KEY,
    timestamp: String(Math.floor(Date.now() / 1000)),
    channel: 'kpay',
    order_no: 'ORD' + Date.now() + Math.floor(Math.random() * 10000),
    amount: '100.00',
    currency: 'HKD',
    pay_method: 'alipay_scan',
    subject: '商品购买',
    notify_url: 'https://www.example.com/notify',
    return_url: 'https://www.example.com/return',
};
params.sign = makeSign(params, API_SECRET);

const postData = querystring.stringify(params);
const url = new URL(API_URL + 'pay.php');
const req = https.request({
    hostname: url.hostname, path: url.pathname,
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
}, (res) => {
    let body = '';
    res.on('data', d => body += d);
    res.on('end', () => {
        const result = JSON.parse(body);
        if (result.code === 0) {
            console.log('pay_url:', result.data.pay_url);
        }
    });
});
req.write(postData);
req.end();
timestamp 与服务器时间差不能超过 5分钟,否则请求会被拒绝。请确保服务器时间准确。

安全机制

后端调用

所有平台 API 必须从商户后端服务器调用,严禁在前端(浏览器/客户端)直接调用。
前端调用会导致 API Secret 泄露,且无法通过 IP 白名单校验。
正确架构:商户前端 → 商户后端 → 平台 API

IP白名单

商户可在商户后台「API密钥」页面设置IP白名单。设置后,只有白名单中的IP才能调用API。

配置项说明
格式多个IP用英文逗号分隔,如 1.2.3.4,5.6.7.8
留空不限制,所有IP均可访问
生效范围统一下单、订单查询、退款等所有API接口
强烈建议生产环境设置IP白名单,将商户后端服务器的出口IP加入白名单。
如果返回 IP不在白名单中: x.x.x.x 错误,请将提示中的IP地址加入白名单。

频率限制

维度默认值说明
每分钟60次超出返回 请求过于频繁
每天5000次超出返回 已达每日调用上限
频率限制由管理员设置,如需调整请联系管理员。

请求有效期

timestamp(Unix 秒级时间戳)与服务器时间差不能超过 5分钟,否则返回 请求已过期。建议配置 NTP 时间同步。

HTTPS

所有API请求必须使用 HTTPS 协议,不支持 HTTP 明文传输。

POST 统一下单

/api/pay.php

创建支付订单,返回支付链接。商户后端将链接返回给前端,由前端展示(生成二维码或跳转)。

请求参数

参数必填类型说明
api_keystring商户 API Key
timestampint当前 Unix 时间戳(秒)
signstring签名(见签名认证
channelstring渠道编码,如 kpaywondershengpay。留空则自动在所有已配置渠道中轮询选择
order_nostring商户订单号(同一商户下不可重复,建议 6~32 位字母数字)
amountstring订单金额(保留2位小数,如 100.00
currencystring币种,支持 HKD(港币)和 CNY(人民币),默认 HKD
pay_methodstring支付方式,详见下方支付方式表
subjectstring订单描述(将展示在支付页面)
notify_urlstring异步通知地址(订单状态变化时平台向此地址发送POST通知,包括支付成功、失败、退款、关闭等)
return_url建议string支付完成后用户跳转地址,H5 和卡类支付时强烈建议传入
payment_institutionstring支付宝机构,默认 ALIPAYCN,可选 ALIPAYHK
密钥轮询:商户可为同一渠道配置多套密钥,系统自动采用最少使用(Least-Used)策略轮询选择。
自动选渠道:channel 参数留空时,系统会在商户所有已开通且支持该支付方式的渠道中自动轮询选择。
币种与汇率:上游渠道仅支持 HKD。传入 CNY 等非HKD币种时,平台会按支付类型(微信/支付宝/收银台)单独配置的汇率自动换算为HKD后提交给上游。例如汇率设为 0.91,则 100 HKD = 91 CNY。订单详情中会记录原始金额、原始币种和使用的汇率。请在商户后台「渠道配置」中按支付类型分别设置汇率。

请求示例

POST /api/pay.php HTTP/1.1
Content-Type: application/x-www-form-urlencoded

api_key=4f5d22c3dffccf067a377871f06e350a
×tamp=1709284800
&channel=kpay
&order_no=ORD20260301001
&amount=100.00
¤cy=HKD
&pay_method=alipay_scan
&subject=商品购买
¬ify_url=https://www.example.com/notify.php
&return_url=https://www.example.com/return.php
&sign=e10adc3949ba59abbe56e057f20f883e

成功返回

{
    "code": 0,
    "msg": "success",
    "data": {
        "order_no": "20260301143000123456",   // 平台订单号(用于后续查询/退款)
        "pay_url": "https://..."              // 支付链接
    }
}

失败返回

{ "code": -1, "msg": "签名错误", "data": null }

pay_url 使用方式

微信扫码(wechat_scan):pay_url 为微信协议链接(weixin://wxpay/...),必须生成二维码供用户用微信扫一扫,不能直接跳转。
微信H5(wechat_h5):pay_url 为微信支付直链,必须在微信内置浏览器中打开。
支付宝扫码(alipay_scan):pay_url 为支付宝链接,建议生成二维码供用户扫描使用,浏览器直接跳转不保证能百分百唤醒支付宝App。
支付宝H5(alipay_h5):pay_url 为支付宝收银台链接,可以生成二维码或在支付宝内打开此链接。
收银台支付(cashier):pay_url 为收银台页面链接,浏览器跳转后用户选择支付方式完成支付(KPay卡类支付 + Wonder收银台均统一为此方式)。
收银台支付(cashier):pay_url 为 Wonder 收银台页面链接,浏览器跳转后用户自行选择支付方式。

前端集成指南

商户后端下单成功后,将 pay_url 返回给前端,前端根据支付方式做不同处理。

1. 微信扫码(wechat_scan)— 生成二维码

返回的 pay_url 是微信支付协议链接(形如 weixin://wxpay/bizpayurl?pr=xxx),不能直接在浏览器打开
必须将此链接生成为二维码图片,让用户使用微信扫一扫付款。
// 推荐使用 qrcode.js 库生成二维码
// CDN: https://cdn.jsdelivr.net/npm/qrcodejs/qrcode.min.js

var qr_url = res.data.pay_url;
new QRCode(document.getElementById('qrcode-container'), {
    text: qr_url,
    width: 256,
    height: 256
});
// 展示二维码后,前端轮询自己的后端接口查询订单状态
// 当后端返回已支付时自动跳转成功页面

2. 微信H5(wechat_h5)— 微信内打开

返回的 pay_url 是微信 H5 支付链接,必须在微信内置浏览器中打开才能唤起支付。
在普通手机浏览器中打开无法完成支付。
window.location.href = res.data.pay_url;
// 建议下单时传入 return_url,支付完成后自动跳回

3. 支付宝扫码(alipay_scan)— 建议生成二维码

返回的 pay_url 是支付宝标准链接。建议生成二维码供用户使用支付宝扫描付款。
浏览器直接跳转此链接不保证能百分百唤醒支付宝App(受系统、浏览器版本等影响)。
// 推荐方式:生成二维码供用户扫描
new QRCode(document.getElementById('qrcode-container'), {
    text: res.data.pay_url,
    width: 256, height: 256
});
// 提示用户"请打开支付宝扫一扫付款"

4. 支付宝H5(alipay_h5)— 二维码或支付宝内打开

返回的 pay_url 是拼装好的支付宝收银台链接。
可以生成二维码供用户扫描,也可以在支付宝内置浏览器中直接打开此链接完成支付。
// 方式一:生成二维码
new QRCode(document.getElementById('qrcode-container'), {
    text: res.data.pay_url,
    width: 256, height: 256
});

// 方式二:支付宝内直接打开
window.location.href = res.data.pay_url;

5. 收银台支付(cashier)— 跳转收银台

返回的 pay_url 是收银台页面链接。
直接跳转即可,用户在收银台输入银行卡号、有效期、CVV 完成支付。
window.location.href = res.data.pay_url;
// 支付完成后会跳转回 return_url

6. 收银台支付(cashier)— 跳转Wonder收银台

返回的 pay_url 是 Wonder 收银台页面链接。
用户在收银台自行选择微信、支付宝、银行卡等支付方式完成支付。适用于不需要指定支付方式的场景。
window.location.href = res.data.pay_url;
// 支付完成后会跳转回 return_url

各渠道支持的支付方式

渠道pay_method名称说明
kpaywechat_scan微信扫码返回微信协议链接,须生成二维码
wechat_h5微信H5返回微信支付直链,须在微信内打开
alipay_scan支付宝扫码建议生成二维码供用户扫描
alipay_h5支付宝H5可生成二维码或支付宝内打开链接
cashier收银台支付跳转至收银台页面完成支付(统一KPay卡类与Wonder收银台)
wonderwechat_scan微信扫码返回微信协议链接(weixin://),须生成二维码
wechat_h5微信H5微信内打开直接支付,非微信跳转支付页面
alipay_scan支付宝扫码返回支付宝二维码链接,须生成二维码
alipay_h5支付宝H5返回支付宝H5直接支付链接
cashier收银台支付跳转至Wonder收银台,用户自选支付方式
shengpaywechat_scan微信扫码返回微信Native支付链接,须生成二维码
wechat_h5微信H5返回微信H5支付链接,手机浏览器跳转支付
alipay_scan支付宝扫码返回支付宝二维码链接,须生成二维码
alipay_h5支付宝H5返回支付宝H5支付链接,手机浏览器跳转支付
alipay_pc支付宝PC返回支付宝电脑网站支付链接,PC浏览器跳转支付
实际可用支付方式以管理员分配和渠道方配置为准。可在商户后台的「渠道配置」页面查看已开通渠道支持的完整列表。

POST GET 订单查询

/api/query.php

查询订单当前状态。支持通过平台订单号或商户订单号查询。

请求参数

参数必填类型说明
api_keystring商户 API Key
timestampint当前 Unix 时间戳(秒)
signstring签名
order_no二选一string平台订单号(下单时返回的 order_no
merchant_order_no二选一string商户订单号(下单时传入的 order_no
order_nomerchant_order_no 至少传一个。如果两个都传,优先使用 order_no

成功返回

{
    "code": 0,
    "msg": "success",
    "data": {
        "order_no": "20260301143000123456",      // 平台订单号
        "merchant_order_no": "ORD20260301001",   // 商户订单号
        "channel": "kpay",                      // 渠道编码
        "channel_order_no": "2026030114350000001", // 上游渠道订单号(支付成功后返回)
        "amount": "100.00",                      // 订单金额(HKD)
        "currency": "HKD",                       // 币种
        "subject": "测试商品",                    // 订单描述
        "status": 1,                              // 状态码(见状态码表)
        "trade_status": "PAID",                  // 交易状态
        "status_text": "已支付",                  // 状态中文
        "pay_method": "wechat_scan",             // 支付方式
        "refund_amount": "0.00",                  // 退款金额
        "original_amount": "91.00",               // 原始金额(仅非HKD下单时返回)
        "original_currency": "CNY",               // 原始币种(仅非HKD下单时返回)
        "exchange_rate": "0.910000",              // 下单时汇率(仅非HKD下单时返回)
        "pay_url": "https://...",                 // 支付链接
        "return_url": "https://...",              // 支付完成跳转地址
        "notify_url": "https://...",              // 商户通知回调地址
        "notify_status": 1,                       // 通知状态:0=未通知 1=成功 2=失败
        "notify_status_text": "成功",              // 通知状态中文
        "notify_count": 1,                        // 已通知次数
        "paid_at": "2026-03-01 14:35:00",         // 支付时间
        "failed_at": null,                        // 失败时间
        "refunded_at": null,                      // 退款时间
        "closed_at": null,                        // 关闭时间
        "created_at": "2026-03-01 14:30:00"       // 下单时间
    }
}

响应参数说明

参数类型说明
order_nostring平台订单号
merchant_order_nostring商户订单号
channelstring渠道编码
channel_order_nostring上游渠道订单号(支付成功后才有)
amountstring订单金额(HKD)
currencystring币种
subjectstring订单描述
statusint状态码:0=待支付 1=已支付 2=失败 3=已退款 4=已关闭
trade_statusstring交易状态:WAIT_PAY / PAID / FAIL / REFUNDED / CLOSED
status_textstring状态中文描述
pay_methodstring支付方式
refund_amountstring退款金额
original_amountstring原始金额(仅非HKD下单时返回)
original_currencystring原始币种(仅非HKD下单时返回)
exchange_ratestring下单时汇率(仅非HKD下单时返回)
pay_urlstring支付链接
return_urlstring支付完成跳转地址
notify_urlstring商户通知回调地址
notify_statusint通知状态:0=未通知 1=成功 2=失败
notify_status_textstring通知状态中文
notify_countint已通知次数
paid_atstring支付时间(未支付为null)
failed_atstring失败时间(未失败为null)
refunded_atstring退款时间(未退款为null)
closed_atstring关闭时间(未关闭为null)
created_atstring下单时间

支付状态轮询

重要:订单查询接口受 IP 白名单限制,只能从商户后端服务器调用。
正确的轮询架构:商户前端请求商户自己的后端接口,商户后端再调用平台查询 API。
商户前端(浏览器) --> 商户后端 /check_order --> 平台 /api/query.php

商户后端:查询订单状态

商户后端封装一个接口(如 /check_order),内部调用平台查询API并将结果返回给前端。

PHP Python Java Node.js
<?php
// check_order.php - 商户后端接口,供前端轮询调用

$order_no = $_GET['order_no'] ?? '';
if (!$order_no) { echo json_encode(['status' => -1]); exit; }

$params = [
    'api_key'   => API_KEY,
    'timestamp' => time(),
    'order_no'  => $order_no,
];

// 签名(复用签名函数)
ksort($params);
$sign_str = '';
foreach ($params as $k => $v) {
    if ($v !== '') $sign_str .= "$k=$v&";
}
$sign_str .= "secret=" . API_SECRET;
$params['sign'] = md5($sign_str);

// 调用平台查询接口
$ch = curl_init('https://平台地址/api/query.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);

// 返回给前端
header('Content-Type: application/json');
echo json_encode([
    'status'  => $result['data']['status'] ?? 0,
    'paid_at' => $result['data']['paid_at'] ?? '',
]);
# Flask 示例 - 商户后端查询接口

from flask import Flask, request, jsonify

@app.route('/check_order')
def check_order():
    order_no = request.args.get('order_no', '')
    if not order_no:
        return jsonify({'status': -1})

    params = {
        'api_key': API_KEY,
        'timestamp': str(int(time.time())),
        'order_no': order_no,
    }
    params['sign'] = make_sign(params, API_SECRET)

    resp = requests.post(API_URL + 'query.php', data=params, verify=False)
    result = resp.json()

    return jsonify({
        'status': result.get('data', {}).get('status', 0),
        'paid_at': result.get('data', {}).get('paid_at', ''),
    })
// Spring Boot 示例 - 商户后端查询接口

@GetMapping("/check_order")
public Map<String, Object> checkOrder(@RequestParam String order_no) {
    Map<String, String> params = new HashMap<>();
    params.put("api_key", API_KEY);
    params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
    params.put("order_no", order_no);
    params.put("sign", sign(params, API_SECRET));

    // 发送 POST 到平台查询接口
    String response = httpPost(API_URL + "query.php", params);
    JSONObject result = new JSONObject(response);
    JSONObject data = result.optJSONObject("data");

    Map<String, Object> ret = new HashMap<>();
    ret.put("status", data != null ? data.optInt("status", 0) : 0);
    ret.put("paid_at", data != null ? data.optString("paid_at", "") : "");
    return ret;
}
// Express 示例 - 商户后端查询接口

app.get('/check_order', async (req, res) => {
    const { order_no } = req.query;
    if (!order_no) return res.json({ status: -1 });

    const params = {
        api_key: API_KEY,
        timestamp: String(Math.floor(Date.now() / 1000)),
        order_no,
    };
    params.sign = makeSign(params, API_SECRET);

    const resp = await fetch(API_URL + 'query.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams(params),
    });
    const result = await resp.json();
    const data = result.data || {};

    res.json({
        status: data.status || 0,
        paid_at: data.paid_at || '',
    });
});

商户前端:轮询自己的后端

// 前端轮询商户自己的后端接口(不是直接调平台API)
var order_no = '20260301143000123456'; // 下单时拿到的平台订单号

var timer = setInterval(function() {
    fetch('/check_order?order_no=' + order_no)
        .then(function(r) { return r.json(); })
        .then(function(res) {
            if (res.status === 1) {
                clearInterval(timer);
                alert('支付成功!');
                window.location.href = '/success.html';
            }
        });
}, 3000); // 每3秒查询一次

// 5分钟后停止轮询
setTimeout(function() { clearInterval(timer); }, 300000);
对于 微信扫码支付宝扫码 等需要用户主动操作的场景,建议展示二维码后启动轮询,
每隔 3~5秒 查询一次,当 status=1 时自动跳转成功页面。最长轮询 5分钟,超时提示用户重新下单。

POST 退款

/api/refund.php

对已支付的订单发起退款。退款金额不能超过原订单金额。

请求参数

参数必填类型说明
api_keystring商户 API Key
timestampint当前 Unix 时间戳(秒)
signstring签名
order_nostring平台订单号
refund_amountstring退款金额(不能超过原订单金额,保留2位小数)

退款示例

PHP Python Java Node.js
$params = [
    'api_key'       => API_KEY,
    'timestamp'     => time(),
    'order_no'      => '20260301143000123456',
    'refund_amount' => '100.00',
];
// 签名(同下单签名逻辑)
ksort($params);
$sign_str = '';
foreach ($params as $k => $v) {
    if ($v !== '') $sign_str .= "$k=$v&";
}
$sign_str .= "secret=" . API_SECRET;
$params['sign'] = md5($sign_str);

$ch = curl_init('https://平台地址/api/refund.php');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = json_decode(curl_exec($ch), true);
params = {
    'api_key': API_KEY,
    'timestamp': str(int(time.time())),
    'order_no': '20260301143000123456',
    'refund_amount': '100.00',
}
params['sign'] = make_sign(params, API_SECRET)

resp = requests.post(API_URL + 'refund.php', data=params, verify=False)
result = resp.json()
if result['code'] == 0:
    print('退款成功', result['data'])
Map<String, String> params = new HashMap<>();
params.put("api_key", API_KEY);
params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
params.put("order_no", "20260301143000123456");
params.put("refund_amount", "100.00");
params.put("sign", sign(params, API_SECRET));

String response = httpPost(API_URL + "refund.php", params);
const params = {
    api_key: API_KEY,
    timestamp: String(Math.floor(Date.now() / 1000)),
    order_no: '20260301143000123456',
    refund_amount: '100.00',
};
params.sign = makeSign(params, API_SECRET);

const resp = await fetch(API_URL + 'refund.php', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams(params),
});
const result = await resp.json();

成功返回

{
    "code": 0,
    "msg": "success",
    "data": {
        "order_no": "20260301143000123456",
        "refund_amount": "100.00",
        "refund_order_no": "R20260301..."
    }
}
退款须知:
  • 已支付(status=1)的订单可以退款
  • 退款金额不能超过原订单金额
  • 退款成功后订单状态变为 3(已退款),不可撤回
  • 退款到账时间取决于上游渠道,通常 1~7 个工作日
  • 退款成功后,平台会向 notify_url 发送 trade_status=REFUNDED 的异步通知

异步通知

当订单状态发生变化时(支付成功、支付失败、退款、关闭等),平台会主动向商户下单时传入的 notify_url 发送 POST 通知。

通知触发场景

trade_statusstatus场景说明
PAID1支付成功用户完成支付,需要发货或执行业务逻辑
FAIL2支付失败支付过程出现错误,可提示用户重新支付
REFUNDED3退款成功退款已到账,refund_amount 字段为实际退款金额
CLOSED4订单关闭订单被撤销或超时关闭

通知流程

订单状态变化 --> 上游渠道通知平台 --> 平台验证并更新订单 --> 平台 POST 通知商户 --> 商户返回 success

通知参数

平台以 application/x-www-form-urlencoded 格式 POST 以下参数到商户的 notify_url

参数类型说明
order_nostring平台订单号
merchant_order_nostring商户订单号
amountstring订单金额
currencystring币种
statusint订单状态码(0=待支付 1=已支付 2=失败 3=已退款 4=已关闭)
trade_statusstring交易状态(PAID / FAIL / REFUNDED / CLOSED
建议优先使用此字段判断业务逻辑
channelstring渠道编码
pay_methodstring支付方式(如 wechat_scan、alipay_h5 等)
refund_amountstring退款金额(仅退款时有值,如 50.00;未退款时为空)
original_amountstring商户原始金额(仅非HKD下单时返回,如 91.00
original_currencystring商户原始币种(仅非HKD下单时返回,如 CNY
exchange_ratestring下单时使用的汇率(仅非HKD下单时返回)
paid_atstring支付时间(仅支付成功时有值)
timestampint通知发送时间戳
signstring签名(验签方式与请求签名一致)

通知数据示例

支付成功通知:

amount=100.00&channel=kpay¤cy=HKD&merchant_order_no=ORD20260301001
&order_no=20260301143000123456&paid_at=2026-03-01+14%3A35%3A00&pay_method=wechat_scan
&status=1&trade_status=PAID×tamp=1709285700&sign=a1b2c3d4e5...

退款成功通知:

amount=100.00&channel=kpay¤cy=HKD&merchant_order_no=ORD20260301001
&order_no=20260301143000123456&paid_at=2026-03-01+14%3A35%3A00&pay_method=wechat_scan
&refund_amount=50.00&status=3&trade_status=REFUNDED×tamp=1709287500&sign=b2c3d4e5f6...
退款通知中 amount 仍为原订单金额(100.00),refund_amount 为实际退款金额(50.00)。
重要:
  • 收到通知后必须先验证签名,防止伪造通知
  • 验签通过后,根据 trade_status 字段判断业务类型并处理对应逻辑
  • 处理完成后,HTTP 响应必须返回状态码 200,且响应体为纯文本 success
  • 如果未返回 success,平台会按重试策略重新发送通知
  • 同一订单可能收到多次通知(支付、退款等不同类型),请做好幂等处理
  • 不要只处理支付成功:退款和关闭通知同样重要,需要及时更新本地订单状态

商户接收通知示例

PHP Python Java Node.js
<?php
// notify.php - 商户端通知接收地址

$params = $_POST;
$sign = $params['sign'];
unset($params['sign']);

// 1. 验证签名
ksort($params);
$sign_str = '';
foreach ($params as $k => $v) {
    if ($v !== '') $sign_str .= "$k=$v&";
}
$sign_str .= "secret=" . API_SECRET;

if (md5($sign_str) !== $sign) {
    echo 'sign error'; exit;
}

$trade_status = $params['trade_status'];
$order_no = $params['merchant_order_no'];

// 2. 根据 trade_status 分别处理
switch ($trade_status) {
    case 'PAID':
        // 支付成功:发货/充值等
        db_update("UPDATE orders SET status='paid' WHERE order_no=?", [$order_no]);
        break;
    case 'REFUNDED':
        // 退款成功:更新订单状态、退回库存等
        db_update("UPDATE orders SET status='refunded' WHERE order_no=?", [$order_no]);
        break;
    case 'CLOSED':
        // 订单关闭:释放库存等
        db_update("UPDATE orders SET status='closed' WHERE order_no=?", [$order_no]);
        break;
    case 'FAIL':
        // 支付失败:可提示用户重新支付
        db_update("UPDATE orders SET status='failed' WHERE order_no=?", [$order_no]);
        break;
}

// 3. 必须返回 success(无论哪种通知类型)
echo 'success';
# Flask 示例
from flask import Flask, request
import hashlib

@app.route('/notify', methods=['POST'])
def notify():
    params = dict(request.form)
    sign = params.pop('sign', '')

    # 1. 验签
    filtered = {k: v for k, v in sorted(params.items()) if v != ''}
    sign_str = '&'.join(f'{k}={v}' for k, v in filtered.items())
    sign_str += f'&secret={API_SECRET}'

    if hashlib.md5(sign_str.encode()).hexdigest() != sign:
        return 'sign error', 200

    # 2. 根据 trade_status 分别处理
    trade_status = params['trade_status']
    order_no = params['merchant_order_no']

    if trade_status == 'PAID':
        # 发货/充值等
        pass
    elif trade_status == 'REFUNDED':
        # 退款:更新订单状态
        pass
    elif trade_status == 'CLOSED':
        # 订单关闭
        pass
    elif trade_status == 'FAIL':
        # 支付失败
        pass

    return 'success', 200
// Spring Boot 示例
@PostMapping("/notify")
public String notify(@RequestParam Map<String, String> params) {
    String sign = params.remove("sign");

    // 1. 验签
    String expected = sign(new HashMap<>(params), API_SECRET);
    if (!expected.equals(sign)) {
        return "sign error";
    }

    // 2. 根据 trade_status 分别处理
    String tradeStatus = params.get("trade_status");
    String orderNo = params.get("merchant_order_no");

    switch (tradeStatus) {
        case "PAID":
            // 发货/充值等
            break;
        case "REFUNDED":
            // 退款处理
            break;
        case "CLOSED":
            // 订单关闭
            break;
        case "FAIL":
            // 支付失败
            break;
    }

    return "success";
}
// Express 示例
const express = require('express');
app.use(express.urlencoded({ extended: true }));

app.post('/notify', (req, res) => {
    const params = { ...req.body };
    const sign = params.sign;
    delete params.sign;

    // 1. 验签
    const expected = makeSign(params, API_SECRET);
    if (expected !== sign) {
        return res.send('sign error');
    }

    // 2. 根据 trade_status 分别处理
    const { trade_status, merchant_order_no } = params;

    switch (trade_status) {
        case 'PAID':
            // 发货/充值等
            break;
        case 'REFUNDED':
            // 退款处理
            break;
        case 'CLOSED':
            // 订单关闭
            break;
        case 'FAIL':
            // 支付失败
            break;
    }

    res.send('success');
});

回调重试策略

若商户 notify_url 未返回 success,平台会按以下间隔自动重试:

次数间隔说明
第1次立即状态变化时立即发送
第2次1 分钟后第1次重试
第3次5 分钟后第2次重试
第4次30 分钟后第3次重试
第5次1 小时后最后一次自动重试
共最多通知 5次。如果5次均失败,管理员可在后台手动重发。
成功条件:HTTP 状态码 200,且响应体为纯文本 success(不含引号、空格、HTML 标签、BOM 头等)。
返回 "success"(带引号)、SUCCESS(大写)、{ "status": "ok" }(JSON)等均视为失败。

幂等处理建议

  1. 收到通知后,先用 merchant_order_no + trade_status 查询本地订单状态
  2. 如果该状态已处理过,直接返回 success,不要重复处理
  3. 如果未处理,在数据库事务中执行业务逻辑,完成后返回 success
  4. 注意:同一订单可能先后收到 PAIDREFUNDED 等不同类型的通知,这是正常的状态流转
  5. 建议使用数据库唯一索引或分布式锁防止并发重复处理

通知与主动查询的关系

异步通知是平台主动推送给商户的,但不能100%保证送达。
建议商户在关键业务流程中,除了接收通知外,还应主动调用订单查询接口确认订单状态,做到双重保障。

订单状态码

statustrade_status状态说明
0WAIT_PAY待支付订单已创建,等待用户支付
1PAID已支付用户支付成功
2FAIL支付失败支付过程中出现错误
3REFUNDED已退款订单已成功退款(由上游渠道通知或商户发起退款)
4CLOSED已关闭订单被撤销或超时关闭
status 为数字类型,trade_status 为字符串类型,两者含义一致。
建议通知处理中优先使用 trade_status 字段,语义更明确。

错误码

code说明
0成功
-1业务错误(具体看 msg 字段)

常见错误信息

类别msg说明与排查
签名缺少签名参数请求中缺少 api_key / timestamp / sign
请求已过期timestamp 与服务器时间差超过5分钟
无效的api_keyapi_key 不存在或商户已被禁用
商户已过期商户有效期已过,联系管理员续期
签名错误检查:(1)参数排序 (2)空值排除 (3)Secret 是否正确
安全IP不在白名单中: x.x.x.x将提示的IP加入白名单
请求过于频繁 / 已达每日调用上限超出频率限制,联系管理员调整
下单渠道不可用渠道已关闭或不存在
您未开通此渠道管理员未给该商户开通此渠道
该渠道不支持此支付方式pay_method 值错误
订单号重复同一 order_no 已存在
退款订单状态不允许退款仅 status=1 可退款
退款金额不能超过订单金额检查退款金额

常见问题 (FAQ)

Q: 签名一直报错怎么排查?

  1. 确认 API Key 和 API Secret 是否正确(注意前后空格)
  2. 确认参数按 key 的 ASCII 码升序排列
  3. 确认空值参数不参与签名拼接
  4. 确认末尾拼接格式为 &secret=xxx(注意有 &
  5. 确认使用 32位小写 MD5
  6. 打印签名前的完整字符串,与平台日志对比

Q: 订单状态变化了但没收到通知?

  1. 确认 notify_url 是否可从外网访问
  2. 确认 notify_url 返回的是纯文本 success,HTTP状态码 200
  3. 检查防火墙是否屏蔽了平台 IP
  4. 在管理后台的「调用日志」中查看通知记录和响应
  5. 建议同时使用后端主动查询作为补充

Q: 可以在前端直接调用平台API吗?

不可以。所有平台 API 必须从商户后端服务器调用。原因:
1. 签名需要 API Secret,前端调用会暴露密钥
2. IP 白名单限制了只有指定服务器IP才能访问,浏览器用户的IP无法通过校验
正确做法:商户前端请求商户自己的后端,商户后端再调用平台 API。

Q: 微信扫码的 pay_url 能直接跳转吗?

不能。wechat_scan 返回的 pay_url 是微信支付协议链接(weixin://wxpay/...),必须生成二维码让用户用微信扫一扫。

Q: 订单号有什么要求?

商户订单号在同一商户下必须唯一。建议使用时间戳+随机数的组合,长度 6~32 位,仅包含字母和数字。

Q: 支持哪些币种?如何设置汇率?

支持 HKD(港币)和 CNY(人民币),默认 HKD。

如需使用人民币下单,请在商户后台「渠道配置」中按支付类型(微信/支付宝/收银台)分别设置汇率。例如设置汇率为 0.91,表示 1 HKD = 0.91 CNY
非HKD币种会按照支付类型(微信/支付宝/收银台)单独配置的汇率自动换算为HKD后提交给上游渠道。

重要:数据库中统一以 HKD 存储。退款也以 HKD 金额发起。通知和查询接口会额外返回 original_amountoriginal_currencyexchange_rate 字段,方便商户对账。

Q: 测试环境和生产环境有什么区别?

本平台 API 接口地址不区分测试/生产环境。渠道的测试与生产取决于渠道方配置(如 KPay 的 UAT/PROD 环境),由管理员在后台设置。

Q: 如何获取 API Key 和 Secret?

登录商户后台,在「API密钥」页面即可查看。API Key 可直接复制,API Secret 需点击「显示」按钮查看。请妥善保管,切勿泄露。

通用付款页面

平台为每个商户提供一个通用的在线付款页面,客户打开后可自行选择支付方式并完成付款。

链接格式

https://您的域名/api/payment.php?merchant=M202603040336

功能说明

  • 根据 merchant 参数自动识别商户
  • 自动列出该商户已开通的所有支付方式供客户选择
  • 扫码支付(微信扫码/支付宝扫码)自动生成二维码
  • H5支付自动跳转到支付链接
  • 收银台支付跳转到收银台页面
  • 支持 HKD(港币)和 CNY(人民币)两种币种

使用方式

商户可在商户后台的「密钥管理」页面查看付款链接和二维码。
将链接或二维码发送给客户,客户打开后即可进行付款。
付款链接是固定的,可长期使用。