商户 API 对接手册 v1.2

商户 API 对接手册

适用对象:接入本平台代收(payin)与代付(payout)能力的商户技术团队。

版本 v1.2 · 2026-09-08 · 新增通道列表接口与 10006 错误码

开通时单独提供

环境地址、app_idapi_keyapi_secret 由平台运营在开通时单独提供,不在本文档内。可用的 channel_code 与币种请用第 7 节的通道列表接口自行查询。

01 接口总览

方法路径用途
POST/api/v1/payin/create创建收款订单
GET/api/v1/payin/{payin_order_no}查询收款订单
POST/api/v1/payout/create创建出款订单
GET/api/v1/payout/{payout_order_no}查询出款订单
GET/api/v1/balance查询商户余额
GET/api/v1/channel/list查询本商户已开通的通道

所有接口均需按第 2 节的签名规范鉴权。查询接口是订单最终状态的权威来源;异步通知可能重复、延迟或耗尽重试,收到通知后仍建议查单核对。

02 签名与鉴权

2.1 请求头

每个请求必须携带以下 5 个 Header:

Header说明
X-App-Id平台分配的应用 ID
X-Api-Key平台分配的 API Key
X-TimestampUnix 秒级时间戳,与服务器时间差不得超过 300 秒
X-Nonce随机字符串,单次使用,短期内重复会被拒绝
X-Sign按 2.2 计算的签名,小写十六进制

2.2 签名算法

算法:HMAC-SHA256,密钥为 api_secret,结果取小写 hex。待签名串固定为 8 段,以换行符 \n 连接。某段为空时该段留空,但分隔符不可省略

1METHOD
2PATH
3QUERY
4APP_ID
5API_KEY
6TIMESTAMP
7NONCE
8SHA256_HEX(RAW_BODY)

规范化规则:

  1. METHOD 大写(如 POSTGET)。
  2. PATH实际请求路径,含路径参数的真实值(如 /api/v1/payin/PO20260829000123),不是路由模板。
  3. QUERY 为规范化后的查询串:参数按键升序、同键多值按值升序,键和值分别做百分号编码后以 k=v& 连接;无查询参数时为空串。
  4. 最后一段是原始请求体字节的 SHA-256 小写 hex;GET 等无 body 请求对空字节串取哈希,即 e3b0c442…7852b855。不要先反序列化再重新序列化 body,必须对实际发送的字节计算。

2.3 Python 签名示例

import hashlib, hmac, time, uuid, requests
from urllib.parse import urlencode

APP_ID = "your_app_id"
API_KEY = "your_api_key"
API_SECRET = "your_api_secret"
BASE = "https://<platform-host>"

def canonical_query(params: dict) -> str:
    if not params:
        return ""
    pairs = []
    for k in sorted(params):
        vs = params[k] if isinstance(params[k], list) else [params[k]]
        for v in sorted(map(str, vs)):
            pairs.append((str(k), v))
    # 必须使用 urlencode:它与平台的 URL QueryEscape 一致,空格编码为 +。
    return urlencode(pairs)

def sign(method: str, path: str, query: dict, body: bytes, ts: str, nonce: str) -> str:
    body_sha = hashlib.sha256(body).hexdigest()
    source = "\n".join([
        method.upper(), path, canonical_query(query),
        APP_ID, API_KEY, ts, nonce, body_sha,
    ])
    return hmac.new(API_SECRET.encode(), source.encode(), hashlib.sha256).hexdigest()

def request(method: str, path: str, query: dict | None = None, body: bytes = b""):
    ts = str(int(time.time()))
    nonce = uuid.uuid4().hex
    headers = {
        "X-App-Id": APP_ID,
        "X-Api-Key": API_KEY,
        "X-Timestamp": ts,
        "X-Nonce": nonce,
        "X-Sign": sign(method, path, query or {}, body, ts, nonce),
        "Content-Type": "application/json",
    }
    url = BASE + path + (("?" + urlencode(query)) if query else "")
    return requests.request(method, url, headers=headers, data=body, timeout=10)

2.4 注意事项

  • 商户身份由平台从已验签的凭证解析,请求体中不接受、也无法传入商户号。
  • 防重放存储不可用时平台会直接拒绝请求(返回服务不可用类错误码),请退避重试,勿降级绕过签名。

03 统一响应与错误码

所有接口返回统一信封:

{
  "code": 0,
  "message": "成功",
  "data": { },
  "trace_id": "…",
  "timestamp": 1756450000
}
  • code = 0 表示成功;失败时 data 省略。
  • 请按 code 分支处理,不要解析 message 文案,文案会本地化、会调整。
  • 联系平台排障时请提供 trace_id
codeHTTP含义商户侧处理
10000500系统错误平台内部异常;持续出现请联系平台
10001400参数错误修正参数后重发;重试不会改变结果
10002404资源不存在确认单号/商户号后重查
10003409重复/冲突同一 merchant_request_id 已受理,改用查单接口取结果
10004503数据库错误平台存储层不可用,退避后重试
10005400币种小数位不受支持amount 小数位需与该币种精度一致,平台不做四舍五入
10006400通道未开通channel_code 未对本商户开通,联系平台开通后再下单;重发不会改变结果。用第 7 节接口确认可用通道
20001401未鉴权/签名验证失败检查 app_id 与签名算法
20002401登录失效管理后台场景,网关接口一般不会返回
20003403权限不足管理后台场景,网关接口一般不会返回
20004403账户被冻结/商户不可用联系平台
20005403账户被禁用联系平台
20006429触发限流退避后重试

04 金额、币种与幂等

  • 金额一律为主单位十进制字符串,如 "100.00";小数位必须与币种精度一致(各币种精度由平台提供),多余小数或非法格式会被拒绝,平台不做四舍五入
  • channel_code 由商户显式指定,一个通道唯一对应一个方向和一个币种。
  • 创建幂等键为 merchant_request_id(商户维度唯一):
    • 完全相同的请求重发 → 返回首次创建的订单,不会产生第二笔;
    • 同一 merchant_request_id 修改了金额、通道、收款人等任一关键要素 → 返回 10003 冲突,不会创建新订单,此时应改用查单接口获取原订单结果;
    • 因此不要复用 merchant_request_id 来换金额/换通道/换收款人;重新下单请换新的 merchant_request_id

05 代收 Payin

5.1 创建订单 POST /api/v1/payin/create

字段类型必填说明
merchant_order_nostring(≤64)商户订单号
merchant_request_idstring(≤64)幂等键,见第 4 节
amountstring(≤32)主单位金额字符串,如 "100.00"
currencystring币种代码
channel_codestring通道代码
product_namestring(≤128)商品名
notify_urlstring(≤512)本单通知地址;为空则用商户默认收款通知地址
return_urlstring(≤512)支付完成跳转地址

响应 data

{
  "url": "https://…收银台地址…",
  "payin_order_no": "PO…",
  "merchant_order_no": "…",
  "amount": "100.00",
  "currency": "PKR",
  "channel_code": "…",
  "pay_method": "…",
  "status": 2,
  "channel_order_no": "上游订单号,可能为 null",
  "provider_pay_url": "上游支付链接,可能为 null",
  "provider_qr_code": "上游二维码内容,可能为 null",
  "fail_code": "",
  "fail_reason": null
}
三态契约

status = 2(PENDING):上游已受理,provider_pay_urlprovider_qr_code 至少其一有值,引导付款人支付。

status = 5(FAIL):明确失败,fail_code 给出可编程原因;如需重试请更换 merchant_request_id 重新下单。

status = 13(INIT/PROCESSING,少数情况):上游响应慢或结果不确定,请轮询查单接口,或引导付款人访问 url 收银台兜底。

下单失败不影响信封语义:订单已受理即返回 code=0,失败通过 statusfail_code 表达。

5.2 查询订单 GET /api/v1/payin/{payin_order_no}

响应 data 字段:payin_order_nomerchant_nomerchant_order_nomerchant_request_idstatusamountcurrencychannel_codepay_methodchannel_order_noprovider_pay_urlprovider_qr_code(仅可继续支付状态返回)、notify_urlreturn_urlexpire_timesuccess_timefail_codefail_reasoncreated_at。时间字段为 RFC3339 格式。

5.3 收款订单状态

进行中 · 继续等待1INIT2PENDING3PROCESSING7PENDING_VERIFY终态 · 可以收口4SUCCESS5FAIL6CLOSED
状态含义
1INIT已建单,尚未触达上游
2PENDING上游已受理,等待支付
3PROCESSING正在创建/确认中
4SUCCESS支付成功(终态,已入账)
5FAIL失败(终态)
6CLOSED超时/关闭(终态)
7PENDING_VERIFY结果待人工核验;不要当作成功或失败处理,等待终态通知或轮询查单

06 代付 Payout

6.1 创建订单 POST /api/v1/payout/create

字段类型必填说明
merchant_order_nostring(≤64)商户订单号
merchant_request_idstring(≤64)幂等键
amountstring(≤32)主单位金额字符串
currencystring币种代码
channel_codestring通道代码
receiver_namestring(≤128)收款人姓名
receiver_accountstring(≤128)收款账号
receiver_bank_codestring(≤64)收款银行代码,部分通道必需,以通道说明为准
receiver_emailstring(≤128)收款人邮箱,做格式校验

响应 data

{ "payout_order_no": "PT…", "status": 1 }
资金语义

下单成功即从商户可用余额冻结 订单金额 + 手续费;失败(终态 5)自动全额解冻;成功(终态 4)从冻结余额扣除。余额不足时下单整体失败,不产生订单。

6.2 查询订单 GET /api/v1/payout/{payout_order_no}

响应 data 字段:payout_order_nomerchant_order_noamountcurrencystatusstatus_textfail_codefail_reasonreceiver_account脱敏)、success_timecreated_at

6.3 出款订单状态

进行中 · 继续等待1INIT3PROCESSING7PENDING_VERIFY终态 · 可以收口4SUCCESS5FAIL6CLOSED
状态含义
1INIT已建单并冻结资金
3PROCESSING出款处理中
4SUCCESS出款成功(终态,已结算)
5FAIL失败(终态,已解冻)
6CLOSED已关闭(终态)
7PENDING_VERIFY结果不明,资金保持冻结,等待核验;不要当作失败重新下单
重复出款风险

PENDING_VERIFY(7)期间平台不会通知成功或失败。若此时用新的 merchant_request_id 重新下单,一旦原单最终成功会造成重复出款。请等待原单进入终态。

07 通道列表 GET /api/v1/channel/list

返回本商户已开通的通道。下单用的 channel_code 从这里取;平台关闭某条通道后它会从列表中消失,用已消失的通道下单返回 10006

查询参数:biz_type(1=代收,2=代付,缺省 1)、page(默认 1)、size(默认 20,最大 100;超过 100 按 100 处理,不报错)。

响应 datapagesizetotallistlist 每项包含:

字段说明
channel_code通道代码,下单时按它指定通道
biz_type1=代收,2=代付
method_code / method_name支付方式代码与名称
currency_code该通道的币种,一条通道只对应一个币种
fee_rate平台对本商户在该通道上的费率,百分比字符串,如 "2.5000"
fixed_fee固定手续费,最小货币单位;不收固定费时返回 0

单笔金额上下限不在本接口返回:它是上游的技术约束、会随上游调整而变,以下单接口的实时判定为准。建议在本地缓存一段时间后刷新,不要每笔订单调用一次。

08 余额查询 GET /api/v1/balance

无请求参数,身份来自签名凭证。响应 data

{
  "as_of": "2026-08-29T12:00:00Z",
  "balances": [
    {
      "currency": "PKR",
      "available_balance": "12345.00",
      "frozen_balance": "100.00",
      "total_balance": "12445.00"
    }
  ]
}

金额均为主单位字符串。总余额 = 可用 + 冻结

09 异步通知

订单进入可通知终态(SUCCESS/FAIL)后,平台向商户推送通知:

  • 代收:优先使用下单时的 notify_url,为空则用商户默认收款通知地址;
  • 代付:使用商户默认出款通知地址;
  • 地址未配置则跳过通知,请依赖查单。

9.1 通知报文

POST,JSON body:

{
  "biz_type": "PAYIN",
  "order_no": "PO…",
  "merchant_order_no": "…",
  "merchant_no": "…",
  "amount": "100.00",
  "currency": "PKR",
  "status": "SUCCESS",
  "fail_code": "",
  "fail_reason": "",
  "finished_at": "2026-08-29T12:00:00Z",
  "notified_at": "2026-08-29T12:00:05Z"
}
  • biz_typePAYINPAYOUTstatusSUCCESSFAIL
  • 通知携带与商户请求完全相同规范的 5 个签名 Header(第 2 节),对通知目标 URL 的实际路径和查询参数签名。商户可复用自己的签名代码验签,必须验签后再处理

9.2 应答约定

商户收到并处理后必须返回:HTTP 200,响应体为纯文本 success(去除首尾空白后严格等于)。其他任何响应视为投递失败。平台请求超时 5 秒,不跟随重定向,响应体最多读取 1 KiB。

9.3 重试与幂等

  • 至少一次(at-least-once)语义,通知可能重复。请以 biz_type + order_no 幂等处理,重复通知直接返回 success
  • 投递失败最多重试 3 次(总计 4 次),间隔约 15 秒 / 1 分钟 / 5 分钟;超过 30 分钟未成功则停止。
  • 通知耗尽后订单状态不变;最终状态以查单接口为准,建议对长时间未收到通知的订单主动轮询。

9.4 商户侧验签示例(Python / Flask)

@app.post("/notify")
def on_notify():
    body = request.get_data()          # 原始字节,勿先解析再序列化
    ts = request.headers.get("X-Timestamp", "")
    nonce = request.headers.get("X-Nonce", "")
    got = request.headers.get("X-Sign", "")
    want = sign("POST", request.path, dict(request.args), body, ts, nonce)
    if not hmac.compare_digest(got, want):
        return "invalid sign", 401
    if abs(time.time() - int(ts)) > 300:
        return "expired", 401
    data = json.loads(body)
    handle_idempotently(data["biz_type"], data["order_no"], data["status"])
    return "success", 200

10 对接最佳实践

  1. 以查单为准:通知用于加速,查单接口是最终事实。
  2. 幂等三原则:新订单换新 merchant_request_id;收到 10003 冲突去查单而不是重试;通知按订单号幂等消费。
  3. 按码编程:状态用数值枚举、失败原因用 fail_code 判断;messagestatus_textfail_reason 仅用于展示。
  4. PENDING_VERIFY 不是失败:收付款订单进入 7 时保持等待,切勿重复下单,代付侧重复下单可能造成双重打款。
  5. 可用性类错误退避重试(10000/10004/20006),参数类错误(10001/10005/10006)修正后再发。
  6. 保护好 api_secret:仅存服务端;怀疑泄露立即联系平台轮换。

11 联调闭环与验收

商户服务平台上游通道带签名创建订单提交订单受理 / 最终结果创建响应异步通知(可重复)带签名查单确认最终状态

完成以下 5 项即可进入验收:

  1. 使用平台提供的测试凭据完成一次代收和一次代付。
  2. 对创建请求做同幂等键重发,确认不会产生第二笔订单。
  3. 接收并验签通知;故意让首次应答失败,确认重复通知可被幂等消费。
  4. 对每笔订单以查单接口确认最终状态和金额。
  5. 验收通过后,由平台单独开通生产凭据、IP 白名单、通道和币种;测试凭据不得复用。
商户 API 对接手册 v1.2 · 2026-09-08 · 内容如与平台实现不一致,以平台运营给出的最新版本为准。