虹桥支付 商户对接文档

版本 v1.0 · 最后更新 2026-05-25

1. 简介

虹桥支付 是一套支付聚合网关,向商户提供 代收(接收用户支付)和 代付(向用户付款)两类业务接口。本文档面向商户技术对接方,描述如何安全调用 虹桥支付 网关 API 并处理异步通知。

本套 API 的核心特征:

2. 快速开始

完成首次对接的 5 个步骤:

  1. 获取凭证:联系 虹桥支付 运营开通商户号,登录商户后台领取 api_keyapi_secret(见 第 6 节
  2. 实现签名工具函数:参考 第 5.3 节 多语言示例,把签名函数集成到你的系统
  3. 调用代收下单接口:构造请求 → 计算签名 → POST 到 /gateway/collect/order,拿到 pay_url 引导用户支付
  4. 实现 notify_url 接收回调:暴露一个 HTTP 端点接收 虹桥支付 的支付结果通知,验签后更新订单状态,回复 success
  5. 实现订单主动查询:作为回调失败时的兜底,调用 /gateway/collect/query 对账

3. 网关地址

生产环境
https://hongqiao.uk/gateway

所有业务接口在此基地址下,路径如 POST /gateway/collect/order

本文档地址:https://apihongqiao.uk/

4. 公共请求参数

每个业务请求的 JSON body 必须包含以下 4 个鉴权字段(与业务字段平铺在同一层级):

字段类型必填说明
api_keystring必填商户的 API Key,形如 ak_xxxx-xxxx-...
timestampinteger必填当前 Unix 时间戳(,非毫秒)。服务端允许 ±300 秒漂移
request_idstring必填请求唯一标识,建议用 UUID。10 分钟内全网不可重复(防重放)
signstring必填签名值,算法见 第 5 节
注意timestamp 单位是 (10 位整数)。若你常对接的另一家上游用 13 位毫秒,请别搞混。

5. 签名算法

5.1 算法步骤

  1. 把请求 JSON body 里 sign的所有字段收集为键值对集合(包括 api_key/timestamp/request_id 这些鉴权字段,以及全部业务字段
  2. 过滤掉值为 空字符串 / null / 未传入的字段
  3. 把剩余字段按 key 的 ASCII 升序排序
  4. key1=value1&key2=value2&... 格式拼接成一个字符串(不要 URL 编码
  5. 在末尾追加 &key=API_SECRET(注意是字面字符串 key=,不是你的 api_key 字段)
  6. 对拼接结果做 MD5,取 32 位小写十六进制字符串作为 sign
关键差异:MD5 结果为 小写(与某些上游的大写规范不同)。如果签名一直对不上,先确认大小写。

5.2 完整签名计算示例

假设你的凭证:

要发起的代收下单请求 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&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

步骤 5:追加 &key=API_SECRET

amount=10000&...&timestamp=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 default
function 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'); // 小写
}
提示:上面的代码对 number/boolean 值会按 fmt.Sprintf("%v", ...) 或语言原生 toString 规则转字符串。请确保你 生成签名时的字符串表示与 JSON 序列化后服务端看到的一致(特别注意:浮点数、布尔值、null)。

6. 凭证管理

API Key/Secret 的获取流程(首次):

  1. 虹桥支付 管理员在运营后台为商户「签发凭证
  2. 明文 api_secret 仅在 Redis 缓存 24 小时,DB 内永久 AES 加密存储
  3. 商户登录商户后台「领取凭证」拿到一次性明文,自行妥善保管
  4. 逾期未领取需联系运营重新签发(旧的会作废)
安全建议api_secret 视同密码,不要硬编码到客户端、前端、移动 App。一旦泄露立即联系运营吊销并重新签发。

7. 接口清单

7.1 代收下单

POST/gateway/collect/order

请求字段(除公共鉴权字段外):

字段类型必填说明
merchant_order_nostring必填商户单号,同商户下唯一
product_codestring必填产品码,由 虹桥支付 配置(如 ALIPAYBANK_CARD
amountinteger必填下单金额,最小货币单位(如人民币以 计:100 元 = 10000
currencystring必填货币 ISO 代码,如 CNY
subjectstring可选订单标题,显示给用户
notify_urlstring可选异步通知地址,订单终态时回调;空则不通知

响应 data 字段

字段类型说明
order_nostring系统订单号(雪花算法)
pay_urlstring支付跳转链接,引导用户访问完成支付
statusstring订单初始状态,通常为 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 代收订单查询

POST/gateway/collect/query

用于异步通知失败兜底、对账核对、补单等场景。

请求字段order_nomerchant_order_no 二选一):

字段类型必填说明
order_nostring二选一虹桥支付 系统订单号
merchant_order_nostring二选一商户单号

响应 data 字段

字段类型说明
order_nostring系统订单号
merchant_order_nostring商户单号
statusstring订单状态,详见 第 10 节
currencystring货币代码
amountinteger下单金额(最小货币单位)
fee_amountinteger手续费
actual_amountinteger实际到账金额 = amount − fee_amount
channel_order_nostring上游渠道订单号(如有)
subjectstring订单标题
paid_atinteger支付成功时间 Unix 秒,未支付为 0
created_atinteger订单创建时间 Unix 秒

7.3 代付下单

POST/gateway/payout/order

请求字段

字段类型必填说明
merchant_order_nostring必填商户单号,同商户下唯一
product_codestring必填产品码
amountinteger必填代付金额(最小货币单位)
currencystring必填货币代码
payee_namestring必填收款人姓名
payee_accountstring必填收款账号(银行卡号 / 电子钱包账户等)
payee_bank_codestring可选银行代码(银行卡代付时必填)
payee_bank_namestring可选银行名称
payee_countrystring可选收款人国家 ISO 代码
payee_extraobject可选扩展信息(具体字段取决于产品类型)
subjectstring可选订单标题
notify_urlstring可选异步通知地址

响应 data 字段

字段类型说明
order_nostring系统订单号
statusstring初始状态,通常为 pendingreviewing
签名时 payee_extra 怎么处理:嵌套对象通常 不参与签名(参考代码里只把顶层字符串字段加入签名集合)。如果你的对接出现 sign 不一致,请确认服务端实现,建议把嵌套对象先 JSON 序列化为字符串再作为字符串字段参与签名 / 或与运营对齐规则。

7.4 代付订单查询

POST/gateway/payout/query

请求字段同 7.2order_nomerchant_order_no 二选一)。

响应 data 字段

字段类型说明
order_nostring系统订单号
merchant_order_nostring商户单号
statusstring状态
currencystring货币代码
amountinteger代付金额
fee_amountinteger手续费
actual_amountinteger实际打给收款方 = amount − fee_amount
channel_order_nostring上游渠道订单号
payee_namestring收款人姓名
payee_accountstring收款账号(脱敏:前 4 + **** + 后 4)
subjectstring订单标题
created_atinteger订单创建时间 Unix 秒

8. 异步通知(回调)

8.1 触发时机

订单进入终态(代收的 success/failed,代付的 success/failed)时,若下单请求中提供了 notify_url,虹桥支付 会向该 URL POST JSON 报文。

8.2 通知 body 字段

代收通知

字段类型说明
order_nostring系统订单号
merchant_order_nostring商户单号
statusstring订单状态(success/failed
amountinteger订单金额
actual_amountinteger实际到账金额
currencystring货币代码
api_keystring商户 API Key(由网关签名时注入)
signstring签名值,需校验

代付通知

字段类型说明
order_nostring系统订单号
merchant_order_nostring商户单号
statusstring订单状态
amountinteger代付金额
currencystring货币代码
api_keystring商户 API Key
signstring签名值

8.3 验签步骤(商户必做)

  1. 解析 JSON body,提取 sign 字段并保存
  2. 用 body 里剩余字段(含 api_key,去掉 sign)按 第 5 节相同算法计算签名
  3. 常量时间比对计算结果与请求里的 sign,相等才视为合法回调
  4. 对账:若验签通过但 order_no/merchant_order_no 在你系统里查不到,不要触发任何业务动作

8.4 应答要求

商户系统处理成功后,需返回:

任何其他响应(非 200、body 不是 "success"、超时)都会被判定为失败,触发重试。

8.5 重试策略

8.6 幂等性

由于网络抖动、商户系统重启等原因,同一订单的回调可能被多次推送。商户系统必须做幂等处理:

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_norequest_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 怎么排查?

按顺序排查:

  1. timestamp 是不是(10 位整数),不是毫秒
  2. 本机时间是否准确(与 NTP 偏差超过 ±300 秒会被拒)
  3. MD5 结果是 小写(许多 SDK 默认大写)
  4. 签名时是否包含了 api_key/timestamp/request_id 这三个鉴权字段
  5. 签名时是否排除了 sign 字段
  6. 空字符串字段是否过滤掉了(不要参与签名)
  7. 末尾拼接的是字面字符串 &key=,不是 &api_key=
  8. api_secret 是否完整(注意空格、换行)

Q2:request_id 一直 DUPLICATE_ORDER 怎么办?

同一个 request_id 10 分钟内只能用一次(防重放)。每次请求都生成新的 UUID。重试时也要换。

Q3:金额单位是元还是分?

最小货币单位。人民币是分(100 元 = 10000),美元是 cents。具体看 currency 对应的最小单位。

Q4:收不到 notify 怎么办?

三件事一起做:

  1. 检查 notify_url 是否能从公网访问(不能是 localhost、内网 IP)
  2. 检查防火墙是否屏蔽了 虹桥支付 的回源 IP
  3. 实现兜底查询:下单后定时(如每分钟)调用查询接口,直至订单终态

Q5:通知失败重试间隔是多少?

默认 5 次,间隔逐步加大。具体值可联系运营查询并调整。

Q6:能不能批量下单?

当前网关不支持批量下单接口,需要客户端循环单笔下单。

Q7:怎么测试?

联系 虹桥支付 运营开通测试商户号 + 测试 product_code,下单后会接入 mock 渠道返回模拟支付链接,访问后可手动触发回调。


如有未覆盖问题,请联系 虹桥支付 技术支持。本文档源码在 docs/index.html,欢迎反馈不清晰之处。