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_key string 商户 API Key
timestamp int 当前 Unix 时间戳(秒)
sign string 签名(见签名认证
channel string 渠道编码,如 kpaywondershengpay。留空则自动在所有已配置渠道中轮询选择
order_no string 商户订单号(同一商户下不可重复,建议 6~32 位字母数字)
amount string 订单金额(保留2位小数,如 100.00
currency string 币种,默认 HKD
· 上游普通渠道(kpay/wonder/shengpay)原生结算币种为 HKD,传入其他币种时按商户后台「渠道配置」中设置的汇率自动换算
· 支付宝直连渠道(channel=alipay)仅支持 CNY
pay_method string 支付方式,详见下方支付方式表
subject string 订单描述(将展示在支付页面)
notify_url string 异步通知地址(订单状态变化时平台向此地址发送POST通知,包括支付成功、失败、退款、关闭等)
return_url 建议 string 支付完成后用户跳转地址,H5 和卡类支付时强烈建议传入
payment_institution string 支付宝机构,默认 ALIPAYCN,可选 ALIPAYHK
ip string 用户真实 IP,缺省时平台自动取请求 IP。
shengpay 渠道必填(盛付通风控要求传入用户真实 IP)。
密钥轮询:商户可为同一渠道配置多套密钥,系统自动采用最少使用(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
&timestamp=1709284800
&channel=kpay
&order_no=ORD20260301001
&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
&sign=e10adc3949ba59abbe56e057f20f883e

成功返回

{
    "code": 0,
    "msg": "success",
    "data": {
        "order_no": "20260301143000123456",   // 平台订单号(用于后续查询/退款)
        "pay_url": "https://...",             // 原始支付链接(自定义UI时使用)
        "checkout_url": "https://.../api/payment.php?order_no=..."  // 内置H5收银台URL(推荐直接使用)
    }
}

失败返回

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

checkout_url 使用方式(推荐)

checkout_url 是平台内置的 H5 收银台页面,商户无需自行渲染二维码或处理支付方式差异
直接把 checkout_url 给到用户(重定向、新开窗口、或在前端 iframe 嵌入)即可:
· PC 端访问:自动显示大二维码,引导用户用微信/支付宝扫码支付
· 手机浏览器访问:根据 Android/iOS 分别给出最佳引导(点击拉起 App 或长按保存二维码扫码)
· 微信内访问:自动识别并给出"在浏览器打开"等正确提示
· 支持金额展示、订单倒计时、订单状态自动轮询、支付成功自动跳转 return_url
· 自动反诈骗提示,符合合规要求
注意:当 pay_method=cashier 时(收银台支付方式),上游已自带完整收银页,checkout_url 返回为空字符串,请直接使用 pay_url 跳转。

pay_url 使用方式(自定义UI时使用)

如商户需要自行实现收银 UI,可直接使用 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. 支付宝PC(alipay_pc)— PC 端浏览器跳转

返回的 pay_url 是支付宝电脑网站支付链接。在 PC 浏览器中跳转,进入支付宝网页收银台,用户扫码或登录账号完成支付。
仅 shengpay 渠道支持。
window.location.href = res.data.pay_url;

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

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

各渠道支持的支付方式

渠道 pay_method 名称 说明
kpay wechat_scan 微信扫码 返回微信协议链接,须生成二维码
wechat_h5 微信H5 返回微信支付直链,须在微信内打开
wechat_jsapi 微信JSAPI 微信内置浏览器支付(wechat_h5 在微信内打开的变体),下单时复用 wechat_h5 配置
alipay_scan 支付宝扫码 建议生成二维码供用户扫描
alipay_h5 支付宝H5 可生成二维码或支付宝内打开链接
cashier 收银台支付 跳转至 KPay 自带收银台完成支付
wonder wechat_scan 微信扫码 返回微信协议链接(weixin://),须生成二维码
wechat_h5 微信H5 微信内打开直接支付,非微信跳转支付页面
alipay_scan 支付宝扫码 返回支付宝二维码链接,须生成二维码
alipay_h5 支付宝H5 返回支付宝H5直接支付链接
cashier 收银台支付 跳转至Wonder收银台,用户自选支付方式
shengpay wechat_scan 微信扫码 返回微信Native支付链接,须生成二维码
wechat_h5 微信H5 返回微信H5支付链接,手机浏览器跳转支付
alipay_scan 支付宝扫码 返回支付宝二维码链接,须生成二维码
alipay_h5 支付宝H5 返回支付宝H5支付链接,手机浏览器跳转支付
alipay_pc 支付宝PC 返回支付宝电脑网站支付链接,PC浏览器跳转支付
alipay alipay_scan 支付宝扫码(直连) 支付宝官方直连渠道,币种仅支持 CNY
alipay_h5 支付宝H5(直连) 支付宝官方直连渠道,币种仅支持 CNY
实际可用支付方式以管理员分配和渠道方配置为准。可在商户后台的「渠道配置」页面查看已开通渠道支持的完整列表。

POST GET 订单查询

/api/query.php

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

请求参数

参数 必填 类型 说明
api_key string 商户 API Key
timestamp int 当前 Unix 时间戳(秒)
sign string 签名
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_no string 平台订单号
merchant_order_no string 商户订单号
channel string 渠道编码
channel_order_no string 上游渠道订单号(支付成功后才有)
amount string 订单金额(HKD)
currency string 币种
subject string 订单描述
status int 状态码:0=待支付 1=已支付 2=失败 3=已退款 4=已关闭
trade_status string 交易状态:WAIT_PAY / PAID / FAIL / REFUNDED / CLOSED
status_text string 状态中文描述
pay_method string 支付方式
refund_amount string 退款金额
original_amount string 原始金额(仅非HKD下单时返回)
original_currency string 原始币种(仅非HKD下单时返回)
exchange_rate string 下单时汇率(仅非HKD下单时返回)
pay_url string 支付链接
return_url string 支付完成跳转地址
notify_url string 商户通知回调地址
notify_status int 通知状态:0=未通知 1=成功 2=失败
notify_status_text string 通知状态中文
notify_count int 已通知次数
paid_at string 支付时间(未支付为null)
failed_at string 失败时间(未失败为null)
refunded_at string 退款时间(未退款为null)
closed_at string 关闭时间(未关闭为null)
created_at string 下单时间

支付状态轮询

重要:订单查询接口受 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_key string 商户 API Key
timestamp int 当前 Unix 时间戳(秒)
sign string 签名
order_no string 平台订单号
refund_amount string 退款金额(不能超过原订单金额,保留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_status status 场景 说明
PAID 1 支付成功 用户完成支付,需要发货或执行业务逻辑
FAIL 2 支付失败 支付过程出现错误,可提示用户重新支付
REFUNDED 3 退款成功 退款已到账,refund_amount 字段为实际退款金额
CLOSED 4 订单关闭 订单被撤销或超时关闭

通知流程

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

通知参数

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

参数 类型 说明
order_no string 平台订单号
merchant_order_no string 商户订单号
amount string 订单金额
currency string 币种
status int 订单状态码(0=待支付 1=已支付 2=失败 3=已退款 4=已关闭)
trade_status string 交易状态(PAID / FAIL / REFUNDED / CLOSED
建议优先使用此字段判断业务逻辑
channel string 渠道编码
pay_method string 支付方式(如 wechat_scan、alipay_h5 等)
refund_amount string 退款金额(仅退款时有值,如 50.00;未退款时为空)
original_amount string 商户原始金额(仅非HKD下单时返回,如 91.00
original_currency string 商户原始币种(仅非HKD下单时返回,如 CNY
exchange_rate string 下单时使用的汇率(仅非HKD下单时返回)
paid_at string 支付时间(仅支付成功时有值)
timestamp int 通知发送时间戳
sign string 签名(验签方式与请求签名一致)

通知数据示例

支付成功通知:

amount=100.00&channel=kpay&currency=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&timestamp=1709285700&sign=a1b2c3d4e5...

退款成功通知:

amount=100.00&channel=kpay&currency=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&timestamp=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%保证送达。
建议商户在关键业务流程中,除了接收通知外,还应主动调用订单查询接口确认订单状态,做到双重保障。

订单状态码

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

错误码

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

常见错误信息

类别 msg 说明与排查
签名 缺少签名参数 请求中缺少 api_key / timestamp / sign
请求已过期 timestamp 与服务器时间差超过5分钟
无效的api_key api_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

# 预填金额(金额锁定不可改,币种仍可选)
https://您的域名/api/payment.php?merchant=M202603040336&amount=100

# 预填币种(币种锁定不显示,金额仍可填)
https://您的域名/api/payment.php?merchant=M202603040336&currency=CNY

# 预填金额+币种(都锁定,用户只需选择支付方式即可付款)
https://您的域名/api/payment.php?merchant=M202603040336&amount=100&currency=CNY

GET 参数

参数必填说明
merchant商户号(管理后台/商户后台密钥页可查)
amount预填金额。传入后页面以大字展示,用户无法修改
currency预填币种(HKD/CNY/USD/EUR/JPY/TWD)。传入后币种选择按钮隐藏
pay_method预选支付方式。粗粒度可传 wechat/alipay/cashier,细粒度可传 wechat_h5/alipay_scan 等。
· 商户已开通该方式 → 收银台预选中
· 商户未开通该方式 → 自动忽略,正常显示所有可用方式
skip是否跳过收银台界面,取值 1true
仅当同时满足以下条件才生效:
1. amount 有效
2. pay_method 已传入且商户已开通
满足时后端直接创建订单,用户跳转到支付页(二维码/拉起 App);任一条件不满足时自动降级显示收银台。

组合示例

# 仅预选微信支付,金额由用户输入
/api/payment.php?merchant=Mxxx&pay_method=wechat

# 金额+币种+预选支付方式 — 收银台只剩"立即付款"一步
/api/payment.php?merchant=Mxxx&amount=100&currency=CNY&pay_method=alipay

# 全部锁定 + skip=1 → 用户直接看到二维码/支付按钮,无收银台中间页
/api/payment.php?merchant=Mxxx&amount=100&currency=CNY&pay_method=alipay&skip=1

功能说明

  • 根据 merchant 参数自动识别商户,列出商户已开通的支付组(微信支付/支付宝/收银台)
  • 自动检测当前浏览环境(PC / 手机浏览器 / 微信内 / 支付宝内),并隐藏不可用的方式(如微信内隐藏支付宝)
  • 用户点击"立即付款"后内部调用下单接口创建订单,自动跳转到统一支付页 ?order_no=xxx
  • 支付页同样基于 payment.php(与 checkout_url 完全一致),二维码/拉起/扫码引导全部自动适配
  • 支持金额倒计时、订单状态自动轮询、支付成功自动跳转 return_url

使用方式

商户后台:「账户信息」页面可查看付款链接和二维码(链接格式为不带 amount/currency 的标准形态)。
管理后台:商户管理 → 密钥弹窗里也可查看每个商户的专属支付页链接。
动态调用:商户可在自有页面后端拼接带 amount/currency 的 URL 给客户跳转,无需调用 API 即可完成"指定金额收款"。