商户交易API接口文档
本文档描述商户对接支付系统的完整API接口,供商户开发人员接入使用。
更新日志
| 日期 | 版本说明 |
|---|---|
| 2026-06-01 | 发布品牌化版本,迁移至自有平台 |
| 2026-06-06 | 重点补充签名说明及代码示例 |
🔐 签名规则(重点)
⚠️ 签名错误是最常见的接入问题!90%的接入失败都是签名计算错误导致的。请务必仔细阅读以下规则,参照代码示例进行对接。
一、请求签名(商户 → 平台)
商户调用所有接口时,需在请求中携带 app_id、timestamp、sign 三个鉴权参数。签名用于验证请求来源的合法性。
📝 签名公式
sign = MD5( app_id + app_secret + timestamp )
即:将 app_id、app_secret、timestamp 三个值直接拼接(中间无分隔符,无等号,无&符号),对拼接后的字符串做 MD5 运算,得到32位小写十六进制字符串。
鉴权参数说明
| 参数 | 必填 | 限制 | 类型 | 说明 |
|---|---|---|---|---|
app_id | 是 | 32 | string | 应用ID,在商户后台「密钥管理」中获取 |
timestamp | 是 | 10 | int | 请求时间戳,10位精确到秒(如 1717728000) |
sign | 是 | 32 | string | 请求参数签名,32位小写MD5 |
签名计算示例
已知参数:
app_id = "app_37b2ee23d733b7cb"
app_secret = "sec_a1b2c3d4e5f6g7h8"
timestamp = 1717728000
拼接字符串(无分隔符):
"app_37b2ee23d733b7cbsec_a1b2c3d4e5f6g7h81717728000"
MD5 计算:
sign = md5("app_37b2ee23d733b7cbsec_a1b2c3d4e5f6g7h81717728000")
= "e10adc3949ba59abbe56e057f20f883e" ← 32位小写
⚠️ 签名常见错误(务必避免):
- ❌ 用
&或=拼接三个值 —— 正确:直接拼接,无任何分隔符 - ❌
timestamp传了13位毫秒级 —— 正确:10位秒级 - ❌ MD5 结果转成了大写 —— 正确:32位小写
- ❌ 拼接顺序不对 —— 正确:app_id → app_secret → timestamp,顺序固定
- ❌
app_secret传错了值 —— 正确:商户后台「密钥管理」中显示的密钥
二、回调验签(平台 → 商户)
支付成功/退款成功后,平台会向商户的 notify_url 发送 POST 通知(application/json)。商户需验证签名以确保通知来自平台。
📝 验签公式
sign = MD5( key1=value1&key2=value2&...&keyN=valueN &key= app_secret )
规则:将回调 JSON 中所有字段(除 sign 本身和值为空的字段外)按 key 的 ASCII 升序排列,拼接成 key=value 格式,最后追加上 &key=***(***为 app_secret),对整个字符串做 MD5。
验签步骤
1. 接收回调 JSON 数据 2. 取出 sign 字段值备用 3. 移除 sign 字段,过滤掉值为空(""或null)的字段 4. 剩余字段按 key 的 ASCII 码升序排列(字母顺序) 5. 拼接:key1=value1&key2=value2&...&keyN=valueN 6. 末尾追加 &key=你的app_secret 7. 对整个字符串做 MD5,得到32位小写十六进制 8. 比对计算结果与回调中的 sign 字段是否一致
验签计算示例
回调数据:
{
"amount": "1.00",
"order_no": "SF20260606123456XXXXXX",
"out_order_no": "M202606060001",
"paid_at": "2026-06-06T12:00:00.000Z",
"status": "paid",
"trade_no": "4200001234202606061234567890",
"sign": "xxxxxxxx"
}
Step 1: 移除sign,按key升序排列
amount, order_no, out_order_no, paid_at, status, trade_no
Step 2: 拼接
amount=1.00&order_no=SF...&out_order_no=M...&paid_at=2026-06-06T12:00:00.000Z&status=paid&trade_no=4200...
Step 3: 追加 &key=***
...&trade_no=4200...&key=sec_a1b2c3d4e5f6g7h8
Step 4: MD5计算,与回调中sign对比
一致 → ✅ 验签通过
不一致 → ❌ 非平台通知,请忽略
💡 验签要点:
- 参与签名的字段 = 回调JSON中所有字段 - sign字段 - 值为空的字段
- 字段按 key的ASCII码升序排列(字母顺序:a→z)
- 回调参数后期可能会增加,验签时禁止固定写死字段列表,应动态遍历
- 处理成功后返回
"SUCCESS",否则平台会自动重试(最多5次,间隔递增)
签名代码示例
PHP 请求签名 + 回调验签
<?php
// ===== 请求签名 =====
$app_id = 'app_37b2ee23d733b7cb'; // 你的应用ID
$app_secret = 'sec_a1b2c3d4e5f6g7h8'; // 你的应用密钥
$timestamp = time(); // 10位秒级时间戳
// 拼接:app_id + app_secret + timestamp(无分隔符!)
$sign = md5($app_id . $app_secret . $timestamp);
// 构造请求参数
$params = [
'app_id' => $app_id,
'timestamp' => $timestamp,
'sign' => $sign,
'out_order_no' => 'M202606060001',
'amount' => '1.00',
'notify_url' => 'https://your-site.com/notify',
'return_url' => 'https://your-site.com/return',
'user_ip' => '123.456.789.012',
];
$ch = curl_init('https://zf.qqtkm.com/api/pay/order');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
// ===== 回调验签 =====
$notifyData = json_decode(file_get_contents('php://input'), true);
$receivedSign = $notifyData['sign'];
unset($notifyData['sign']);
$notifyData = array_filter($notifyData, function($v) { return $v !== '' && $v !== null; });
ksort($notifyData);
$signStr = '';
foreach ($notifyData as $k => $v) { $signStr .= ($signStr ? '&' : '') . $k . '=' . $v; }
$signStr .= '&key=' . $app_secret;
$calcSign = md5($signStr);
if ($calcSign === $receivedSign) {
echo 'SUCCESS'; // ✅ 验签通过
} else {
echo 'SIGN_ERROR'; // ❌ 验签失败
}
?>
Python 请求签名 + 回调验签
import hashlib, time, requests
app_id = 'app_37b2ee23d733b7cb'
app_secret = 'sec_a1b2c3d4e5f6g7h8'
timestamp = int(time.time())
# ===== 请求签名 =====
sign_str = app_id + app_secret + str(timestamp) # 无分隔符!
sign = hashlib.md5(sign_str.encode('utf-8')).hexdigest()
params = {
'app_id': app_id, 'timestamp': timestamp, 'sign': sign,
'out_order_no': 'M202606060001', 'amount': '1.00',
'notify_url': 'https://your-site.com/notify',
'return_url': 'https://your-site.com/return',
'user_ip': '123.456.789.012',
}
resp = requests.post('https://zf.qqtkm.com/api/pay/order', json=params)
print(resp.json())
# ===== 回调验签(Flask示例)=====
from flask import Flask, request
app = Flask(__name__)
@app.route('/notify', methods=['POST'])
def notify():
data = request.json
received_sign = data.pop('sign', '')
filtered = {k: v for k, v in data.items() if v != '' and v is not None}
sign_str = '&'.join(f'{k}={v}' for k, v in sorted(filtered.items()))
sign_str += f'&key={app_secret}'
calc_sign = hashlib.md5(sign_str.encode('utf-8')).hexdigest()
return 'SUCCESS' if calc_sign == received_sign else 'SIGN_ERROR'
Java 请求签名
import java.security.MessageDigest;
String appId = "app_37b2ee23d733b7cb";
String appSecret = "sec_a1b2c3d4e5f6g7h8";
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String signStr = appId + appSecret + timestamp; // 无分隔符!
String sign = md5(signStr);
static String md5(String input) {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(input.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte b : digest) sb.append(String.format("%02x", b & 0xff));
return sb.toString(); // 32位小写
}
常见签名问题排查
| 错误现象 | 原因 | 解决方法 |
|---|---|---|
接口返回 code: 1003, msg: "签名错误" | 签名计算有误 | 对照上方签名公式检查拼接方式、时间戳位数、大小写 |
拼接用了 app_id=xxx&app_secret=xxx | 错误使用键值对拼接 | 直接拼接值,不要加key=,正确:app_id + app_secret + timestamp |
| timestamp 传了13位 | 毫秒级时间戳 | 改为10位秒级:time()(PHP)/int(time.time())(Python) |
| sign 结果是大写 | MD5输出格式错误 | 转为32位小写 |
| 回调验签不通过 | 字段顺序错误或漏字段 | 必须按key升序,动态遍历所有非空字段,不要写死字段名 |
| 提示"提交时间与系统时间相差超过60秒" | 服务器时间不准 | 检查服务器时间为北京时间 |
API目录
| 分类 | 接口中文名 | 描述 |
|---|---|---|
| 下单接口 | 统一下单 | 主扫,C扫B。消费者主动扫码支付。 |
| 查询接口 | 订单查询 | 查询订单支付状态 |
| 退款接口 | 订单退款 | 对已支付订单发起退款 |
| 退款查询 | 退款查询 | 查询退款状态 |
公共请求参数
所有接口均需携带以下公共参数:
| 变量名 | 必填 | 限制 | 类型 | 说明 |
|---|---|---|---|---|
app_id | 是 | 32 | string | 应用APPID,在商户后台「密钥管理」中获取 |
timestamp | 是 | 10 | int | 请求时间戳,10位精确到秒 |
sign | 是 | 32 | string | 请求参数签名,签名规则 |
user_ipdescriptionsub_mch_id请求数据示例
{
"app_id": "app_37b2ee23d733b7cb",
"timestamp": 1717728000,
"sign": "e10adc3949ba59abbe56e057f20f883e",
"out_order_no": "M202606060001",
"amount": "1.00",
"notify_url": "https://your-site.com/notify",
"return_url": "https://your-site.com/return",
"user_ip": "123.456.789.012",
"description": "商品标题"
}
返回参数
| 变量名 | 必填 | 类型 | 说明 |
|---|---|---|---|
code | 是 | int | 返回状态,0=成功,其它均为失败 |
msg | 是 | string | 返回信息 |
data.order_no | 是 | string | 平台订单号 |
data.pay_url | 是 | string | 支付链接,跳转给用户完成支付 |
返回数据示例
{
"code": 0,
"msg": "success",
"data": {
"order_no": "SF20260606123456XXXXXX",
"pay_url": "https://zf.qqtkm.com/pay/go/SF20260606123456XXXXXX"
}
}
2、订单查询
支付完成后可调用该接口查询订单状态。
GET /api/pay/query/:order_no请求参数(Query String)
| 变量名 | 必填 | 类型 | 说明 |
|---|---|---|---|
app_id | 是 | string | 应用APPID |
timestamp | 是 | int | 10位秒级时间戳 |
sign | 是 | string | 签名 |
请求示例
GET /api/pay/query/SF20260606123456XXXXXX?app_id=app_xxx×tamp=1717728000&sign=abc123
返回参数
| 变量名 | 必填 | 类型 | 说明 |
|---|---|---|---|
code | 是 | int | 返回状态,0=成功 |
msg | 是 | string | 返回信息 |
data.order_no | 是 | string | 平台订单号 |
data.out_order_no | 是 | string | 商户订单号 |
data.trade_no | 否 | string | 微信交易号 |
data.amount | 是 | decimal | 订单金额 |
data.fee | 是 | decimal | 手续费 |
data.status | 是 | string | 订单状态:pending/paid/refunded/closed |
data.paid_at | 否 | string | 支付成功时间 |
data.created_at | 是 | string | 订单创建时间 |
3、订单退款
支付完成后可调用该接口进行退款。当前仅支持全额退款。
POST /api/pay/refund请求参数
| 变量名 | 必填 | 限制 | 类型 | 说明 |
|---|---|---|---|---|
out_order_no | 是 | 64 | string | 商户订单号 |
请求数据示例
{
"app_id": "app_37b2ee23d733b7cb",
"timestamp": 1717728000,
"sign": "e10adc3949ba59abbe56e057f20f883e",
"out_order_no": "M202606060001"
}
返回参数
| 变量名 | 必填 | 类型 | 说明 |
|---|---|---|---|
code | 是 | int | 0=退款成功,其它均为失败 |
msg | 是 | string | 返回信息 |
4、订单退款查询
退款发起后,可通过订单查询接口查看退款状态。当订单状态为 refunded 时表示退款成功。
5、异步交易通知
由于诸多因素都会影响到支付结果的通知,推送可能存在延迟,并且不能保证100%推送成功。故强烈建议主接交易查询接口。
注意:为保障异步队列正常运行,系统限制异步请求时间5秒内处理完毕并返回正确报文。超过5秒系统自动中断,将会导致您的服务器无法正常收到异步交易通知。建议接收异步通知后2秒内处理完毕并返回正确报文。
如果第一次通知不成功或者返回异常,系统将会重发通知,通知频率:15秒→30秒→60秒→120秒→300秒,最多5次。
通知参数(后期可能根据不同情况新增参数,请做好处理)
| 变量名 | 必填 | 限制 | 类型 | 说明 |
|---|---|---|---|---|
out_order_no | 是 | 64 | string | 商户订单号 |
order_no | 是 | 64 | string | 平台订单号 |
trade_no | 否 | 64 | string | 微信交易号 |
amount | 是 | 11 | decimal | 订单金额 |
status | 是 | 10 | string | 订单状态,固定值 paid=支付成功 |
paid_at | 是 | 32 | string | 支付成功时间(ISO 8601格式) |
sign | 是 | 32 | string | 签名,验签方式 |
通知数据示例
{
"out_order_no": "M202606060001",
"order_no": "SF20260606123456XXXXXX",
"trade_no": "4200001234202606061234567890",
"amount": "1.00",
"status": "paid",
"paid_at": "2026-06-06T12:00:00.000Z",
"sign": "a1b2c3d4e5f6..."
}
注意:
- 所有通知参数除空值与签名串外均需加入验签字段,通知参数后期可能会增加,验签时请动态遍历,禁止固定写死验签字段。
- 接收到通知并处理成功后需返回
"success"或"SUCCESS"。 - 回调验签请参照验签规则,使用 app_secret 进行验签。
6、退款异步通知
退款成功后,平台同样向商户的 notify_url 发送POST通知。通知规则与交易通知一致,5秒超时限制,处理成功后返回 "success"。
通知参数
| 变量名 | 必填 | 限制 | 类型 | 说明 |
|---|---|---|---|---|
out_order_no | 是 | 64 | string | 商户订单号 |
order_no | 是 | 64 | string | 平台订单号 |
trade_no | 否 | 64 | string | 微信交易号 |
amount | 是 | 12 | string | 原订单金额 |
refund_amount | 是 | 12 | string | 退款金额 |
status | 是 | 10 | string | refunded=退款完成, refunding=退款中 |
refunded_at | 否 | 32 | string | 退款完成时间(退款中为null) |
sign | 是 | 32 | string | 签名 |
7、错误码说明
| 错误码 | 说明 |
|---|---|
0 | 成功 |
1001 | 缺少鉴权参数(app_id/timestamp/sign) |
1003 | ❌ 签名错误(最常见的接入问题,请检查签名算法) |
2001 | 参数错误(金额/订单号等) |
2002 | 应用ID不存在或已禁用 |
2003 | 无可用商户号 |
2004 | 订单不存在 / 订单已退款 |
2005 | 接口已关闭 |
2006 | 商户订单号重复 |
2010 | 订单已标记线下退款,无法重复退款 |
3001 | 上游支付未配置 |
3002 | 上游下单失败 |
500 | 系统内部错误 |
💡 接入帮助:如遇接入问题,请重点检查签名计算方式。可使用商户后台的「密钥管理」功能查看 app_id 和 app_secret。