PUYUNPAY API V1.0

浦云支付商户对接文档

本文档只描述当前生产系统已经实现并开放给商户的能力:大额支付宝、易安支付宝/微信、数字货币收银台、USDT-TRC20 的统一下单、订单查询和支付结果通知。

生产网关:https://pay.125.momHTTPS + UTF-8MD5 签名金额单位:分

1. 系统概览

商户系统提交签名订单,浦云支付根据该商户后台已勾选授权的通道创建上游订单,并返回可跳转的支付地址。支付完成后,浦云支付向商户的 notifyUrl 推送签名通知。

① 创建订单商户后端签名请求统一下单接口。
② 用户支付浏览器跳转至返回的 payData
③ 确认结果验签回调并主动查单,幂等入账。
安全要求:App Secret 只能保存在商户服务端,禁止放入网页、APP、小程序或公开仓库。用户资产到账必须以验签成功且订单状态为 2 为准。

2. 接入准备

商户账号安全:建议在个人中心的“安全设置”中绑定 Authenticator,并离线保存一次性恢复码。
  1. 登录商户平台:https://user.puyunpay.com/
  2. 获取商户号 mchNo、应用 ID appId 和应用密钥 appSecret
  3. 确认管理员已给该商户勾选需要的支付通道。
  4. 如需限制调用来源,在商户端个人中心的“API白名单”中填写商户服务器公网 IP;同一商户下所有 AppId 共用该白名单。
  5. 准备公网可访问的 HTTPS 异步通知地址。
  6. 在商户服务端实现签名、验签、订单幂等和主动查单。
API IP白名单:每行填写一个精确 IPv4 或 IPv6,最多50个。留空表示不限制;填写后,该商户所有签名 API 请求都必须来自白名单地址。接口参数中的 clientIp 不能代替服务器真实来源 IP。
文档示例全部使用演示值。请勿直接复制示例商户号或密钥到生产环境。

3. 请求与响应规则

项目要求
生产网关https://pay.125.mom
数字货币收银台https://uapi.125.mom/i/{invoiceId},统一下单成功后由系统在 payData 中返回完整地址
编码UTF-8
下单方式POST,推荐 application/json;系统也支持表单参数
查询方式POST 或 GET;生产建议统一使用 POST JSON
金额整数,单位为分。例如人民币 500 元传 50000
时间13位毫秒时间戳字符串,例如 1787078400000
版本version=1.0
签名signType=MD5,签名输出大写

公共请求参数

字段必填类型说明
mchNoString商户号
appIdString商户应用 ID,必须属于该商户
reqTimeString请求毫秒时间戳
versionString固定 1.0
signTypeString固定 MD5
signString32位大写 MD5 签名

公共响应结构

{
  "code": 0,
  "msg": "SUCCESS",
  "data": {},
  "sign": "响应 data 的 MD5 签名"
}

code=0 仅表示本次接口处理成功。是否支付成功必须继续判断 data.orderState 或查询结果中的 data.state。只有支付数据非空且状态允许支付时才打开支付链接;状态未知时先查单,勿重复下单。

4. MD5 签名算法

  1. 移除 sign 字段。
  2. 排除值为 null 或空字符串的字段;数字 0 不得排除。
  3. 按照字段名不区分大小写升序排列。
  4. 依次拼接为 key=value&
  5. 末尾追加 key=APP_SECRET
  6. 使用 UTF-8 计算 MD5,并转为大写。
对象或数组字段参与签名时,值必须是双方一致的紧凑 JSON 字符串。不要对字段值做 URL 编码后再签名。

固定签名样例

演示密钥:DEMO_APP_SECRET

amount=50000&appId=APP_DEMO_10001&body=浦云支付接口测试&clientIp=203.0.113.10&currency=CNY&expiredTime=1800&extParam=user_10001&mchNo=M_DEMO_10001&mchOrderNo=ORDER202608190001&notifyUrl=https://merchant.example.com/pay/notify&reqTime=1787078400000&returnUrl=https://merchant.example.com/pay/result&signType=MD5&subject=测试订单&version=1.0&wayCode=ALIPAY_DE&key=DEMO_APP_SECRET

签名结果:8FC1237997EC94C5DD3D3EABF2F1ECF8

响应验签和回调验签采用同一算法:仅对 data 内字段或回调字段计算,先移除收到的 sign

5. 当前开放的支付通道

名称wayCode实际接口说明
大额支付宝500–30000ALIPAY_DEwfpay金额范围人民币500–30000元;实际限额以商户配置和上游返回为准
易安支付宝937YIAN_937yianpay937需运营后台逐项开通并设置费率;实际限额和可用性以上游配置为准
易安微信938YIAN_938yianpay938
微信双端原生100(淘宝)FVXF_201fvxfpay19个支付方式由运营后台逐项为商户开通并分别设置费率;实际限额和可用性以上游配置为准
微信双端原生200(淘宝)FVXF_202fvxfpay
微信双端10-100FVXF_203fvxfpay
美团FVXF_204fvxfpay
微信/聚合码/综合支付FVXF_333fvxfpay
UID超大FVXF_2001fvxfpay
UID大额FVXF_2002fvxfpay
UID中额FVXF_2003fvxfpay
UID小额FVXF_2004fvxfpay
金条300FVXF_2005fvxfpay
金条200FVXF_2006fvxfpay
金条100FVXF_2007fvxfpay
特价宝妈FVXF_2008fvxfpay
免输金条200FVXF_2102fvxfpay
免输金条300FVXF_2103fvxfpay
免输金条500FVXF_2104fvxfpay
AA收款FVXF_3001fvxfpay
抖音固额100FVXF_3002fvxfpay
复制转账大额FVXF_6666fvxfpay
数字货币收银台CRYPTO_CASHIERbitcart打开数字货币收银台,由当前商户已配置的钱包决定可选币种
USDT-TRC20USDT_TRC20bitcart限定使用商户已配置的 USDT-TRC20 钱包
商户只能调用后台已勾选授权且已配置的通道。未授权、停用或未完成参数配置的通道会下单失败。

数字货币订单仍使用统一金额规则:amount / 100 作为订单价格,currency 原样转换为大写后发送到收银台。例如 amount=10000, currency=CNY 表示价格 100.00 CNY,再由收银台展示应付数字货币数量。

USDT-TRC20 收银台会在换算结果后增加一段随机小数尾数,用于在同一收款地址上准确识别订单。付款人必须按页面显示的完整 6 位小数金额支付,不得自行抹零、四舍五入或只支付商户订单的整数金额。随机尾数只影响链上应付 USDT 数量,不改变商户接口中的原始 amountcurrency 和到账核对规则。

6. 统一下单

POSThttps://pay.125.mom/api/pay/unifiedOrder

业务请求参数

字段必填类型说明
mchOrderNoString商户订单号;同一商户必须唯一,失败重试不要换号
wayCodeString只能使用当前商户已授权的编码
amountLong整数,单位分,最小1分
currencyString币种代码,建议大写,例如 CNY
clientIpString最终用户公网 IP;不传时系统使用请求来源 IP
subjectString商品标题
bodyString商品描述
notifyUrl建议String支付结果异步通知地址;不传则不会推送回调
returnUrlString支付完成后的浏览器跳转地址,不可代替异步通知
expiredTimeInteger订单有效期,单位秒;不传使用系统默认值
channelExtraString当前三个通道无需传;预留为 JSON 字符串
extParamString商户扩展数据,查询和回调原样返回
divisionModeByte当前通道不开放分账,建议不传或传0

请求示例

{
  "mchNo": "M_DEMO_10001",
  "appId": "APP_DEMO_10001",
  "mchOrderNo": "ORDER202608190001",
  "wayCode": "ALIPAY_DE",
  "amount": 50000,
  "currency": "CNY",
  "clientIp": "203.0.113.10",
  "subject": "测试订单",
  "body": "浦云支付接口测试",
  "notifyUrl": "https://merchant.example.com/pay/notify",
  "returnUrl": "https://merchant.example.com/pay/result",
  "expiredTime": 1800,
  "extParam": "user_10001",
  "reqTime": "1787078400000",
  "version": "1.0",
  "signType": "MD5",
  "sign": "8FC1237997EC94C5DD3D3EABF2F1ECF8"
}

成功响应

{
  "code": 0,
  "msg": "SUCCESS",
  "data": {
    "payOrderId": "P20260819000100001",
    "mchOrderNo": "ORDER202608190001",
    "orderState": 1,
    "payDataType": "payUrl",
    "payData": "https://checkout.example/pay/xxx",
    "errCode": null,
    "errMsg": null
  },
  "sign": "响应签名"
}
字段说明
payOrderId浦云支付订单号,建议保存
mchOrderNo原商户订单号
orderState下单时订单状态
payDataType当前通道正常返回 payUrl
payData用户支付页面 URL;由浏览器跳转打开
errCode / errMsg上游错误信息;可能为空

7. 支付订单查询

POSThttps://pay.125.mom/api/pay/query

payOrderIdmchOrderNo 二选一。推荐保留两者并优先使用浦云支付订单号查询。

{
  "mchNo": "M_DEMO_10001",
  "appId": "APP_DEMO_10001",
  "payOrderId": "P20260819000100001",
  "reqTime": "1787078460000",
  "version": "1.0",
  "signType": "MD5",
  "sign": "按本文算法计算"
}

查询响应 data

字段类型说明
payOrderIdString浦云支付订单号
mchNo / appIdString所属商户和应用
mchOrderNoString商户订单号
ifCodeString实际通道:wfpaybitcart
wayCodeString支付方式编码
amount / currencyLong / String订单金额(分)和币种
stateByte最终判断使用的订单状态
channelOrderNoString上游订单或发票号
errCode / errMsgString上游错误
extParamString下单时的扩展参数
createdAt / successTimeLong13位毫秒时间戳
推荐策略:回调验签成功后再查一次订单;长时间支付中的订单按 5秒、10秒、30秒、60秒逐步降低频率,避免高频轮询。

8. 支付结果异步通知

当订单状态变化并需要通知商户时,浦云支付向下单时的 notifyUrl 发起 HTTP POST。

项目实际行为
请求方法POST
内容类型application/x-www-form-urlencoded
连接超时20秒
成功应答正文返回 SUCCESS,不区分大小写;建议严格返回大写且无空格、无JSON、无HTML
重试最多6次,约在0、30、60、90、120、150秒发送

回调字段

字段与订单查询 data 基本一致,并额外包含 reqTimesign。验签时移除 sign,其余非空字段全部参与签名。

payOrderId=P20260819000100001
mchNo=M_DEMO_10001
appId=APP_DEMO_10001
mchOrderNo=ORDER202608190001
ifCode=wfpay
wayCode=ALIPAY_DE
amount=50000
currency=CNY
state=2
extParam=user_10001
createdAt=1787078400000
successTime=1787078499000
reqTime=1787078500000
sign=回调签名

正确处理顺序

  1. 读取所有表单字段并取出 sign
  2. 使用该应用 App Secret 验签。
  3. 核对 mchNoappId、订单号、金额和币种。
  4. 确认 state=2,必要时调用查询接口复核。
  5. 用商户订单号做数据库唯一约束,在同一事务中幂等入账。
  6. 事务提交成功后输出纯文本 SUCCESS
不要相信浏览器 returnUrl 参数,不要仅凭页面跳转发货,不要在验签或入账失败时返回 SUCCESS。

9. 订单状态

状态商户处理
0订单生成等待用户支付
1支付中继续等待或主动查询
2支付成功验签、核对金额、幂等入账
3支付失败终止本次支付
4已撤销不可交付
5已退款按业务处理退款结果
6订单关闭不可继续支付

10. 错误码与排查

易安上游通知与商户通知:易安937/938的上游通知入口为 https://pay.125.mom/api/pay/notify/yianpay/{payOrderId}。平台全局支付网关地址必须是可公网访问的HTTPS地址,不能填写Docker内部服务名。商户下单的 notifyUrl 是平台确认成功后通知商户的另一层地址,不能混用。已创建订单在上游保存的旧通知地址不会随平台配置修改而自动改变。

易安查单与晚到通知:上游查单为顶层平铺字段,按实际返回字段验签,并核对上游商户号、订单号、金额和渠道流水。平台只有在成功状态落库后才发送商户成功通知。已关闭易安订单的有效成功回调可按条件恢复,重复回调不得重复处理;已关闭且未收到上游回调的历史订单须先核实上游支付证据,再逐单恢复,不能批量改成成功。

皇冠V9:上游明确返回“未适配到通道”时,记录失败状态及 FVXF_CHANNEL_UNAVAILABLE,需要平台核对当前账户的通道和金额条件;通信异常或支付数据缺失保持待确认,错误码分别为 FVXF_RESULT_UNKNOWNFVXF_INCOMPLETE_RESPONSE。不要根据历史参考限额反复重下订单。

应用授权与限额:商户支付通道列表按 APPID 展示,下单必须使用该行对应应用。商户其他应用已授权不代表当前应用已授权。新增或更新应用会补齐商户已开通且该应用尚未配置的易安授权,不覆盖已有易安授权和费率。“未配置,请确认通道限额”不代表无限额;金额范围及固定金额要求须向平台确认,不能仅凭通道启用判断实时可用。

WFPay 白名单与下单异常:商户 API 来源白名单和上游代收服务器 IP 白名单是两套配置。明确的上游白名单拒绝会记录订单失败(orderState=3)及 WFPAY_AUTH_REJECTED;通信异常或未返回完整支付数据保持待确认(orderState=1),可在查单的 errCode / errMsg 中查看 WFPAY_RESULT_UNKNOWNWFPAY_INCOMPLETE_RESPONSE。此时不要付款或盲目换订单号重试。HTTP 403 本身不能作为支付失败依据。

code含义处理建议
0SUCCESS继续检查 data 中的订单状态
10系统异常记录请求号和 msg,稍后使用原商户订单号重试或查单
11参数有误检查必填字段、类型、金额和时间戳
12数据库服务异常不要换订单号盲目重下,先查单
9999自定义业务异常以 msg 为准,常见为验签失败、通道未授权或配置不可用

常见问题

  • 验签失败:确认排除了 sign 和空字符串,排序规则一致,中文使用 UTF-8,末尾是 key=密钥
  • 不支持的支付方式:检查 wayCode 拼写及商户后台是否已勾选。
  • 重复商户订单号:先调用查单,不要创建不同订单号重复扣款。
  • 支付成功但没收到回调:检查 notifyUrl 公网 HTTPS、响应耗时、防火墙和是否返回纯文本 SUCCESS。
  • 一直支付中:主动查单;数字货币需要上游达到完整确认条件后才会成功。

11. 服务端代码示例

以下代码展示签名核心逻辑。HTTP 请求、日志脱敏、超时、重试和持久化请按商户项目规范补齐。

cURL 下单

curl -X POST 'https://pay.125.mom/api/pay/unifiedOrder' \
  -H 'Content-Type: application/json' \
  -d '{"mchNo":"M_DEMO_10001","appId":"APP_DEMO_10001","mchOrderNo":"ORDER202608190001","wayCode":"ALIPAY_DE","amount":50000,"currency":"CNY","subject":"测试订单","body":"浦云支付接口测试","notifyUrl":"https://merchant.example.com/pay/notify","reqTime":"1787078400000","version":"1.0","signType":"MD5","sign":"请由服务端计算"}'

PHP 签名

<?php
function puyunSign(array $data, string $secret): string {
    unset($data['sign']);
    $data = array_filter($data, fn($v) => $v !== null && $v !== '');
    uksort($data, 'strcasecmp');
    $text = '';
    foreach ($data as $key => $value) {
        $text .= $key . '=' . $value . '&';
    }
    return strtoupper(md5($text . 'key=' . $secret));
}

$payload['sign'] = puyunSign($payload, getenv('PUYUNPAY_APP_SECRET'));

Node.js 签名

import crypto from 'node:crypto';

export function puyunSign(input, secret) {
  const data = { ...input };
  delete data.sign;
  const text = Object.entries(data)
    .filter(([, value]) => value !== null && value !== '')
    .sort(([a], [b]) => a.toLowerCase().localeCompare(b.toLowerCase()))
    .map(([key, value]) => `${key}=${value}&`)
    .join('') + `key=${secret}`;
  return crypto.createHash('md5').update(text, 'utf8').digest('hex').toUpperCase();
}

Python 签名

import hashlib

def puyun_sign(data: dict, secret: str) -> str:
    items = [(k, v) for k, v in data.items()
             if k != "sign" and v is not None and v != ""]
    items.sort(key=lambda item: item[0].lower())
    text = "".join(f"{k}={v}&" for k, v in items) + f"key={secret}"
    return hashlib.md5(text.encode("utf-8")).hexdigest().upper()

Java 签名

static String puyunSign(Map<String, Object> input, String secret) throws Exception {
    List<Map.Entry<String, Object>> items = input.entrySet().stream()
        .filter(e -> !"sign".equals(e.getKey()))
        .filter(e -> e.getValue() != null && !"".equals(e.getValue()))
        .sorted((a, b) -> a.getKey().compareToIgnoreCase(b.getKey()))
        .toList();
    StringBuilder text = new StringBuilder();
    for (var item : items) text.append(item.getKey()).append('=')
        .append(item.getValue()).append('&');
    text.append("key=").append(secret);
    byte[] digest = MessageDigest.getInstance("MD5")
        .digest(text.toString().getBytes(StandardCharsets.UTF_8));
    return HexFormat.of().withUpperCase().formatHex(digest);
}

PHP 回调骨架

<?php
$params = $_POST;
$receivedSign = $params['sign'] ?? '';
$expectedSign = puyunSign($params, getenv('PUYUNPAY_APP_SECRET'));

if (!hash_equals($expectedSign, strtoupper($receivedSign))) {
    http_response_code(400);
    exit('INVALID SIGN');
}
if (($params['state'] ?? '') !== '2') exit('WAIT');

// 开启数据库事务;锁定 mchOrderNo;核对金额、币种;仅首次成功时入账;提交事务。
header('Content-Type: text/plain; charset=UTF-8');
echo 'SUCCESS';

12. 上线检查清单

商户管理员上线前应绑定 Authenticator,并确认恢复码已离线保存。
  • 商户号、App ID、App Secret 来自同一应用。
  • App Secret 仅保存在服务端环境变量或密钥系统。
  • 如已启用API白名单,确认商户服务器的实际公网出口IP已加入白名单。
  • 通道已由管理员勾选授权。
  • 金额全部使用整数分,数据库保存原始请求金额;USDT 付款严格使用收银台显示的完整随机尾数金额。
  • 商户订单号有唯一索引。
  • 回调先验签,再核对商户、订单、金额、币种和状态。
  • 重复回调不会重复加款或重复发货。
  • notifyUrl 使用 HTTPS,20秒内完成处理并返回 SUCCESS。
  • 下单超时先查单,不直接换订单号重下。
  • 生产日志不打印 App Secret、完整签名原文或用户敏感数据。

13. USDT TRC20代付

代付采用“商户提交订单、同步数字货币后台、管理员审核、人工链上转账、系统回调结果”的模式。系统不保存商户私钥,不自动广播链上交易。仅 USDT_TRC20 支付成功后的净额计入可用余额;提交代付时冻结对应金额并生成数字货币后台付款单,余额不足则不创建订单;同步或审核失败时解冻,人工转账后输入64位TRON交易哈希确认到账并扣除冻结额。

13.1 提交代付订单

POSThttps://pay.125.mom/api/transferOrder
字段必填说明
mchNoappId商户号和应用AppId
mchOrderNo商户代付单号;同一商户必须唯一
ifCode固定为 bitcart
entryType固定为 USDT_TRC20
amount整数,单位分,例如100 USDT传10000
currency固定为 USD
accountNo有效TRC20收款地址
transferDesc代付备注
notifyUrl成功或失败结果通知地址
extParam商户扩展参数,查询及通知原样返回
reqTimeversionsignTypesign与支付接口使用相同签名规则
{
  "mchNo":"M_DEMO_10001","appId":"APP_DEMO_10001",
  "mchOrderNo":"TRANSFER202608190001","ifCode":"bitcart",
  "entryType":"USDT_TRC20","amount":10000,"currency":"USD",
  "accountNo":"TRC20收款地址","transferDesc":"USDT代付",
  "notifyUrl":"https://merchant.example.com/transfer/notify",
  "reqTime":1787078400000,"version":"1.0","signType":"MD5","sign":"服务端计算"
}

同一商户订单号重复提交且关键参数一致时返回原订单;金额、地址、应用或通道等参数不一致时拒绝。状态:1代付中、2成功、3失败。

13.2 查询代付订单

POSThttps://pay.125.mom/api/transfer/query

签名公共参数之外,传 mchOrderNotransferId 之一。订单严格按当前 mchNo + appId 隔离。成功结果中的 channelOrderNo 为TRON交易哈希。

13.3 代付结果通知

订单被管理员确认成功或失败后,系统向下单时的 notifyUrl 发送 application/x-www-form-urlencoded POST通知,字段与查询结果一致,并增加 reqTimesign。商户验签、核对订单和金额并完成幂等处理后,返回纯文本 SUCCESS。未返回SUCCESS时系统最多发送6次。

商户平台仍可在“商户中心 → 代付”提交,并在“订单中心 → 代付订单”查询;提现管理是另一项独立功能。

超管后台删除订单采用软删除;删除仍冻结余额的代付订单时,系统会在同一事务内先释放冻结余额。删除操作不改变已完成的链上交易结果。

14. 当前未开放能力

以下能力暂不作为商户可用接口公开:
  • 退款申请与退款查询
  • 自动链上代付
  • 余额查询
  • 分账
  • 支付宝、微信、云闪付官方直连

即使系统底层存在部分通用路由,也不代表当前通道已经实现。待对应通道真实开发并联调通过后,文档才会增加相关章节。

这里的余额查询指商户开放 API。已绑定商户群可使用机器人只读查询余额和订单,不新增公开余额 API,也不开放群内资金操作。