商户 API 对接文档

版本 v1.2 · 最后更新:2026-04-10

一、接入准备

1.1 获取密钥

1登录商户后台
2进入「API管理」页面
3获取以下信息:
参数说明
商户号 memberid平台分配的6位商户号
商户密钥 key用于签名的密钥,请妥善保管
网关地址 gatewayAPI请求的基础URL

1.2 对接须知

项目说明
请求方式所有接口均使用 POST
请求格式application/x-www-form-urlencodedmultipart/form-data
字符编码UTF-8
签名方式HMAC-SHA256
IP白名单建议在商户后台配置回调IP白名单。未配置时允许所有IP访问
金额格式单位:元,保留2位小数,如 100.00
时间格式YYYY-MM-DD HH:mm:ss,如 2026-04-04 14:30:00
时间说明pay_applydate 为商户系统当前时间,任意时区均可,仅参与签名计算

二、签名算法

平台使用 HMAC-SHA256 签名算法:

签名类型安全等级说明
HMAC-SHA25664位签名

2.1 通用步骤

1筛选参与签名的字段,去除值为空的参数和签名字段(pay_sign)本身。
2将剩余参数按照 参数名 ASCII 码从小到大排序(字典序),使用 URL 键值对的格式拼接。
3使用 HMAC-SHA256 算法计算签名:

HMAC-SHA256

sign = HMAC_SHA256(stringA, 商户密钥).toUpperCase()
密钥作为 HMAC 的 key 参数,不拼接在字符串末尾。

2.2 签名示例

假设参数如下:

pay_memberid  = "100001"
pay_orderid   = "20260404143000001"
pay_applydate = "2026-04-04 14:30:00"
pay_notifyurl = "https://merchant.com/notify"
pay_callbackurl = "https://merchant.com/callback"
pay_amount    = "500.00"
商户密钥       = "a1b2c3d4e5f6..."

拼接后(按参数名ASCII排序):

stringA = "pay_amount=500.00&pay_applydate=2026-04-04 14:30:00&pay_callbackurl=https://merchant.com/callback&pay_memberid=100001&pay_notifyurl=https://merchant.com/notify&pay_orderid=20260404143000001"
注意事项 值为空的参数不参与签名 · 签名字段本身不参与签名 · 中文参数不需要URL编码后再签名 · 参数值前后不要有空格

三、代收 API

3.1 代收下单

商户发起收款请求,平台匹配可用通道并返回收款信息。

POST{网关地址}/api/v1/gateway/pay

请求参数

参数名含义必填签名说明
pay_memberid商户号YY平台分配的商户号
pay_orderid商户订单号YY商户系统唯一订单号,最长50字符
pay_applydate提交时间YY商户系统当前时间,格式:YYYY-MM-DD HH:mm:ss,任意时区均可,仅参与签名
pay_bankcode通道编码NY支付通道编码,不传则使用默认通道,参见通道编码表
pay_notifyurl服务端通知地址YY支付成功后,平台通过POST异步通知此地址,必须为外网可访问的URL
pay_callbackurl页面跳转地址YY支付成功后,收银台页面自动跳转至此URL(5秒倒计时)
pay_amount订单金额YY单位元,保留2位小数,如 100.00
pay_productname商品名称NN商品描述,可选参数
pay_ip付款人IPNN付款人的真实IP地址
pay_sign签名Y-签名值,参见签名算法
pay_attach附加数据NN原样返回,中文需URL编码
pay_userid会员IDNN商户系统内的用户唯一标识
pay_username付款人姓名NN转卡或银联通道必填

请求示例

{
    "pay_memberid": "100001",
    "pay_orderid": "20260414100000001",
    "pay_applydate": "2026-04-14 10:00:00",
    "pay_notifyurl": "https://merchant.com/notify",
    "pay_callbackurl": "https://merchant.com/success",
    "pay_amount": "500.00",
    "pay_username": "张三",
    "pay_sign": "A1B2C3D4E5F6..."
}

成功响应

{
    "code": 200,
    "message": "操作成功",
    "data": {
        "status": "1",
        "msg": "下单成功",
        "order_id": "C20260404143000123456",
        "mch_order_id": "20260404143000001",
        "pay_url": "/cashier/eyJhbGciOiJIUzI1NiJ9..."
    },
    "timestamp": 1775551800000
}
参数说明
codeHTTP状态码,200 为成功
message操作结果描述
data.status"1" 为成功
data.msg状态描述信息
data.order_id平台系统订单号(C开头=代收)
data.mch_order_id商户订单号(原样返回 pay_orderid)
data.pay_url收银台页面相对路径,需拼接网关域名后引导付款人访问
timestamp服务端时间戳(毫秒)

常见错误

{
    "code": 403,
    "message": "签名错误",
    "data": null,
    "timestamp": 1775551851034
}
codemessage原因
403签名错误签名算法不正确或密钥错误
404商户不存在商户号错误
403商户已禁用商户被平台禁用
403商户代收功能未开启商户未开启收款功能
403IP不在白名单中请求IP不在商户配置的白名单中
400金额须在 X - Y 之间金额低于最小或超过最大限额
404暂无可用通道当前无匹配的收款产品
429请求过于频繁,请稍后再试超出API频率限制

3.2 代收回调通知

付款人完成支付后,平台会向商户的 pay_notifyurl 发送POST异步通知。

POST商户的 pay_notifyurl 地址

回调参数

参数名含义签名说明
memberid商户号Y平台分配的商户号
orderid商户订单号Y商户系统的订单号
amount订单金额Y实际支付金额,保留2位小数
transaction_id平台流水号Y平台生成的唯一交易流水号
datetime交易时间Y平台时区时间,格式 YYYY-MM-DD HH:mm:ss,如 2026-04-04 14:35:00
returncode交易状态Y00 表示支付成功
timestamp回调时间戳YUnix时间戳(秒),用于防重放校验
nonce随机串Y32位随机字符串,每次回调唯一
attach附加数据N下单时传入的附加字段,原样返回
sign签名-签名值,商户需验签确认通知合法性

商户响应要求

验签通过并处理业务逻辑后,商户需在HTTP响应体中返回纯文本:

OK
必须返回大写的 OK 无引号、无空格、无其他字符。如果平台未收到 OK 响应,将按 30秒 → 60秒 → 120秒 间隔重试3次。
安全校验建议 1. 检查 timestamp:当前时间 - timestamp > 300秒 → 拒绝(可能是重放攻击)
2. 检查 nonce:是否已处理过 → 已处理则直接返回OK
3. 回调可能重复发送,商户需做好幂等处理

3.3 代收订单查询

主动查询代收订单的支付状态。

POST{网关地址}/api/v1/gateway/query

请求参数

参数名含义必填签名说明
pay_memberid商户号YY平台分配的商户号
pay_orderid商户订单号YY需要查询的商户订单号
pay_sign签名Y-签名值

请求示例

{
    "pay_memberid": "100001",
    "pay_orderid": "20260414100000001",
    "pay_sign": "A1B2C3D4E5F6..."
}

响应参数

{
    "code": 200,
    "message": "操作成功",
    "data": {
        "memberid": "100001",
        "orderid": "20260404143000001",
        "amount": "500.00",
        "time_end": "2026-04-04T14:35:00.000Z",
        "transaction_id": "C20260404143000123456",
        "returncode": "00",
        "trade_state": "SUCCESS",
        "sign": "A1B2C3D4..."
    },
    "timestamp": 1775551800000
}
参数名含义签名说明
data.memberid商户号Y平台分配的商户号
data.orderid商户订单号Y原样返回下单时的 pay_orderid
data.amount订单金额Y保留2位小数,如 500.00
data.time_end支付完成时间YISO 8601 格式,未支付时为空字符串
data.transaction_id平台流水号Y平台系统订单号(C开头=代收)
data.returncode请求状态Y00=查询成功 / 01=未支付。仅表示查询请求状态,不代表已支付
data.trade_state交易状态YNOTPAY=待支付 / SUCCESS=已支付 / EXPIRED=已过期
data.sign签名-平台对响应参数的签名,商户可验签确认合法性
判断支付状态请使用 trade_state 字段,不要用 returncode

四、代付 API

4.1 代付下单

商户发起付款请求,平台将资金通过渠道转账至指定收款账户。

POST{网关地址}/api/v1/gateway/payout

请求参数

参数名含义必填签名说明
pay_memberid商户号YY平台分配的商户号
pay_orderid商户订单号YY商户系统唯一订单号,最长50字符
pay_applydate提交时间YY商户系统当前时间,格式:YYYY-MM-DD HH:mm:ss,仅参与签名
pay_bankcode通道编码NY付款通道编码,不传则使用默认通道,参见通道编码表
pay_notifyurl服务端通知YY付款完成后,平台通过POST异步通知此地址,必须为外网可访问的URL
pay_amount付款金额YY单位元,保留2位小数
pay_callbackurl页面跳转地址NN付款完成后的前端跳转地址
pay_bank_name收款银行YY中国工商银行
pay_account_name收款人姓名YY收款方真实姓名
pay_account_no收款账号YY收款方银行卡号
pay_sign签名Y-签名值
pay_attach附加数据NN原样返回

请求示例

{
    "pay_memberid": "100001",
    "pay_orderid": "20260414150000001",
    "pay_applydate": "2026-04-14 15:00:00",
    "pay_notifyurl": "https://merchant.com/notify",
    "pay_amount": "1000.00",
    "pay_bank_name": "中国工商银行",
    "pay_account_name": "李四",
    "pay_account_no": "6222021234567890123",
    "pay_sign": "A1B2C3D4E5F6..."
}

成功响应

{
    "code": 200,
    "message": "操作成功",
    "data": {
        "status": "1",
        "msg": "下单成功",
        "order_id": "P20260404150000654321",
        "mch_order_id": "20260404150000001",
        "amount": 1000,
        "fee": 32
    },
    "timestamp": 1775551800000
}
参数说明
codeHTTP状态码,200 为成功
data.status"1" 为成功
data.msg状态描述信息
data.order_id平台系统订单号(P开头=代付)
data.mch_order_id商户订单号(原样返回 pay_orderid)
data.amount付款金额
data.fee手续费金额(百分比费用 + 固定费用)
timestamp服务端时间戳(毫秒)
余额要求 代付需要商户预先充值。所需余额 = 付款金额 + 手续费。例如:付款1000元,费率3%+2,则需余额 ≥ 1032元。

4.2 代付回调通知

付款完成后,平台会向商户的 pay_notifyurl 发送POST异步通知。

POST商户的 pay_notifyurl 地址

回调参数

参数名含义签名说明
memberid商户号Y平台分配的商户号
orderid商户订单号Y商户系统的订单号
amount付款金额Y实际付款金额,保留2位小数
transaction_id平台流水号Y平台生成的唯一交易流水号(P开头)
datetime交易时间Y平台时区时间,格式 YYYY-MM-DD HH:mm:ss
returncode交易状态Y见下方状态说明
timestamp回调时间戳YUnix时间戳(秒),用于防重放校验
nonce随机串Y32位随机字符串,每次回调唯一
attach附加数据N下单时传入的附加字段,原样返回
sign签名-签名值,商户需验签确认通知合法性

returncode 状态说明

returncode含义商户处理建议
00付款成功标记订单为付款完成
01付款失败联系平台客服排查原因
02已取消付款被取消,款项不会到账
商户响应要求 与代收回调相同,验签通过后需返回纯文本 OK(大写),否则平台重试3次。

4.3 代付订单查询

POST{网关地址}/api/v1/gateway/payout/query

请求参数

参数名含义必填签名
pay_memberid商户号YY
pay_orderid商户订单号YY
pay_sign签名Y-

请求示例

{
    "pay_memberid": "100001",
    "pay_orderid": "20260414150000001",
    "pay_sign": "A1B2C3D4E5F6..."
}

响应示例

{
    "code": 200,
    "message": "操作成功",
    "data": {
        "memberid": "100001",
        "orderid": "20260404150000001",
        "amount": "1000.00",
        "time_end": "2026-04-04T15:10:00.000Z",
        "transaction_id": "P20260404150000654321",
        "returncode": "00",
        "trade_state": "SUCCESS",
        "sign": "A1B2C3D4..."
    },
    "timestamp": 1775551800000
}
参数名含义签名说明
data.memberid商户号Y平台分配的商户号
data.orderid商户订单号Y原样返回下单时的 pay_orderid
data.amount付款金额Y保留2位小数,如 1000.00
data.time_end付款完成时间YISO 8601 格式,未完成时为空字符串
data.transaction_id平台流水号Y平台系统订单号(P开头=代付)
data.returncode请求状态Y00=查询成功 / 01=未完成
data.trade_state交易状态YNOTPAY=待付款 / SUCCESS=已付款 / CANCELLED=已取消 / EXPIRED=已过期
data.sign签名-平台对响应参数的签名,商户可验签确认合法性

五、余额查询

POST{网关地址}/api/v1/gateway/balance

请求参数

参数名含义必填签名
pay_memberid商户号YY
pay_orderid请求标识YY
pay_applydate查询时间YY
pay_sign签名Y-

请求示例

{
    "pay_memberid": "100001",
    "pay_orderid": "BAL20260414100000",
    "pay_applydate": "2026-04-14 10:00:00",
    "pay_sign": "A1B2C3D4E5F6..."
}

响应示例

{
    "code": 200,
    "message": "操作成功",
    "data": {
        "status": "1",
        "memberid": "100001",
        "balance": "50000.00",
        "frozen_balance": "3000.00"
    },
    "timestamp": 1775551800000
}
参数说明
codeHTTP状态码,200 为成功
data.status"1" 为成功
data.memberid商户号
data.balance可用余额
data.frozen_balance冻结余额(进行中订单占用)
余额查询接口响应不包含签名,仅在HTTPS环境下使用。

六、支付通道编码

通道编码通道名称类型说明
BANK_CARD银行卡转卡代收/代付默认通道,代收时需填写 pay_username
当前版本仅支持银行卡转卡通道。通道编码为可选参数,不传时默认使用银行卡通道。

七、签名示例代码

PHP

<?php
function createSign($params, $key) {
    unset($params['pay_sign'], $params['sign']);
    $params = array_filter($params, function($v) { return $v !== ''; });
    ksort($params);

    $stringA = '';
    foreach ($params as $k => $v) {
        $stringA .= $k . '=' . $v . '&';
    }
    $stringA = rtrim($stringA, '&');

    return strtoupper(hash_hmac('sha256', $stringA, $key));
}

$params = [
    'pay_memberid'    => '100001',
    'pay_orderid'     => '20260404143000001',
    'pay_applydate'   => '2026-04-04 14:30:00',
    'pay_notifyurl'   => 'https://merchant.com/notify',
    'pay_callbackurl' => 'https://merchant.com/callback',
    'pay_amount'      => '500.00',
];

$params['pay_sign'] = createSign($params, 'your_secret_key');

$ch = curl_init('https://platform.com/api/v1/gateway/pay');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
echo $response;

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;

public class PaySign {

    public static String createSign(Map<String, String> params, String key) {
        TreeMap<String, String> sorted = new TreeMap<>();
        for (Map.Entry<String, String> e : params.entrySet()) {
            if (!"pay_sign".equals(e.getKey()) && !"sign".equals(e.getKey())
                && e.getValue() != null && !e.getValue().isEmpty()) {
                sorted.put(e.getKey(), e.getValue());
            }
        }

        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String, String> e : sorted.entrySet()) {
            if (sb.length() > 0) sb.append("&");
            sb.append(e.getKey()).append("=").append(e.getValue());
        }

        return hmacSha256(sb.toString(), key).toUpperCase();
    }

    private static String hmacSha256(String data, String key) {
        try {
            Mac mac = Mac.getInstance("HmacSHA256");
            mac.init(new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"));
            byte[] bytes = mac.doFinal(data.getBytes("UTF-8"));
            StringBuilder sb = new StringBuilder();
            for (byte b : bytes) sb.append(String.format("%02x", b));
            return sb.toString();
        } catch (Exception e) { throw new RuntimeException(e); }
    }
}

Python

import hashlib, hmac, requests

def create_sign(params: dict, key: str) -> str:
    filtered = {k: v for k, v in params.items()
                if k not in ('pay_sign', 'sign') and v}
    sorted_keys = sorted(filtered.keys())
    string_a = '&'.join(f'{k}={filtered[k]}' for k in sorted_keys)

    return hmac.new(key.encode(), string_a.encode(), hashlib.sha256).hexdigest().upper()

params = {
    'pay_memberid':    '100001',
    'pay_orderid':     '20260404143000001',
    'pay_applydate':   '2026-04-04 14:30:00',
    'pay_notifyurl':   'https://merchant.com/notify',
    'pay_callbackurl': 'https://merchant.com/callback',
    'pay_amount':      '500.00',
}

params['pay_sign'] = create_sign(params, 'your_secret_key')
resp = requests.post('https://platform.com/api/v1/gateway/pay', data=params)
print(resp.json())

Node.js

const crypto = require('crypto');
const axios = require('axios');

function createSign(params, key) {
    const filtered = Object.entries(params)
        .filter(([k, v]) => !['pay_sign', 'sign'].includes(k) && v !== '')
        .sort(([a], [b]) => a.localeCompare(b));

    const stringA = filtered.map(([k, v]) => `${k}=${v}`).join('&');

    return crypto.createHmac('sha256', key)
        .update(stringA, 'utf8').digest('hex').toUpperCase();
}

const params = {
    pay_memberid:    '100001',
    pay_orderid:     '20260404143000001',
    pay_applydate:   '2026-04-04 14:30:00',
    pay_notifyurl:   'https://merchant.com/notify',
    pay_callbackurl: 'https://merchant.com/callback',
    pay_amount:      '500.00',
};

params.pay_sign = createSign(params, 'your_secret_key');

axios.post('https://platform.com/api/v1/gateway/pay', new URLSearchParams(params))
    .then(res => console.log(res.data));

Go

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net/http"
	"net/url"
	"sort"
	"strings"
)

// CreateSign 生成签名
func CreateSign(params map[string]string, key string) string {
	// 1. 过滤空值和签名字段,按 key 排序
	keys := make([]string, 0)
	for k, v := range params {
		if k == "pay_sign" || k == "sign" || v == "" {
			continue
		}
		keys = append(keys, k)
	}
	sort.Strings(keys)

	// 2. 拼接参数
	pairs := make([]string, 0, len(keys))
	for _, k := range keys {
		pairs = append(pairs, k+"="+params[k])
	}
	stringA := strings.Join(pairs, "&")

	// 3. 计算签名
	mac := hmac.New(sha256.New, []byte(key))
	mac.Write([]byte(stringA))
	return strings.ToUpper(hex.EncodeToString(mac.Sum(nil)))
}

func main() {
	params := map[string]string{
		"pay_memberid":    "100001",
		"pay_orderid":     "20260404143000001",
		"pay_applydate":   "2026-04-04 14:30:00",
		"pay_notifyurl":   "https://merchant.com/notify",
		"pay_callbackurl": "https://merchant.com/callback",
		"pay_amount":      "500.00",
	}

	secretKey := "your_secret_key"
	params["pay_sign"] = CreateSign(params, secretKey)

	// 发送请求
	form := url.Values{}
	for k, v := range params {
		form.Set(k, v)
	}

	resp, err := http.PostForm("https://platform.com/api/v1/gateway/pay", form)
	if err != nil {
		fmt.Println("请求失败:", err)
		return
	}
	defer resp.Body.Close()

	buf := make([]byte, 4096)
	n, _ := resp.Body.Read(buf)
	fmt.Println("响应:", string(buf[:n]))
}

回调验签示例 (Node.js)

// 生产环境用 Redis 存储已处理的 nonce,TTL 24小时
const processedNonces = new Set();

app.post('/notify', async (req, res) => {
    const { sign, attach, ...signParams } = req.body;

    // 1. 防重放:校验时间戳
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(signParams.timestamp)) > 300) {
        return res.send('FAIL');  // 超过5分钟,可能是重放
    }

    // 2. 防重复:校验 nonce
    if (processedNonces.has(signParams.nonce)) {
        return res.send('OK');   // 已处理过,直接返回OK
    }

    // 3. 验签
    const calculated = createSign(signParams, MERCHANT_KEY, 'HMAC-SHA256');
    if (calculated !== sign) {
        return res.send('FAIL'); // 签名不匹配
    }

    // 4. 验证交易状态
    if (signParams.returncode !== '00') {
        return res.send('FAIL');
    }

    // 5. 幂等:检查订单是否已处理
    const order = await getOrder(signParams.orderid);
    if (order.status === 'paid') {
        processedNonces.add(signParams.nonce);
        return res.send('OK');
    }

    // 6. 处理业务逻辑
    await updateOrderStatus(signParams.orderid, 'paid', {
        transactionId: signParams.transaction_id,
        amount: signParams.amount,
        paidAt: signParams.datetime,
    });

    // 7. 记录 nonce + 返回OK
    processedNonces.add(signParams.nonce);
    res.send('OK');
});

八、常见问题

Q1: 签名一直不通过?

  1. 确认参与签名的字段是否正确(参见各接口的「参与签名」列)
  2. 确认排序是否按参数名 ASCII 码升序(不是按值排序)
  3. 确认空值参数是否已排除
  4. HMAC-SHA256:密钥是 HMAC 的 key,不拼在字符串里
  5. 确认结果已转为大写
  6. 打印签名前的拼接字符串,逐字符核对

Q2: 回调一直收不到?

  1. 检查 pay_notifyurl 是否可从外网访问
  2. 检查服务器防火墙是否放行了平台IP
  3. 确认回调接口返回的是纯文本 OK,而非JSON或HTML
  4. 主动调用订单查询接口确认订单状态

Q3: 下单返回"暂无可用通道"?

  1. 当前通道可能繁忙,请稍后重试
  2. 如传入了 pay_bankcode,确认编码是否正确
  3. 确认该通道是否已在商户后台启用
  4. 联系平台确认通道状态

Q4: 代付下单返回"余额不足"?

代付需要商户预先充值。所需余额 = 付款金额 + 手续费(百分比 + 固定费用)。
例如:付款1000元,费率3%+2,则需余额 ≥ 1000 + 30 + 2 = 1032元

Q5: 订单超时了但用户已付款怎么办?

联系平台客服进行补单操作,平台确认到账后会重新回调通知。

附录:错误响应说明

所有错误响应格式统一为:

{
    "code": 403,
    "message": "签名错误",
    "data": null,
    "timestamp": 1775551851034
}

其中 code 为 HTTP 状态码,message 为中文错误描述。常见错误如下:

HTTP 状态码message原因
400金额无效金额为0、负数或非数字
400金额须在 X - Y 之间金额超出商户配置的范围
400xxx 必须是字符串 / 不能为空必填参数缺失或类型错误
400不允许的字段: xxx传入了接口未定义的参数
403缺少商户号未传 pay_memberid
403签名错误签名算法或密钥不正确
403商户已禁用商户被平台禁用
403商户代收功能未开启商户后台未开启代收
403商户代付功能未开启商户后台未开启代付
403IP不在白名单中请求IP不在商户配置的白名单
404商户不存在商户号错误
404订单不存在查询的订单号不存在
404暂无可用通道无匹配的收/付款产品
429请求过于频繁,请稍后再试超出API频率限制
500服务器内部错误平台异常,请联系客服
注意 商户对接时应优先判断 code(HTTP状态码),200 为成功,其他均为失败。message 为人类可读的错误描述,不建议作为程序判断依据(内容可能调整)。