商户对接文档(统一收银台 API)

版本:v1.1(2026-10-04)| 适用对象:接入本平台的外部商户技术人员 | 历史版本


1. 概述与接入流程

本平台为多国聚合支付平台,采用「一国一收银台」模型:商户只需对接一次平台统一 API,即可使用各国收银台的代收 / 代付能力。上游通道的切换、权重、启停由平台侧完成,商户无感。

接入流程(5 步):

  1. 开户:联系平台运营开通商户号(商户后台登录账号)。
  2. 拿密钥:登录商户后台 → 「API 密钥管理」获取 API Key 与 API Secret(见 §2)。
  3. 沙箱联调:用测试服地址按本文档完成签名、下单、查询、通知验签联调(见 §8)。
  4. 配置通知地址:下单时传入 notify_url(或在商户后台配置默认通知地址,以联调时确认为准),确保能接收并正确处理异步通知(见 §6)。
  5. 上线:联调通过后切换生产网关地址与生产密钥,开始真实交易。

2. 接入准备

名称 说明 在哪拿
商户号 平台分配的商户标识,如 M10001 开户时由运营提供
API Key 接口身份标识(公开,不参与签名) 商户后台 → API 密钥管理
API Secret 接口签名密钥(绝密,参与签名与通知验签) 商户后台 → API 密钥管理 → 「重置密钥」获取明文,只显示一次,请立即保存

⚠️ API Secret 明文平台不保存、不回显。遗失请重新重置,重置后旧密钥立即失效。

网关地址:

环境 网关地址
沙箱 / 测试 http://45.76.155.11
生产 以开户时提供的为准

以下接口路径均为相对路径,如代收下单完整地址为 http://45.76.155.11/api/v1/pay/mch/payin/create。

支持的国家收银台(沙箱):

国家 国家代码 币种 支付方式编码
巴基斯坦 PK PKR EP、JZ
孟加拉 BD BDT NG、BK、EP
泰国 TH THB THAI_QR
印度 IN INR UPI

下单时 currency 必须与收银台币种一致,pay_method 必须在该国支持列表内,否则返回参数/业务错误。


3. 签名规则

所有商户统一 API 请求都必须签名。鉴权方式为请求头三件套:

请求头 说明
X-Api-Key 你的 API Key(公开标识,不参与签名)
X-Timestamp Unix 秒级时间戳,服务端允许 ±300 秒漂移,超时拒绝
X-Sign 按下述算法计算的签名字符串

签名算法(7 步):

  1. 收集待签参数:URL query 参数与 JSON body 顶层字段合并为一个字典(本接口一般无 query 参数,即对 body 顶层字段签名);
  2. 排除 sign 字段(精确匹配小写 sign);
  3. 空值不参与签名:值为 null 或 ""(空字符串)时跳过;
  4. 剩余参数按 key 的 ASCII 码升序排列;
  5. 拼接为 k1=v1&k2=v2&...&kn=vn 形式。值先字符串化:
  6. 数字 / 布尔:按服务端语言的字符串转换(Python str(),即布尔为 True/False,注意不是 true/false);
  7. 嵌套 dict / list:按 JSON 紧凑格式序列化(key 排序、分隔符为 , 和 :、不转义 Unicode),即 json.dumps(v, sort_keys=True, separators=(",", ":"), ensure_ascii=False);
  8. 尾部拼接 &key={api_secret},api_secret 为你的接口密钥明文;
  9. 对拼接串取 MD5,结果转大写,即为 X-Sign 的值。

示例(api_secret = "testsecret123",body 为 {"country_code":"PK","pay_method":"EP","amount_minor":10000,"currency":"PKR","mch_order_no":"ORDER20260930001","notify_url":"https://merchant.example/notify"}):

待签串:
amount_minor=10000&country_code=PK&currency=PKR&mch_order_no=ORDER20260930001&notify_url=https://merchant.example/notify&pay_method=EP&key=testsecret123
X-Sign = MD5(待签串).upper()

注意:mode、return_url、goods_desc、client_ip 若传空字符串则不参与签名;若传了非空值则参与签名,body 与签名必须一致。

验签失败一律返回 HTTP 401,code=40100(签名验证失败 / 时间戳过期 / 缺少鉴权参数 / 商户不存在或被禁用)。


4. 代收

4.1 代收下单

POST /api/v1/pay/mch/payin/create | Content-Type: application/json

请求字段:

字段 类型 必填 说明
country_code string 是 国家二字码,如 PK(大小写不敏感)
pay_method string 是 支付方式编码,如 EP(大小写不敏感)
mode string 否 cashier=收银台 H5(默认),direct=直连
amount_minor int 是 金额(最小货币单位,如 PKR 100.00 元传 10000),必须 > 0
currency string 是 币种三字码,如 PKR,必须与收银台币种一致
mch_order_no string 是 商户订单号,1–64 字符,同一商户下必须唯一,重复返回 40900
notify_url string 是 异步通知地址(支付终态时平台 POST 通知,见 §6)
return_url string 否 同步跳转地址(收银台支付完成后用户跳回地址),默认空
goods_desc string 否 商品描述,最长 128 字符,默认空
client_ip string 否 用户 IP,默认空(为空时取请求来源 IP)

响应(code=0):

{
  "code": 0,
  "msg": "下单成功",
  "data": {
    "platform_order_no": "PI20260930010101001",
    "mch_order_no": "ORDER20260930001",
    "pay_url": "http://45.76.155.11/api/v1/pay/cashier/PI20260930010101001",
    "qr_data": "",
    "status": "PENDING"
  }
}
字段 说明
platform_order_no 平台订单号,代收前缀 PI + UTC 时间 yyyymmddHHMMSS + 6 位随机数
pay_url 收款链接:将用户引导至该地址完成支付(cashier 模式为平台收银台 H5)
qr_data 二维码内容(有则展示,无则为空字符串)
status 下单时状态,一般为 PENDING(待支付)

4.2 代收查询

POST /api/v1/pay/mch/payin/query | Content-Type: application/json

请求字段(二选一,至少传一个):

字段 类型 必填 说明
mch_order_no string 条件 商户订单号
platform_order_no string 条件 平台订单号

响应(code=0,data 为订单详情):

{
  "code": 0,
  "msg": "成功",
  "data": {
    "platform_order_no": "PI20260930010101001",
    "mch_order_no": "ORDER20260930001",
    "order_type": "payin",
    "country_code": "PK",
    "country_name": "巴基斯坦",
    "pay_method": "EP",
    "mode": "cashier",
    "amount_minor": 10000,
    "currency": "PKR",
    "fee": 30,
    "status": "SUCCESS",
    "upstream_order_no": "UP202609300001",
    "pay_url": "http://45.76.155.11/api/v1/pay/cashier/PI20260930010101001",
    "qr_data": "",
    "notified": true,
    "paid_at": "2026-09-30T10:05:00+00:00",
    "created_at": "2026-09-30T10:01:00+00:00"
  }
}

订单状态说明:

状态 含义
PENDING 待支付
PROCESSING 处理中
SUCCESS 成功(终态)
FAILED 失败(终态)
CANCELLED 已取消(终态)
REVERSED 已冲正(终态)

5. 代付

5.1 代付下单

POST /api/v1/pay/mch/payout/create | Content-Type: application/json

请求字段:

字段 类型 必填 说明
country_code string 是 国家二字码,如 PK
pay_method string 是 支付方式编码,如 JZ
amount_minor int 是 金额(最小货币单位),必须 > 0
currency string 是 币种三字码,必须与收银台币种一致
mch_order_no string 是 商户订单号,1–64 字符,同一商户下必须唯一
account_no string 是 收款账号
account_name string 是 收款人姓名
bank_code string 否 银行编码,默认空
extra object 否 扩展字段(JSON 对象),默认 {}
notify_url string 否 异步通知地址,默认空(为空则不发送通知)
client_ip string 否 用户 IP,默认空

响应(code=0):

{
  "code": 0,
  "msg": "代付下单成功",
  "data": {
    "platform_order_no": "PO20260930010101002",
    "mch_order_no": "PAYOUT20260930001",
    "status": "PROCESSING"
  }
}

平台订单号代付前缀为 PO。代付为资金出账操作,请确保商户余额充足。

5.2 代付查询

POST /api/v1/pay/mch/payout/query | Content-Type: application/json

请求字段与响应格式同 §4.2(order_type 为 payout)。

5.3 余额查询

POST /api/v1/pay/mch/balance/query | Content-Type: application/json

代付前检查商户余额。请求体可选 currency(如 PKR)过滤币种,不传返回全部币种。

{
  "currency": "PKR"
}

响应 data.balances 为数组,每项含 currency、balance(可用余额,最小货币单位)、frozen(冻结金额)。


6. 异步通知

订单进入终态(SUCCESS / FAILED / CANCELLED / REVERSED)且下单时传入了 notify_url,平台将向该地址发送异步通知。

字段 类型 说明
platform_order_no string 平台订单号
mch_order_no string 商户订单号
order_type string payin=代收,payout=代付
status string 终态:SUCCESS / FAILED / CANCELLED / REVERSED
amount_minor int 金额(最小货币单位)
paid_amount int 实付金额(最小货币单位),无差异时等于 amount_minor
currency string 币种
fee int 手续费(最小货币单位)
paid_at string 支付成功时间(ISO8601),失败时为空字符串
timestamp int 通知发送时的 Unix 秒级时间戳
sign string 签名(MD5 大写,算法同 §3,用你的 api_secret 计算;验签时排除 sign 本身与空值)

验签:收到通知后,用 §3 算法对除 sign 外的字段重新计算签名,与 sign 比对(转大写后比对)。验签不通过的通知不得作为入账依据。

商户收到通知后应返回什么:HTTP 状态码 < 500,且响应体包含 SUCCESS 或 success(如直接返回纯文本 SUCCESS),平台即视为送达成功。

重试机制:首次发送失败后平台自动重试,最多 8 次,退避间隔为 1 分钟 → 5 分钟 → 30 分钟 → 2 小时 → 12 小时 → 12 小时 → 12 小时;8 次仍失败标记为不再重试。请做好幂等处理(同一订单可能重复通知,以 platform_order_no + status 去重)。

建议:不要完全依赖通知,重要订单主动调用查询接口对账。


7. 错误码表

统一响应体为 {"code": ..., "msg": "...", "data": ...}。code=0 表示成功。

code 说明 常见场景
0 成功 —
40000 参数错误 缺字段、字段格式非法、mode 非法、支付方式不在该国列表
40100 未登录或登录已过期(HTTP 401) 缺少 X-Api-Key/X-Timestamp/X-Sign、签名验证失败、时间戳过期、商户不存在或被禁用、密钥未配置
40300 无权限 —
40400 资源不存在 查询的订单不存在
40900 资源已存在 mch_order_no 重复("商户订单号已存在")
42200 业务校验失败 币种与收银台不一致、商户被禁用等
42900 请求过于频繁 触发限流,请稍后重试
50000 系统内部错误 服务端异常
50300 服务暂不可用 —

8. 沙箱联调说明


9. 示例代码

9.1 Python(签名 + 代收下单 + 通知验签,完整可跑)

import hashlib
import hmac
import json
import time
import urllib.request

API_BASE = "http://45.76.155.11"
API_KEY = "你的API Key"
API_SECRET = "你的API Secret"


def _stringify(v):
    if isinstance(v, bool):
        return str(v)  # True/False,与服务端一致
    if isinstance(v, (dict, list)):
        return json.dumps(v, sort_keys=True, separators=(",", ":"),
                          ensure_ascii=False)
    return str(v)


def build_sign(params: dict, api_secret: str) -> str:
    items = [(k, _stringify(v)) for k, v in params.items()
             if k != "sign" and v is not None and v != ""]
    items.sort(key=lambda kv: kv[0])  # ASCII 升序
    canonical = "&".join(f"{k}={v}" for k, v in items)
    canonical = f"{canonical}&key={api_secret}" if canonical else f"key={api_secret}"
    return hashlib.md5(canonical.encode("utf-8")).hexdigest().upper()


def api_post(path: str, body: dict) -> dict:
    ts = str(int(time.time()))
    headers = {
        "Content-Type": "application/json",
        "X-Api-Key": API_KEY,
        "X-Timestamp": ts,
        "X-Sign": build_sign(body, API_SECRET),
    }
    req = urllib.request.Request(API_BASE + path,
                                 data=json.dumps(body).encode("utf-8"),
                                 headers=headers, method="POST")
    with urllib.request.urlopen(req, timeout=15) as resp:
        return json.loads(resp.read().decode("utf-8"))


# ---- 代收下单 ----
order = api_post("/api/v1/pay/mch/payin/create", {
    "country_code": "PK",
    "pay_method": "EP",
    "mode": "cashier",
    "amount_minor": 10000,
    "currency": "PKR",
    "mch_order_no": "TEST" + str(int(time.time())),
    "notify_url": "https://merchant.example/notify",
    "return_url": "https://merchant.example/return",
    "goods_desc": "测试商品",
})
print(json.dumps(order, ensure_ascii=False, indent=2))

# ---- 通知验签(在你的 notify_url 处理函数中使用) ----
def verify_notify(payload: dict) -> bool:
    sign = str(payload.get("sign", "")).upper()
    return hmac.compare_digest(build_sign(payload, API_SECRET), sign)

9.2 PHP(签名函数)

<?php
function mch_ksort_recursive($arr) {
    if (!is_array($arr)) return $arr;
    if (array_keys($arr) !== range(0, count($arr) - 1)) {
        ksort($arr, SORT_STRING);          // 关联数组按 key ASCII 排序
    }
    foreach ($arr as $k => $v) $arr[$k] = mch_ksort_recursive($v);
    return $arr;
}

function mch_stringify($v) {
    if (is_bool($v)) return $v ? 'True' : 'False';   // 对齐服务端 Python str()
    if ($v instanceof stdClass) return '{}';        // 空对象
    if (is_array($v)) {
        return json_encode(mch_ksort_recursive($v),
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    }
    return (string)$v;
}

function mch_sign(array $params, string $apiSecret): string {
    $items = [];
    foreach ($params as $k => $v) {
        if ($k === 'sign' || $v === null || $v === '') continue; // 排除 sign 与空值
        $items[$k] = mch_stringify($v);
    }
    ksort($items, SORT_STRING);
    $pairs = [];
    foreach ($items as $k => $v) $pairs[] = $k . '=' . $v;
    $canonical = implode('&', $pairs);
    $canonical = $canonical !== '' ? $canonical . '&key=' . $apiSecret
                                   : 'key=' . $apiSecret;
    return strtoupper(md5($canonical));
}

// 使用示例
$body = [
    'country_code' => 'PK',
    'pay_method'   => 'EP',
    'mode'         => 'cashier',
    'amount_minor' => 10000,
    'currency'     => 'PKR',
    'mch_order_no' => 'TEST' . time(),
    'notify_url'   => 'https://merchant.example/notify',
];
$headers = [
    'Content-Type: application/json',
    'X-Api-Key: 你的API Key',
    'X-Timestamp: ' . time(),
    'X-Sign: ' . mch_sign($body, '你的API Secret'),
];
// 之后用 curl POST JSON $body 到网关,略

PHP 注意:空对象请用 (object)[] / new stdClass(json_encode 输出 {}),不要用 [](会输出 [] 导致签名不一致);extra 等可选字段不传则不参与签名。

9.3 curl(先算好签名再调,调试用)

TS=$(date +%s)
# X-Sign 需按 §3 算法预先算好,此处以 PLACEHOLDER 代替
curl -X POST http://45.76.155.11/api/v1/pay/mch/payin/create \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: 你的API Key' \
  -H "X-Timestamp: $TS" \
  -H 'X-Sign: 预先算好的大写MD5签名' \
  -d '{
    "country_code": "PK",
    "pay_method": "EP",
    "mode": "cashier",
    "amount_minor": 10000,
    "currency": "PKR",
    "mch_order_no": "TEST1699999999",
    "notify_url": "https://merchant.example/notify",
    "goods_desc": "测试商品"
  }'

文档外问题请联系平台运营 / 技术对接人。生产网关地址与密钥以开户资料为准。

版本变更记录

版本日期变更内容
v1.12026-10-04商户通知 payload 新增 paid_amount(实付金额,最小货币单位;无差异时等于订单金额);文档纳入 git 版本管理,历史版本见版本归档
v1.02026-09-30首版:业务模型、签名算法、下单/查询 API、错误码、回调通知与验签、状态说明、对账账单、沙箱、示例代码(归档快照)