虹桥支付 商户对接文档
版本 v1.0 · 最后更新 2026-05-25
1. 简介
虹桥支付 是一套支付聚合网关,向商户提供 代收(接收用户支付)和 代付(向用户付款)两类业务接口。本文档面向商户技术对接方,描述如何安全调用 虹桥支付 网关 API 并处理异步通知。
本套 API 的核心特征:
- 所有接口:HTTP POST,
Content-Type: application/json - 鉴权方式:基于商户 API Key 与 API Secret 的 MD5 签名(细节见 第 5 节)
- 响应格式统一:
{"code":"SUCCESS","message":"ok","data":{...},"sign":"..."} - 异步通知:订单终态时由 虹桥支付 推送 JSON 报文至商户的
notify_url,商户需校验签名后回success
2. 快速开始
完成首次对接的 5 个步骤:
- 获取凭证:联系 虹桥支付 运营开通商户号,登录商户后台领取
api_key与api_secret(见 第 6 节) - 实现签名工具函数:参考 第 5.3 节 多语言示例,把签名函数集成到你的系统
- 调用代收下单接口:构造请求 → 计算签名 → POST 到
/gateway/collect/order,拿到pay_url引导用户支付 - 实现 notify_url 接收回调:暴露一个 HTTP 端点接收 虹桥支付 的支付结果通知,验签后更新订单状态,回复
success - 实现订单主动查询:作为回调失败时的兜底,调用
/gateway/collect/query对账
3. 网关地址
https://hongqiao.uk/gateway
所有业务接口在此基地址下,路径如 POST /gateway/collect/order。
本文档地址:https://apihongqiao.uk/
4. 公共请求参数
每个业务请求的 JSON body 必须包含以下 4 个鉴权字段(与业务字段平铺在同一层级):
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
api_key | string | 必填 | 商户的 API Key,形如 ak_xxxx-xxxx-... |
timestamp | integer | 必填 | 当前 Unix 时间戳(秒,非毫秒)。服务端允许 ±300 秒漂移 |
request_id | string | 必填 | 请求唯一标识,建议用 UUID。10 分钟内全网不可重复(防重放) |
sign | string | 必填 | 签名值,算法见 第 5 节 |
timestamp 单位是 秒(10 位整数)。若你常对接的另一家上游用 13 位毫秒,请别搞混。5. 签名算法
5.1 算法步骤
- 把请求 JSON body 里 除
sign外的所有字段收集为键值对集合(包括api_key/timestamp/request_id这些鉴权字段,以及全部业务字段) - 过滤掉值为 空字符串 / null / 未传入的字段
- 把剩余字段按 key 的 ASCII 升序排序
- 按
key1=value1&key2=value2&...格式拼接成一个字符串(不要 URL 编码) - 在末尾追加
&key=API_SECRET(注意是字面字符串key=,不是你的api_key字段) - 对拼接结果做 MD5,取 32 位小写十六进制字符串作为
sign
5.2 完整签名计算示例
假设你的凭证:
api_key=ak_abc123api_secret=secret_xyz789
要发起的代收下单请求 body(含鉴权字段,不含 sign):
{
"api_key": "ak_abc123",
"timestamp": 1716566400,
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"merchant_order_no": "M20260524001",
"product_code": "ALIPAY",
"amount": 10000,
"currency": "CNY",
"subject": "Order 001",
"notify_url": "https://your-domain.com/notify"
}
步骤 1-3:按 key ASCII 升序排序:
amount = 10000
api_key = ak_abc123
currency = CNY
merchant_order_no = M20260524001
notify_url = https://your-domain.com/notify
product_code = ALIPAY
request_id = 550e8400-e29b-41d4-a716-446655440000
subject = Order 001
timestamp = 1716566400
步骤 4:拼接:
amount=10000&api_key=ak_abc123¤cy=CNY&merchant_order_no=M20260524001¬ify_url=https://your-domain.com/notify&product_code=ALIPAY&request_id=550e8400-e29b-41d4-a716-446655440000&subject=Order 001×tamp=1716566400
步骤 5:追加 &key=API_SECRET:
amount=10000&...×tamp=1716566400&key=secret_xyz789
步骤 6:MD5 小写:
sign = md5(上面的字符串).toLowerCase()
= "e3b0c44298fc1c149afbf4c8996fb924" // 示例值,实际计算结果以你的数据为准
把 sign 字段加入原 JSON body,发送 HTTP POST 请求。
5.3 多语言代码示例
import hashlib
def sign(params: dict, api_secret: str) -> str:
# 1. 过滤空值 + 去掉 sign 字段
filtered = {k: v for k, v in params.items()
if k != "sign" and v not in (None, "")}
# 2. ASCII 排序后拼接
items = sorted(filtered.items(), key=lambda x: x[0])
raw = "&".join(f"{k}={v}" for k, v in items)
# 3. 末尾追加 &key=secret,MD5 小写
raw += f"&key={api_secret}"
return hashlib.md5(raw.encode("utf-8")).hexdigest() # lowercase by defaultfunction sign(array $params, string $apiSecret): string {
unset($params['sign']);
$filtered = array_filter($params, fn($v) => $v !== null && $v !== '');
ksort($filtered);
$pairs = [];
foreach ($filtered as $k => $v) {
$pairs[] = "$k=$v";
}
$raw = implode('&', $pairs) . '&key=' . $apiSecret;
return md5($raw); // PHP md5() 默认小写
}import (
"crypto/md5"
"encoding/hex"
"fmt"
"sort"
"strings"
)
func Sign(params map[string]any, apiSecret string) string {
keys := make([]string, 0, len(params))
for k, v := range params {
if k == "sign" {
continue
}
sv := fmt.Sprintf("%v", v)
if sv == "" || sv == "<nil>" {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys)+1)
for _, k := range keys {
parts = append(parts, fmt.Sprintf("%s=%v", k, params[k]))
}
parts = append(parts, "key="+apiSecret)
h := md5.Sum([]byte(strings.Join(parts, "&")))
return hex.EncodeToString(h[:]) // 小写
}import java.security.MessageDigest;
import java.util.*;
public class Signer {
public static String sign(Map<String,Object> params, String apiSecret) {
Map<String,Object> sorted = new TreeMap<>();
for (Map.Entry<String,Object> e : params.entrySet()) {
if ("sign".equals(e.getKey())) continue;
Object v = e.getValue();
if (v == null || "".equals(v.toString())) continue;
sorted.put(e.getKey(), v);
}
StringBuilder sb = new StringBuilder();
for (Map.Entry<String,Object> e : sorted.entrySet()) {
if (sb.length() > 0) sb.append('&');
sb.append(e.getKey()).append('=').append(e.getValue());
}
sb.append("&key=").append(apiSecret);
try {
byte[] md = MessageDigest.getInstance("MD5")
.digest(sb.toString().getBytes("UTF-8"));
StringBuilder hex = new StringBuilder();
for (byte b : md) hex.append(String.format("%02x", b));
return hex.toString();
} catch (Exception e) { throw new RuntimeException(e); }
}
}const crypto = require('crypto');
function sign(params, apiSecret) {
const filtered = Object.entries(params)
.filter(([k, v]) => k !== 'sign' && v !== null && v !== undefined && v !== '')
.sort(([a], [b]) => a.localeCompare(b, 'en')); // ASCII order
const raw = filtered.map(([k, v]) => `${k}=${v}`).join('&')
+ `&key=${apiSecret}`;
return crypto.createHash('md5').update(raw, 'utf8').digest('hex'); // 小写
}fmt.Sprintf("%v", ...) 或语言原生 toString 规则转字符串。请确保你 生成签名时的字符串表示与 JSON 序列化后服务端看到的一致(特别注意:浮点数、布尔值、null)。6. 凭证管理
API Key/Secret 的获取流程(首次):
- 虹桥支付 管理员在运营后台为商户「签发凭证」
- 明文
api_secret仅在 Redis 缓存 24 小时,DB 内永久 AES 加密存储 - 商户登录商户后台「领取凭证」拿到一次性明文,自行妥善保管
- 逾期未领取需联系运营重新签发(旧的会作废)
api_secret 视同密码,不要硬编码到客户端、前端、移动 App。一旦泄露立即联系运营吊销并重新签发。7. 接口清单
7.1 代收下单
请求字段(除公共鉴权字段外):
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchant_order_no | string | 必填 | 商户单号,同商户下唯一 |
product_code | string | 必填 | 产品码,由 虹桥支付 配置(如 ALIPAY、BANK_CARD) |
amount | integer | 必填 | 下单金额,最小货币单位(如人民币以 分计:100 元 = 10000) |
currency | string | 必填 | 货币 ISO 代码,如 CNY |
subject | string | 可选 | 订单标题,显示给用户 |
notify_url | string | 可选 | 异步通知地址,订单终态时回调;空则不通知 |
响应 data 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
order_no | string | 系统订单号(雪花算法) |
pay_url | string | 支付跳转链接,引导用户访问完成支付 |
status | string | 订单初始状态,通常为 pending |
请求示例:
POST /gateway/collect/order HTTP/1.1
Host: hongqiao.uk
Content-Type: application/json
{
"api_key": "ak_abc123",
"timestamp": 1716566400,
"request_id": "550e8400-e29b-41d4-a716-446655440000",
"sign": "e3b0c44298fc1c149afbf4c8996fb924",
"merchant_order_no": "M20260524001",
"product_code": "ALIPAY",
"amount": 10000,
"currency": "CNY",
"subject": "Order 001",
"notify_url": "https://your-domain.com/notify/collect"
}
成功响应:
{
"code": "SUCCESS",
"message": "ok",
"data": {
"order_no": "C202605240001000123",
"pay_url": "https://upstream-pay.example.com/p/xxx",
"status": "pending"
},
"sign": "..."
}
7.2 代收订单查询
用于异步通知失败兜底、对账核对、补单等场景。
请求字段(order_no 与 merchant_order_no 二选一):
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
order_no | string | 二选一 | 虹桥支付 系统订单号 |
merchant_order_no | string | 二选一 | 商户单号 |
响应 data 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
order_no | string | 系统订单号 |
merchant_order_no | string | 商户单号 |
status | string | 订单状态,详见 第 10 节 |
currency | string | 货币代码 |
amount | integer | 下单金额(最小货币单位) |
fee_amount | integer | 手续费 |
actual_amount | integer | 实际到账金额 = amount − fee_amount |
channel_order_no | string | 上游渠道订单号(如有) |
subject | string | 订单标题 |
paid_at | integer | 支付成功时间 Unix 秒,未支付为 0 |
created_at | integer | 订单创建时间 Unix 秒 |
7.3 代付下单
请求字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchant_order_no | string | 必填 | 商户单号,同商户下唯一 |
product_code | string | 必填 | 产品码 |
amount | integer | 必填 | 代付金额(最小货币单位) |
currency | string | 必填 | 货币代码 |
payee_name | string | 必填 | 收款人姓名 |
payee_account | string | 必填 | 收款账号(银行卡号 / 电子钱包账户等) |
payee_bank_code | string | 可选 | 银行代码(银行卡代付时必填) |
payee_bank_name | string | 可选 | 银行名称 |
payee_country | string | 可选 | 收款人国家 ISO 代码 |
payee_extra | object | 可选 | 扩展信息(具体字段取决于产品类型) |
subject | string | 可选 | 订单标题 |
notify_url | string | 可选 | 异步通知地址 |
响应 data 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
order_no | string | 系统订单号 |
status | string | 初始状态,通常为 pending 或 reviewing |
payee_extra 怎么处理:嵌套对象通常 不参与签名(参考代码里只把顶层字符串字段加入签名集合)。如果你的对接出现 sign 不一致,请确认服务端实现,建议把嵌套对象先 JSON 序列化为字符串再作为字符串字段参与签名 / 或与运营对齐规则。7.4 代付订单查询
请求字段同 7.2(order_no 与 merchant_order_no 二选一)。
响应 data 字段:
| 字段 | 类型 | 说明 |
|---|---|---|
order_no | string | 系统订单号 |
merchant_order_no | string | 商户单号 |
status | string | 状态 |
currency | string | 货币代码 |
amount | integer | 代付金额 |
fee_amount | integer | 手续费 |
actual_amount | integer | 实际打给收款方 = amount − fee_amount |
channel_order_no | string | 上游渠道订单号 |
payee_name | string | 收款人姓名 |
payee_account | string | 收款账号(脱敏:前 4 + **** + 后 4) |
subject | string | 订单标题 |
created_at | integer | 订单创建时间 Unix 秒 |
8. 异步通知(回调)
8.1 触发时机
订单进入终态(代收的 success/failed,代付的 success/failed)时,若下单请求中提供了 notify_url,虹桥支付 会向该 URL POST JSON 报文。
8.2 通知 body 字段
代收通知:
| 字段 | 类型 | 说明 |
|---|---|---|
order_no | string | 系统订单号 |
merchant_order_no | string | 商户单号 |
status | string | 订单状态(success/failed) |
amount | integer | 订单金额 |
actual_amount | integer | 实际到账金额 |
currency | string | 货币代码 |
api_key | string | 商户 API Key(由网关签名时注入) |
sign | string | 签名值,需校验 |
代付通知:
| 字段 | 类型 | 说明 |
|---|---|---|
order_no | string | 系统订单号 |
merchant_order_no | string | 商户单号 |
status | string | 订单状态 |
amount | integer | 代付金额 |
currency | string | 货币代码 |
api_key | string | 商户 API Key |
sign | string | 签名值 |
8.3 验签步骤(商户必做)
- 解析 JSON body,提取
sign字段并保存 - 用 body 里剩余字段(含
api_key,去掉sign)按 第 5 节相同算法计算签名 - 用常量时间比对计算结果与请求里的
sign,相等才视为合法回调 - 对账:若验签通过但
order_no/merchant_order_no在你系统里查不到,不要触发任何业务动作
8.4 应答要求
商户系统处理成功后,需返回:
- HTTP 状态码
200 - 响应 body 为字符串
success(前后空格会被去除)
任何其他响应(非 200、body 不是 "success"、超时)都会被判定为失败,触发重试。
8.5 重试策略
- 首次发送:订单状态变化后约 30 秒内
- 失败重试:默认 最多 5 次,间隔逐步加大
- 5 次全失败后停止;商户应自行通过 查询接口兜底
- HTTP 请求超时:10 秒
8.6 幂等性
由于网络抖动、商户系统重启等原因,同一订单的回调可能被多次推送。商户系统必须做幂等处理:
- 用
order_no(或merchant_order_no)查本地订单 - 若已是终态,直接返回
success,不要重复入账
9. 错误码
响应顶层 code 字段值(字符串):
| code | 含义 | 建议处理 |
|---|---|---|
SUCCESS | 请求成功 | 解析 data 继续业务 |
SIGN_ERROR | 签名错误 / api_key 无效 / timestamp 过期 | 检查签名计算、时间同步、凭证有效性 |
MERCHANT_DISABLED | 商户被禁用 | 联系运营恢复 |
PARAM_ERROR | 请求参数错误(缺字段、格式不对等) | 检查请求字段是否齐全合法 |
PRODUCT_NOT_OPEN | 产品未开通或不可用 | 联系运营开通该 product_code |
AMOUNT_LIMIT_EXCEEDED | 金额超出产品限额 | 检查产品的单笔最小/最大金额 |
BALANCE_INSUFFICIENT | 商户余额不足(代付场景) | 充值或冻结代付订单 |
DUPLICATE_ORDER | 商户单号重复 / request_id 重放 | 换一个 merchant_order_no 或 request_id |
CHANNEL_UNAVAILABLE | 上游渠道暂不可用 | 稍后重试或换产品 |
ORDER_NOT_FOUND | 订单不存在(查询接口) | 检查订单号、商户号是否匹配 |
SYSTEM_ERROR | 系统内部错误 | 稍后重试,持续失败联系技术支持 |
10. 订单状态
代收订单状态
| 状态值 | 含义 | 是否终态 |
|---|---|---|
| pending | 待支付(已下单未出码 / 出码成功未付) | 否 |
| processing | 处理中(上游正在处理) | 否 |
| success | 支付成功 | 是(触发通知) |
| failed | 支付失败 | 是(触发通知) |
| closed | 订单已关闭(超时未支付) | 是 |
| refunded | 已退款 | 是 |
代付订单状态
| 状态值 | 含义 | 是否终态 |
|---|---|---|
| pending | 待审核 | 否 |
| reviewing | 审核中 | 否 |
| processing | 处理中(已发往上游) | 否 |
| success | 代付成功 | 是(触发通知) |
| failed | 代付失败 | 是(触发通知) |
| cancelled | 已取消 | 是 |
11. 完整对接示例(Python)
下面是一个最小可运行的 Python 示例,展示「下单 → 处理回调 → 主动查询兜底」完整链路。
11.1 公共:签名 + HTTP 调用
import hashlib
import json
import time
import uuid
import requests
API_BASE = "https://hongqiao.uk/gateway"
API_KEY = "ak_abc123..." # 你的 api_key
API_SECRET = "secret_xyz..." # 你的 api_secret
def make_sign(params, secret):
filtered = {k: v for k, v in params.items()
if k != "sign" and v not in (None, "")}
items = sorted(filtered.items(), key=lambda x: x[0])
raw = "&".join(f"{k}={v}" for k, v in items) + f"&key={secret}"
return hashlib.md5(raw.encode("utf-8")).hexdigest()
def call(path, biz_params):
params = {
"api_key": API_KEY,
"timestamp": int(time.time()),
"request_id": str(uuid.uuid4()),
**biz_params,
}
params["sign"] = make_sign(params, API_SECRET)
r = requests.post(API_BASE + path, json=params, timeout=15)
r.raise_for_status()
return r.json()
11.2 代收下单
res = call("/collect/order", {
"merchant_order_no": "M" + str(int(time.time())),
"product_code": "ALIPAY",
"amount": 10000, # 100 元
"currency": "CNY",
"subject": "Order 001",
"notify_url": "https://your-domain.com/notify/collect",
})
if res["code"] == "SUCCESS":
pay_url = res["data"]["pay_url"]
order_no = res["data"]["order_no"]
print(f"引导用户访问支付链接:{pay_url}")
else:
print(f"下单失败:[{res['code']}] {res['message']}")
11.3 接收异步通知
from flask import Flask, request
app = Flask(__name__)
@app.post("/notify/collect")
def notify_collect():
body = request.get_json()
# 1. 验签
incoming_sign = body.pop("sign", "")
expected = make_sign(body, API_SECRET)
if not _constant_time_eq(incoming_sign, expected):
return "fail", 400
# 2. 查本地订单,幂等处理
order = db.get_order(body["merchant_order_no"])
if order is None:
return "fail", 400
if order.status in ("success", "failed"):
return "success", 200 # 已处理过,直接 ACK
# 3. 更新订单状态
if body["status"] == "success":
db.mark_paid(order, actual_amount=body["actual_amount"])
else:
db.mark_failed(order)
return "success", 200
def _constant_time_eq(a, b):
if len(a) != len(b): return False
res = 0
for x, y in zip(a, b): res |= ord(x) ^ ord(y)
return res == 0
11.4 兜底查询
# 创建订单后若 10 分钟内没收到 notify,主动查一次
res = call("/collect/query", {
"merchant_order_no": "M1716566400",
})
if res["code"] == "SUCCESS":
d = res["data"]
print(f"订单 {d['order_no']} 当前状态:{d['status']}")
elif res["code"] == "ORDER_NOT_FOUND":
print("订单不存在(下单时其实没成功?)")
12. 常见问题
Q1:一直 SIGN_ERROR 怎么排查?
按顺序排查:
timestamp是不是秒(10 位整数),不是毫秒- 本机时间是否准确(与 NTP 偏差超过 ±300 秒会被拒)
- MD5 结果是 小写(许多 SDK 默认大写)
- 签名时是否包含了
api_key/timestamp/request_id这三个鉴权字段 - 签名时是否排除了
sign字段 - 空字符串字段是否过滤掉了(不要参与签名)
- 末尾拼接的是字面字符串
&key=,不是&api_key= api_secret是否完整(注意空格、换行)
Q2:request_id 一直 DUPLICATE_ORDER 怎么办?
同一个 request_id 10 分钟内只能用一次(防重放)。每次请求都生成新的 UUID。重试时也要换。
Q3:金额单位是元还是分?
最小货币单位。人民币是分(100 元 = 10000),美元是 cents。具体看 currency 对应的最小单位。
Q4:收不到 notify 怎么办?
三件事一起做:
- 检查
notify_url是否能从公网访问(不能是localhost、内网 IP) - 检查防火墙是否屏蔽了 虹桥支付 的回源 IP
- 实现兜底查询:下单后定时(如每分钟)调用查询接口,直至订单终态
Q5:通知失败重试间隔是多少?
默认 5 次,间隔逐步加大。具体值可联系运营查询并调整。
Q6:能不能批量下单?
当前网关不支持批量下单接口,需要客户端循环单笔下单。
Q7:怎么测试?
联系 虹桥支付 运营开通测试商户号 + 测试 product_code,下单后会接入 mock 渠道返回模拟支付链接,访问后可手动触发回调。
如有未覆盖问题,请联系 虹桥支付 技术支持。本文档源码在 docs/index.html,欢迎反馈不清晰之处。