01接入准备
开始对接前,先在商户后台拿到以下三项凭证。
接口地址(apiurl):https://pay.yunpay.site,后续所有接口均基于该地址。
商户 ID(pid):注册后自动生成,登录商户后台首页即可查看。
商户密钥(key):用于计算签名,在商户后台「接口信息」中获取。密钥只保存在你的服务端,切勿暴露在前端页面或 APP 中。
notify_url 与 return_url 所在域名,需先在商户后台完成接口域名绑定,否则下单会被拒绝。02签名算法(V1·MD5)
所有接口的 sign 字段都按以下规则生成,请求与回调验签使用同一算法。需要更高安全等级的 RSA 签名,请直接看 第 3 章 V2 接口。
将请求参数按 参数名 ASCII 码从小到大排序。
剔除 sign、sign_type 以及值为空的参数。
按 k1=v1&k2=v2&... 拼接(末尾不留 &),不做 URL 编码。
在字符串末尾直接拼接商户密钥,对整串做 MD5(32 位小写)即得 sign。
// 待签名参数(已剔除 sign / sign_type / 空值,并按 ASCII 升序) money=9.90&name=VIP会员¬ify_url=https://demo.com/notify.php&out_trade_no=202610270001&pid=1001&return_url=https://demo.com/return.php&type=alipay // 拼接商户密钥(假设 key 为 abcd1234)后取 MD5 sign = md5("money=9.90&name=VIP会员&...&type=alipayabcd1234")
sign 比对,完全一致才可信。V1 适合快速接入;若需更高安全等级,请切换到 V2(RSA) 并上传商户公钥。V2V2 接口(RSA 密钥对接)
V2 是平台提供的高安全签名模式,使用 RSA 非对称密钥。与 V1(MD5 共享密钥)不同,V2 下双方各持密钥:你用商户私钥给请求签名、平台用商户公钥验签;平台用平台私钥给响应/回调签名、你用平台公钥验签。即使一方私钥泄露也不会危及另一方。
一、两套密钥分别是什么 核心
| 密钥 | 保管方 | 用途 | 在哪里获取 |
|---|---|---|---|
| 平台私钥 | 平台 | 给平台返回的响应、异步通知签名 | 平台保管,你拿不到 |
| 平台公钥 | 你(仅用于验签) | 验证平台发来的响应/通知是否真实 | 商户后台「接口信息 → V2接口」中展示 |
| 商户私钥 | 你(务必离线保管) | 给你的请求签名 | 后台「生成商户RSA密钥对」时展示给你 / 你自己用 openssl 生成 |
| 商户公钥 | 平台 | 平台验证你的请求签名 | 你上传到商户后台「接口信息 → V2接口 → 商户公钥」 |
二、在后台开启 V2
商户后台「接口信息 → 签名方式开关」有三个档位:
开启 MD5+RSA 签名(兼容模式):V1、V2 都能调用,平台按你请求里的 sign_type 自动识别。适合从 V1 平滑升级到 V2。
仅开启 RSA 签名(安全模式):只接受 sign_type=RSA 的请求,提交 MD5 会直接拒绝并返回「该商户只能使用RSA签名类型」。
三、生成并上传商户密钥对
方式一(推荐,后台一键生成):商户后台「接口信息 → V2接口」点击「生成商户RSA密钥对」,平台生成 2048 位密钥对,自动保存你的商户公钥,并把商户私钥展示给你下载保存(请离线保管)。
方式二(自备密钥):你自己用 openssl 生成,把公钥粘贴到后台「商户公钥」框:
openssl genrsa -out merchant_private.pem 2048
openssl rsa -in merchant_private.pem -pubout -out merchant_public.pem
# 把 merchant_public.pem 的内容粘贴到后台「商户公钥」,merchant_private.pem 自己保存
四、请求签名(V2)
拼接待签名串的规则与 V1 完全一致:参数按 ASCII 升序 → 剔除 sign / sign_type / 空值 → 拼成 k=v&(末尾去 &,不做 URL 编码)。区别只在最后一步——用商户私钥对该串做 SHA256withRSA 签名,结果 base64 编码后作为 sign,同时传 sign_type=RSA。
function rsaSign(array $params, $privateKeyPem) { // 1. 排序 + 剔除 sign / sign_type / 空值 ksort($params); $str = ''; foreach ($params as $k => $v) { if ($k=='sign' || $k=='sign_type' || $v==='' || is_array($v)) continue; $str .= $k.'='.$v.'&'; } $str = rtrim($str, '&'); // 2. SHA256withRSA 签名 + base64 $pkey = openssl_pkey_get_private($privateKeyPem); openssl_sign($str, $sign, $pkey, OPENSSL_ALGO_SHA256); return base64_encode($sign); } $params['sign_type'] = 'RSA'; $params['sign'] = rsaSign($params, $merchantPrivateKey);
import hashlib, base64 from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.serialization import load_pem_private_key def rsa_sign(params: dict, private_key_pem: str) -> str: p = {k: v for k, v in params.items() if k not in ('sign', 'sign_type') and v != '' and not isinstance(v, (list, dict))} s = '&'.join(f'{k}={p[k]}' for k in sorted(p)) key = load_pem_private_key(private_key_pem.encode(), password=None) sig = key.sign(s.encode(), padding.PKCS1v15(), hashes.SHA256()) return base64.b64encode(sig).decode() params['sign_type'] = 'RSA' params['sign'] = rsa_sign(params, merchant_private_key)
五、验证平台响应 / 异步通知(V2)
平台响应 / 异步通知的签名类型,取决于订单的创建方式(订单 version 字段),而不是你后台的 keytype 设置:
| 下单方式 | 通知 / 响应签名 | 你用什么验签 |
|---|---|---|
通过 ?s= 接口下单(如 api.php?s=pay/submit、api.php?s=pay/create) | sign_type=RSA(平台私钥签名) | 用商户后台展示的平台公钥做 SHA256withRSA 验签 |
通过传统 submit.php / mapi.php 下单 | sign_type=MD5(你的商户密钥签名) | 按第 2 章 V1 规则用商户密钥重算 MD5 比对 |
sign_type=RSA 即用商户私钥);但平台回推给你的通知用哪种签名,由这笔订单当初怎么建的来决定。下方 RSA 验签代码仅适用于 ?s= 下单产生的 RSA 通知与 ?s= 接口响应。?s= 接口下单,这样请求与通知全程 RSA,验签只认平台公钥。若用 mapi.php 下单,平台仍会以 MD5 回推通知,须用商户密钥做 MD5 验签,与 V2 的 RSA 验签是两套逻辑。function rsaVerify(array $data, $platformPublicKey) { if (!isset($data['sign'])) return false; ksort($data); $str = ''; foreach ($data as $k => $v) { if ($k=='sign' || $k=='sign_type' || $v==='' || is_array($v)) continue; $str .= $k.'='.$v.'&'; } $str = rtrim($str, '&'); $pkey = openssl_pkey_get_public($platformPublicKey); return openssl_verify($str, base64_decode($data['sign']), $pkey, OPENSSL_ALGO_SHA256) === 1; }
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import padding, rsa from cryptography.hazmat.primitives.serialization import load_pem_public_key def rsa_verify(data: dict, platform_public_key: str) -> bool: d = {k: v for k, v in data.items() if k not in ('sign', 'sign_type') and v != '' and not isinstance(v, (list, dict))} s = '&'.join(f'{k}={d[k]}' for k in sorted(d)) key = load_pem_public_key(platform_public_key.encode()) try: key.verify(base64.b64decode(data['sign']), s.encode(), padding.PKCS1v15(), hashes.SHA256()) return True except Exception: return False
六、关于 timestamp(时间戳)
通过平台 ?s= 形式的 RESTful 接口(如 pay/submit、pay/create)调用时,需额外传 timestamp(Unix 秒),与平台服务器时间误差须 ≤ 300 秒,否则返回「时间戳字段不正确,请检查服务器时间」。
04页面跳转支付
用户浏览器跳转到收银台完成付款,适合网页/移动端直接接入。
| 参数 | 必填 | 说明 |
|---|---|---|
pid | 必填 | 商户 ID |
type | 选填 | 支付方式:alipay、wxpay 等。留空则跳转收银台由用户自选 |
out_trade_no | 必填 | 商户订单号,仅允许字母、数字与 ._-|,需保证唯一 |
notify_url | 必填 | 异步通知地址(服务端,POST 不可达时为 GET) |
return_url | 必填 | 支付成功后浏览器跳转地址 |
name | 必填 | 商品名称 |
money | 必填 | 金额,单位元,最多两位小数,如 9.90 |
sitename | 选填 | 站点名称,收银台展示用 |
param | 选填 | 自定义回传参数,回调时原样返回 |
sign / sign_type | 必填 | 签名 与 签名类型(MD5) |
<!-- 组装参数并计算 sign 后,以 GET 跳转或 POST 表单提交 --> <form method="post" action="https://pay.yunpay.site/submit.php"> <input type="hidden" name="pid" value="1001"> <input type="hidden" name="type" value="alipay"> <input type="hidden" name="out_trade_no" value="202610270001"> <input type="hidden" name="notify_url" value="https://demo.com/notify.php"> <input type="hidden" name="return_url" value="https://demo.com/return.php"> <input type="hidden" name="name" value="VIP会员"> <input type="hidden" name="money" value="9.90"> <input type="hidden" name="sign_type" value="MD5"> <input type="hidden" name="sign" value="{按第 2 节规则计算}"> <button type="submit">去支付</button> </form>
return_url 仅作展示跳转,不能作为发货依据;发货必须以异步通知或主动查单结果为准。05API 下单
服务端直接下单,返回 JSON(含支付跳转链接 / 二维码),适合后端服务与 APP 内拉起支付。
请求参数与「页面跳转支付」基本一致,但有两点区别:① clientip(用户真实 IP)为必填,且必须是合法 IPv4/IPv6,否则报「用户IP地址(clientip)不能为空 / 不合法」;② type(支付方式)在 API 下单时为必填(页面跳转留空则由用户自选)。即需 POST:pid、type、out_trade_no、notify_url、return_url、name、money、clientip、sign、sign_type。
返回字段
成功时返回 code=1 与 trade_no,并按支付方式只返回下述其中一个支付字段(不是全部同时返回):
| 字段 | 出现条件 | 说明 |
|---|---|---|
code | 总是 | 1 成功;-2 下单失败(原因见 msg) |
trade_no | 成功时 | 平台订单号,可用于后续查单 |
payurl | 跳转 / H5 支付 | 收银台跳转链接,引导用户打开即可付款 |
qrcode | 扫码支付 | 扫码支付二维码内容(可直接生成二维码图片) |
html | 自动提交表单 | 需服务端/前端原样输出的 HTML 表单(自动跳转支付) |
urlscheme | 小程序 / H5 scheme | 小程序或 H5 深链 scheme 地址 |
msg | 失败时 | 失败原因(成功时不返回该字段) |
pay_type / pay_info)仅由 ?s= 形式的 RESTful 接口提供;mapi.php 统一以 payurl / qrcode / html / urlscheme 之一返回,接到后按字段引导用户即可。{
"code": 1,
"trade_no": "2026102714302512345",
"payurl": "https://bei2.yunpay.site/pay/submit/2026102714302512345/"
}
{
"code": -2,
"msg": "用户IP地址(clientip)不能为空"
}
06异步通知(notify_url)
订单支付成功后,平台向 notify_url 发起通知。请以通知结果作为发货唯一依据。
通知方式 GET
平台以 GET 方式请求你的 notify_url,附加以下参数:
| 参数 | 说明 |
|---|---|
pid | 商户 ID |
trade_no | 平台订单号 |
out_trade_no | 商户订单号 |
type | 实际支付方式 |
name | 商品名称 |
money | 订单金额(数值,如 9.9) |
trade_status | TRADE_SUCCESS 表示支付成功 |
api_trade_no | 上游渠道订单号(部分通道有值,可能不存在) |
param | 下单时传入的自定义参数(原样回传) |
sign / sign_type | 签名与签名类型(MD5 或 RSA,取决于该订单创建方式,见第 3 章第 5 节) |
按第 2 节规则对收到的参数本地重新验签,不一致直接忽略。
校验 out_trade_no 是否存在于你的订单系统、money 是否与订单金额一致。
确认 trade_status 为 TRADE_SUCCESS 后执行发货/入账逻辑。
处理成功后输出纯文本 success(不要有任何多余字符);未收到 success 平台会按退避策略自动重试。
07订单查询
按平台订单号或商户订单号查询支付结果,用于主动查单补单。
也可用 &trade_no={平台订单号} 替换 out_trade_no,二者传其一即可。
返回字段
| 字段 | 说明 |
|---|---|
code | 1 查询成功,-1 订单不存在,-3 商户ID不存在 / 商户密钥错误 / 该商户只能使用RSA签名类型 |
msg | 结果描述,成功时为 succ |
pid | 商户 ID |
trade_no / out_trade_no | 平台订单号 / 商户订单号 |
api_trade_no / bill_trade_no | 上游渠道订单号 / 上游流水号(部分通道有值,可能为空 / null) |
type | 支付方式名称 |
name / money | 商品名称 / 金额(字符串,如 "123.00") |
addtime / endtime | 下单时间 / 支付完成时间 |
status | 0 未支付,1 已支付 |
buyer | 付款方账号(部分通道返回) |
param | 下单时的自定义参数(无则为 null) |
payurl | 本笔订单的收银台链接(可用于重新发起支付,无则为 null) |
pid + key 即可,平台按密钥做身份校验(请勿在公网前端页面暴露 key)。其中 money / status / pid 等字段以字符串形式返回。08商户信息查询
查询商户余额、结算方式与订单概况。
| 字段 | 说明 |
|---|---|
code | 1 查询成功,-3 商户ID不存在 / 商户密钥错误 / 该商户只能使用RSA签名类型 |
pid | 商户 ID |
key | 商户密钥(与后台一致,可用于本地校验) |
money | 商户当前余额 |
active | 商户状态(1 正常) |
type | 结算方式 ID |
account / username | 结算账号 / 收款人 |
orders / orders_today / orders_lastday | 总订单数 / 今日成功 / 昨日成功 |
09接入示例
下单 + 回调验签的最小可运行示例,可直接作为参考实现。以下示例使用 V1(MD5);若要用 V2(RSA),只需把签名函数替换为第 3 节的 rsaSign() 并将 sign_type 改为 RSA 即可,其余参数完全相同。
<?php function getSign($params, $key) { ksort($params); $str = ''; foreach ($params as $k => $v) { if ($k == 'sign' || $k == 'sign_type' || $v === '') continue; $str .= $k . '=' . $v . '&'; } return md5(substr($str, 0, -1) . $key); } $params = [ 'pid' => '1001', 'type' => 'alipay', 'out_trade_no' => '202610270001', 'notify_url' => 'https://demo.com/notify.php', 'return_url' => 'https://demo.com/return.php', 'name' => 'VIP会员', 'money' => '9.90', ]; $params['sign_type'] = 'MD5'; $params['sign'] = getSign($params, '你的商户密钥'); // 页面跳转支付:跳转到 submit.php?http_build_query($params) // API 下单:POST 到 mapi.php,解析返回 JSON 中的 payurl / qrcode
<?php $sign = $_GET['sign']; unset($_GET['sign'], $_GET['sign_type']); if (getSign($_GET, '你的商户密钥') !== $sign) exit('sign error'); if ($_GET['trade_status'] != 'TRADE_SUCCESS') exit('fail'); // 1) 按订单号查出你的订单 2) 比对金额 3) 幂等发货 // $order = db_query($_GET['out_trade_no']); // if ($order && $order['status'] == 0 && $order['money'] == $_GET['money']) { mark_paid_and_deliver(); } echo 'success'; // 只输出 success,其他内容视为失败会触发重试
import hashlib, requests def get_sign(params, key): items = sorted((k, v) for k, v in params.items() if k not in ('sign', 'sign_type') and v != '') s = '&'.join(f'{k}={v}' for k, v in items) return hashlib.md5((s + key).encode()).hexdigest() params = {'pid': '1001', 'type': 'alipay', 'out_trade_no': '202610270001', 'notify_url': 'https://demo.com/notify.php', 'return_url': 'https://demo.com/return.php', 'name': 'VIP会员', 'money': '9.90', 'clientip': '1.2.3.4'} # clientip 为 API 下单必填,填用户真实 IP params['sign_type'] = 'MD5' params['sign'] = get_sign(params, '你的商户密钥') r = requests.post('https://pay.yunpay.site/mapi.php', data=params).json() print(r.get('payurl') or r.get('qrcode') or r.get('html') or r.get('urlscheme'))
10常见问题
对接高频问题与排错入口。
| 报错 | 原因与处理 |
|---|---|
签名校验失败 | 参数排序/空值剔除有误、密钥不对、或对参数做了 URL 编码。严格按第 2 节(V1)或第 3 节(V2)规则重算 |
该商户只能使用RSA签名类型 | 后台已将签名方式设为「仅开启RSA签名(安全模式)」,但请求仍用 MD5。改为 sign_type=RSA 并按第 3 节用商户私钥签名;兼容模式下两种都可 |
签名校验失败,商户公钥错误 | V2 模式下商户公钥与私钥不匹配或格式不对。重新「生成商户RSA密钥对」或确认上传的公钥与本地私钥是同一对 |
时间戳字段不正确,请检查服务器时间 | 仅在 ?s= 接口要求 timestamp,且与平台时间差 > 300 秒。校准你的服务器时间 |
订单号格式不正确 | out_trade_no 只允许字母、数字与 . _ - | |
金额不合法 | money 必须为纯数字且大于 0,最多两位小数 |
域名未绑定 | notify_url / return_url 的域名需先在商户后台完成绑定 |
| 回调收不到 | 检查 notify_url 公网可达、无防火墙拦截;同时用订单查询接口做定时补单兜底 |
| 重复回调 | 正常现象,发货逻辑按订单号做幂等即可 |