23 KiB
Pay-Bridge 支付网关接入文档
| 版本 | 日期 | 说明 |
|---|---|---|
| v1.2 | 2026-02-28 | 新增商户进件接口(SaaS 多商户场景)、统一下单支持 merchant_id |
| v1.1 | 2026-02-28 | 更新接入凭证申请方式 |
| v1.0 | 2026-02-28 | 初始版本 |
目录
1. 接入准备
1.1 获取接入凭证
接入凭证由平台管理员在管理后台创建。请联系平台管理员,按以下步骤为你的系统开通接入权限:
- 管理员登录管理后台,进入「接入应用」模块
- 点击「新建应用」,填写应用名称(如:商城系统、ERP 系统)
- 系统自动生成
app_id和app_secret,Secret 仅在创建时展示一次 - 管理员将凭证告知接入方
重要提示:
app_secret创建后无法再次查看,如遗失需由管理员在后台执行「重置密钥」操作,旧密钥立即失效。
获得的凭证如下:
| 参数 | 示例 | 说明 |
|---|---|---|
app_id |
app_2602280a3f1b7c2d |
应用唯一标识,格式为 app_ + 日期 + 随机串 |
app_secret |
A3F8C2D1E4B7F9A0... |
64 位十六进制字符串,用于请求签名,请妥善保管,切勿泄露或提交至代码仓库 |
1.2 接口基础地址
| 环境 | 地址 |
|---|---|
| 生产环境 | https://pay.your-domain.com |
| 沙箱环境 | https://sandbox-pay.your-domain.com |
2. 请求规范
2.1 基本要求
- 协议:HTTPS
- 方法:POST(下单/退款/关闭)、GET(查询)
- 编码:UTF-8
- Content-Type:
application/json
2.2 公共请求头
所有请求必须携带以下 Header:
| Header | 类型 | 必填 | 说明 |
|---|---|---|---|
X-App-Id |
string | 是 | 平台分配的 app_id |
X-Timestamp |
string | 是 | 当前 Unix 时间戳(秒),与服务器时间差须在 5 分钟以内 |
X-Sign |
string | 是 | 请求签名,见签名算法 |
Content-Type |
string | 是 | application/json |
示例:
POST /api/v1/pay/unified-order HTTP/1.1
Host: pay.your-domain.com
Content-Type: application/json
X-App-Id: app_20260101001
X-Timestamp: 1740700800
X-Sign: a3f8c2d1e4b7f9a0c5d2e8f1b4a7c3d6e9f2b5a8c1d4e7f0a3b6c9d2e5f8a1b4
3. 签名算法
3.1 算法说明
签名算法为 HMAC-SHA256,签名结果为 十六进制小写字符串。
3.2 签名步骤
第一步:拼接签名原文
签名原文 = app_id + timestamp + requestBody
app_id:Header 中的 X-App-Idtimestamp:Header 中的 X-TimestamprequestBody:请求体原始 JSON 字符串(GET 请求时为空字符串"")
第二步:HMAC-SHA256 签名
sign = HMAC-SHA256(签名原文, app_secret)
第三步:Hex 编码,填入 X-Sign Header
3.3 注意事项
- 请求体 JSON 中字段顺序不影响签名,但序列化后的字节内容必须与实际发送的 body 完全一致
- 时间戳精度为秒,不是毫秒
- GET 请求 body 为空字符串参与签名
4. 统一响应格式
所有接口均返回 JSON,HTTP 状态码为 200(业务错误也返回 200,通过 code 字段区分)。
成功响应:
{
"code": "0",
"message": "success",
"data": { ... },
"trace_id": "7f3a2b1c4d5e6f7a"
}
失败响应:
{
"code": "30004",
"message": "退款金额超过可退金额",
"trace_id": "7f3a2b1c4d5e6f7a"
}
| 字段 | 类型 | 说明 |
|---|---|---|
code |
string | "0" 为成功,其他为错误码,见错误码表 |
message |
string | 提示信息 |
data |
object | 业务数据,仅成功时返回 |
trace_id |
string | 请求追踪 ID,排查问题时提供给平台 |
5. 支付接口
5.1 统一下单
接口地址
POST /api/v1/pay/unified-order
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchant_order_no |
string | 是 | 商户系统订单号,同一 app_id 下唯一,最长 64 位 |
pay_method |
string | 是 | 支付方式,见下方枚举 |
amount |
int64 | 是 | 订单金额,单位分,最小 1 |
subject |
string | 是 | 商品描述,最长 256 位 |
notify_url |
string | 是 | 支付结果异步通知地址,须为合法 HTTPS URL |
expire_minutes |
int | 否 | 订单有效期(分钟),默认 30 分钟 |
profit_sharing_amount |
int64 | 否 | 分润金额(分),0 表示不分润 |
extra |
object | 否 | 支付方式附加参数,见下方说明 |
merchant_id |
string | 否 | 指定收款商户 ID(SaaS 多商户场景),须为本 app_id 下已进件审核通过的商户 |
pay_method 枚举值
| 值 | 说明 |
|---|---|
WECHAT_JSAPI |
微信公众号支付 |
WECHAT_H5 |
微信 H5 支付 |
WECHAT_NATIVE |
微信扫码支付 |
WECHAT_MINI |
微信小程序支付 |
ALIPAY |
支付宝扫码支付 |
QUICK_PAY |
快捷支付 |
extra 附加参数说明
| 支付方式 | 参数 | 说明 |
|---|---|---|
WECHAT_JSAPI |
openid(必填) |
用户在公众号的 openid |
WECHAT_MINI |
openid(必填) |
用户在小程序的 openid |
WECHAT_JSAPI / WECHAT_MINI |
sub_appid(可选) |
子商户公众号/小程序 AppID |
请求示例
{
"merchant_order_no": "ORD20260228001",
"pay_method": "WECHAT_JSAPI",
"amount": 9900,
"subject": "会员充值 - 月卡",
"notify_url": "https://your-server.com/callback/pay",
"expire_minutes": 15,
"extra": {
"openid": "oxxxxxxxxxxxxxxxxxxxxxxx"
}
}
成功响应 data
| 字段 | 类型 | 说明 |
|---|---|---|
trade_no |
string | 平台交易号,格式 PAYyyMMddXXXXXXXX |
pay_credential |
object | 支付凭证,透传给前端拉起支付,格式因支付方式不同而异 |
is_idempotent |
bool | true 表示该订单号已存在,返回的是已有订单信息 |
响应示例
{
"code": "0",
"message": "success",
"data": {
"trade_no": "PAY26022800000001",
"pay_credential": {
"appId": "wx1234567890",
"timeStamp": "1740700800",
"nonceStr": "abc123xyz",
"package": "prepay_id=wx28...",
"signType": "RSA",
"paySign": "..."
},
"is_idempotent": false
}
}
幂等说明:使用相同的
merchant_order_no重复调用,不会重复下单,会直接返回已有订单的支付凭证,并且is_idempotent为true。
5.2 查询订单
接口地址
GET /api/v1/pay/query/{trade_no}
路径参数
| 参数 | 类型 | 说明 |
|---|---|---|
trade_no |
string | 平台交易号(下单时返回的 trade_no) |
请求示例
GET /api/v1/pay/query/PAY26022800000001
成功响应 data
| 字段 | 类型 | 说明 |
|---|---|---|
trade_no |
string | 平台交易号 |
merchant_order_no |
string | 商户订单号 |
pay_method |
string | 支付方式 |
amount |
int64 | 订单金额(分) |
status |
string | 订单状态,见订单状态说明 |
channel_trade_no |
string | 渠道侧交易号 |
pay_time |
string | 支付成功时间,ISO8601 格式,未支付时为 null |
created_at |
string | 订单创建时间,ISO8601 格式 |
响应示例
{
"code": "0",
"message": "success",
"data": {
"trade_no": "PAY26022800000001",
"merchant_order_no": "ORD20260228001",
"pay_method": "WECHAT_JSAPI",
"amount": 9900,
"status": "PAID",
"channel_trade_no": "4200002345202602280000000001",
"pay_time": "2026-02-28T10:05:00Z",
"created_at": "2026-02-28T10:00:00Z"
}
}
5.3 关闭订单
关闭仍在支付中的订单,已支付订单不可关闭(请使用退款接口)。
接口地址
POST /api/v1/pay/close
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
trade_no |
string | 是 | 平台交易号 |
请求示例
{
"trade_no": "PAY26022800000001"
}
成功响应
{
"code": "0",
"message": "success"
}
5.4 申请退款
接口地址
POST /api/v1/pay/refund
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
trade_no |
string | 是 | 平台交易号 |
refund_amount |
int64 | 是 | 退款金额(分),不得超过原订单金额 |
reason |
string | 否 | 退款原因,最长 256 位 |
notify_url |
string | 否 | 退款结果异步通知地址 |
请求示例
{
"trade_no": "PAY26022800000001",
"refund_amount": 9900,
"reason": "用户申请退款",
"notify_url": "https://your-server.com/callback/refund"
}
成功响应 data
| 字段 | 类型 | 说明 |
|---|---|---|
refund_no |
string | 平台退款单号 |
trade_no |
string | 关联的平台交易号 |
refund_amount |
int64 | 退款金额(分) |
status |
string | 退款状态:PENDING / PROCESSING / SUCCESS / FAILED |
channel_refund_no |
string | 渠道侧退款单号 |
响应示例
{
"code": "0",
"message": "success",
"data": {
"refund_no": "REF26022800000001",
"trade_no": "PAY26022800000001",
"refund_amount": 9900,
"status": "PROCESSING",
"channel_refund_no": "50300807092026022800000000001"
}
}
注意:退款为异步处理,状态
PROCESSING表示已提交渠道,最终结果通过异步通知推送,建议同时通过查询接口轮询确认。
5.5 查询退款
接口地址
GET /api/v1/pay/refund/query/{refund_no}
路径参数
| 参数 | 类型 | 说明 |
|---|---|---|
refund_no |
string | 平台退款单号(申请退款时返回的 refund_no) |
成功响应 data
| 字段 | 类型 | 说明 |
|---|---|---|
refund_no |
string | 平台退款单号 |
trade_no |
string | 关联的平台交易号 |
refund_amount |
int64 | 退款金额(分) |
status |
string | 退款状态 |
channel_refund_no |
string | 渠道侧退款单号 |
refund_time |
string | 退款成功时间,ISO8601 格式,未完成时为 null |
6. 商户进件接口
适用场景:你的系统是 SaaS 平台,平台上的客户需要先完成进件(企业入网)才能以自己名义收款。
所有进件接口与支付接口使用相同的 HMAC 鉴权(
app_id+app_secret),且数据完全按app_id隔离——你只能看到自己名下的商户。
典型流程
1. 创建商户记录(获取 merchant_id)
↓
2. 上传营业执照等证件文件(获取 file_id)
↓
3. 提交进件申请(将 file_id 填入 submit_data)
↓
4. 轮询查询审核状态,直至 APPROVED
↓
5. 统一下单时传入 merchant_id,指定该商户收款
6.1 创建商户
接口地址
POST /api/v1/merchant
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
merchant_id |
string | 是 | 你方系统的商户唯一标识,最长 32 位,由调用方自定义 |
merchant_name |
string | 是 | 商户名称 |
license_no |
string | 否 | 营业执照号 |
legal_person |
string | 否 | 法定代表人姓名 |
bank_account |
string | 否 | 结算银行账号(脱敏存储) |
请求示例
{
"merchant_id": "tenant_0001",
"merchant_name": "示例科技有限公司",
"license_no": "91310000XXXXXXXXXX",
"legal_person": "张三",
"bank_account": "6222021234567890123"
}
成功响应 data
{
"merchant_id": "tenant_0001"
}
6.2 上传证件文件
接口地址
POST /api/v1/merchant/upload-file
请求格式:multipart/form-data
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
file |
file | 是 | 证件图片,支持 JPG / PNG / PDF |
file_media_type |
string | 是 | 文件类型代码,由渠道定义,如 01=营业执照、02=身份证正面 |
channel_code |
string | 否 | 渠道代码,默认 HEEPAY |
成功响应 data
{
"file_id": "3000000001234567"
}
上传返回的
file_id在提交进件时填入submit_data,有效期以渠道规定为准(通常 48 小时内使用)。
6.3 提交进件申请
接口地址
POST /api/v1/merchant/{merchant_id}/apply
路径参数
| 参数 | 说明 |
|---|---|
merchant_id |
创建商户时使用的商户 ID |
请求参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
channel_code |
string | 是 | 渠道代码,如 HEEPAY |
submit_data |
object | 否 | 渠道要求的进件业务参数,内容因渠道而异,详见各渠道对接手册 |
请求示例
{
"channel_code": "HEEPAY",
"submit_data": {
"business_license_no": "91310000XXXXXXXXXX",
"business_license_copy": "3000000001234567",
"id_card_front": "3000000001234568",
"id_card_back": "3000000001234569",
"contact_name": "张三",
"contact_phone": "138XXXXXXXX",
"bank_account_no": "6222021234567890123",
"bank_name": "中国工商银行"
}
}
成功响应 data
{
"application_id": "APP1a2b3c4d5e6f7a8b"
}
6.4 查询审核状态
接口地址
GET /api/v1/merchant/{merchant_id}/audit
成功响应 data
| 字段 | 类型 | 说明 |
|---|---|---|
application_id |
string | 进件申请 ID |
merchant_id |
string | 商户 ID |
channel_code |
string | 渠道代码 |
audit_status |
string | 审核状态,见下方枚举 |
reject_reason |
string | 拒绝原因,仅 REJECTED 时返回 |
submitted_at |
string | 提交时间 |
audited_at |
string | 审核完成时间,未完成时为 null |
audit_status 枚举值
| 值 | 说明 |
|---|---|
SUBMITTING |
提交中,正在调用渠道 |
REVIEWING |
审核中,渠道人工审核 |
APPROVED |
审核通过,商户可正常收款 |
REJECTED |
审核拒绝,查看 reject_reason 修改后重新提交 |
响应示例
{
"code": "0",
"message": "success",
"data": {
"application_id": "APP1a2b3c4d5e6f7a8b",
"merchant_id": "tenant_0001",
"channel_code": "HEEPAY",
"audit_status": "APPROVED",
"submitted_at": "2026-02-28T10:00:00Z",
"audited_at": "2026-02-28T14:30:00Z"
}
}
6.5 查询商户详情
接口地址
GET /api/v1/merchant/{merchant_id}
成功响应 data:返回完整商户对象,字段同创建时入参,额外包含:
| 字段 | 类型 | 说明 |
|---|---|---|
status |
string | 商户状态:PENDING / ACTIVE / FROZEN / REJECTED |
channel_merchant_id |
string | 渠道侧商户 ID,进件审核通过后由渠道下发 |
created_at |
string | 创建时间 |
如传入的
merchant_id不属于当前app_id,接口返回 404。
6.6 查询商户列表
接口地址
GET /api/v1/merchant
Query 参数
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
status |
string | 否 | 按状态过滤:PENDING / ACTIVE / FROZEN / REJECTED |
limit |
int | 否 | 每页数量,默认 20 |
offset |
int | 否 | 偏移量,默认 0 |
响应示例
{
"code": "0",
"message": "success",
"data": {
"list": [
{
"merchant_id": "tenant_0001",
"merchant_name": "示例科技有限公司",
"status": "ACTIVE",
"created_at": "2026-02-28T10:00:00Z"
}
],
"limit": 20,
"offset": 0
}
}
6.7 指定商户收款(统一下单扩展)
商户进件通过后,在统一下单时传入 merchant_id,平台会将该商户的渠道收款账号注入下单参数,实现分账到具体商户:
{
"merchant_order_no": "ORD20260228002",
"pay_method": "WECHAT_JSAPI",
"amount": 9900,
"subject": "订单支付",
"notify_url": "https://your-server.com/callback/pay",
"merchant_id": "tenant_0001",
"extra": {
"openid": "oxxxxxxxxxxxxxxxxxxxxxxx"
}
}
merchant_id必须属于当前app_id,且商户状态为ACTIVE,否则返回30001。
7. 异步通知
7.1 支付成功通知
用户支付成功后,平台将向下单时提供的 notify_url 发送 POST 请求。
通知内容
{
"trade_no": "PAY26022800000001",
"merchant_order_no": "ORD20260228001",
"status": "PAID",
"amount": 9900,
"pay_method": "WECHAT_JSAPI",
"channel_trade_no": "4200002345202602280000000001",
"pay_time": "2026-02-28T10:05:00Z"
}
7.2 退款结果通知
退款处理完成后,平台将向退款时提供的 notify_url 发送 POST 请求。
通知内容
{
"refund_no": "REF26022800000001",
"trade_no": "PAY26022800000001",
"merchant_order_no": "ORD20260228001",
"refund_amount": 9900,
"status": "SUCCESS",
"refund_time": "2026-02-28T10:30:00Z"
}
7.3 通知应答规则
接收通知后,你的服务器必须在 10 秒内响应以下内容:
HTTP 200
Body: success
返回任何其他内容,或超时未响应,平台将认为通知失败并重试。
7.4 重试策略
| 第 N 次 | 延迟 |
|---|---|
| 1 | 立即 |
| 2 | 15 秒后 |
| 3 | 30 秒后 |
| 4 | 1 分钟后 |
| 5 | 5 分钟后 |
| 6 | 30 分钟后 |
| 7 | 1 小时后 |
| 8 | 6 小时后 |
| 9 | 12 小时后 |
超过 9 次仍失败将停止重试,请通过查询接口主动同步最终状态。
7.5 防重处理
通知可能因网络原因重复发送,请以 trade_no 为唯一键做幂等处理,避免重复发货或重复入账。
8. 订单状态说明
8.1 交易订单状态
| 状态 | 说明 |
|---|---|
CREATING |
创建中,正在调用渠道下单 |
PAYING |
待支付,等待用户付款 |
PAID |
支付成功 |
CLOSED |
已关闭 |
FAILED |
支付失败 |
CREATE_FAILED |
下单失败,渠道返回错误 |
REFUNDED |
已全额退款 |
8.2 退款状态
| 状态 | 说明 |
|---|---|
PENDING |
待处理 |
PROCESSING |
处理中,已提交渠道 |
SUCCESS |
退款成功 |
FAILED |
退款失败,可联系平台处理 |
9. 错误码对照表
| 错误码 | HTTP 状态码 | 说明 | 处理建议 |
|---|---|---|---|
0 |
200 | 成功 | — |
10001 |
400 | 参数校验失败 | 检查请求参数格式和必填项 |
10002 |
400 | 缺少必填参数 | 补充缺少的参数 |
10003 |
400 | 不支持的支付方式 | 检查 pay_method 是否正确 |
10004 |
400 | 金额非法 | 金额须为正整数(分) |
20001 |
401 | 签名验证失败 | 检查签名算法和 app_secret |
20002 |
401 | 应用不存在或已禁用 | 联系平台确认 app_id 状态 |
30001 |
422 | 订单不存在 | 检查 trade_no 是否正确 |
30002 |
422 | 订单已支付 | 勿重复支付 |
30003 |
422 | 订单已关闭 | 需重新下单 |
30004 |
422 | 退款金额超过可退金额 | 检查退款金额 |
30009 |
422 | 订单未支付,无法退款 | 确认订单已支付后再退款 |
30010 |
422 | 退款单不存在 | 检查 refund_no 是否正确 |
40001 |
502 | 渠道下单失败 | 稍后重试,仍失败请联系平台 |
40002 |
502 | 渠道退款失败 | 稍后重试,仍失败请联系平台 |
40003 |
502 | 渠道调用超时 | 稍后重试 |
50099 |
500 | 系统内部错误 | 记录 trace_id 联系平台排查 |
10. 最佳实践
10.1 下单流程建议
1. 用户确认支付
↓
2. 你的服务端调用【统一下单】接口
↓
3. 返回 pay_credential 给前端
↓
4. 前端调用微信/支付宝 SDK 拉起支付
↓
5. 接收异步通知 → 更新订单状态 → 返回 "success"
↓
6. (兜底)若通知未收到,定时调用【查询订单】接口轮询
10.2 安全注意事项
app_secret只在服务端使用,禁止出现在前端代码或 App 客户端中- 异步通知的 IP 建议加入白名单(联系平台获取出口 IP 段)
- 通知接收后务必先返回
success,再处理业务逻辑,避免超时导致重复通知
10.3 金额单位
所有金额字段均以分为单位,整数类型:
- 1 元 =
100 - 9.9 元 =
990 - 99.99 元 =
9999
10.4 时区
所有时间字段均为 UTC 时间,ISO8601 格式,例如 2026-02-28T10:05:00Z。
11. 签名示例代码
Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"time"
)
func sign(appID, appSecret, body string) (timestamp, sign string) {
ts := strconv.FormatInt(time.Now().Unix(), 10)
raw := appID + ts + body
mac := hmac.New(sha256.New, []byte(appSecret))
mac.Write([]byte(raw))
return ts, hex.EncodeToString(mac.Sum(nil))
}
Python
import hmac
import hashlib
import time
def sign(app_id: str, app_secret: str, body: str) -> tuple[str, str]:
timestamp = str(int(time.time()))
raw = app_id + timestamp + body
signature = hmac.new(
app_secret.encode("utf-8"),
raw.encode("utf-8"),
hashlib.sha256
).hexdigest()
return timestamp, signature
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
public static String[] sign(String appId, String appSecret, String body) throws Exception {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String raw = appId + timestamp + body;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(appSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] bytes = mac.doFinal(raw.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : bytes) sb.append(String.format("%02x", b));
return new String[]{timestamp, sb.toString()};
}
PHP
function sign(string $appId, string $appSecret, string $body): array {
$timestamp = (string)time();
$raw = $appId . $timestamp . $body;
$signature = hash_hmac('sha256', $raw, $appSecret);
return [$timestamp, $signature];
}
如有问题请联系平台技术支持,并提供接口返回的 trace_id。