接口说明
同一套能力有三个入口世代,新接入建议用 V3(JSON、金额以分为单位、幂等键、机读错误码); V2 是给已经按 V2 写过代码的人留的;V1 保留是为了让老代码不换一行就能迁过来。
| 世代 | 入口 | 鉴权 | 成功码 | 适用 |
|---|---|---|---|---|
| V3 | POST /v3/…(JSON) |
接入号 + 该接入号专属 AppSecret 的 HMAC-SHA256 签名 + 时间戳 + nonce | 0 | 新接入、需要幂等与机读错误码 |
| V1 | /submit.php、/mapi.php、/api.php?act=… |
pid + 明文商户密钥 key | 1(退款例外为 0) | 老彩虹/易支付 SDK 原样迁移 |
| V2 | /api.php?s=类/方法 或 /api/类/方法 |
MD5 或 RSA 签名 + 时间戳 | 0 | 表单式服务端对服务端 |
三代打的是同一套订单和同一本账:V1 下的单可以在 V3 查、在 V3 退, 不会因为换了接口就出现两套账。但凭据是分开的——V1/V2 用商户号 + 商户密钥,V3 用它自己的「接入号 + AppSecret」, 两边互不影响:重置 V3 的密钥不会打断任何 V1/V2 的老程序,反之也一样。 异步通知的口径跟着下单那一代走(见「支付结果通知」与「V3 异步通知」)。
- 商户号与密钥:注册或进件通过后在商户中心「API 信息」页查看,供 V1/V2 使用。密钥只放在你们服务器,不要进 App、小程序前端或 URL。
- V3 接入凭据:同一个商户中心里申请的接入号(AppID)+ AppSecret,一个商户号下可以有多个接入号,各自独立授权、独立重置,见「V3 接入凭据」。
- 签名类型:默认 MD5(商户密钥)。上传 RSA 公钥并切成 RSA 后,该商户不再接受任何 MD5 请求(V3 用自己的 HMAC 口径,不吃这个开关)。
- 平台公钥:V2 的应答、以及 V2 下单产生的异步通知都由平台 RSA 私钥签名,用平台公钥验签,同在「API 信息」页。
- 字符与金额:全站 UTF-8;V1/V2 金额为「元」的字符串、最多两位小数(内部按分记账不透出),V3 金额一律是「分」的整数;时间格式 V1/V2 为
Y-m-d H:i:s、V3 同样。 - 回调地址:notify_url 必须公网可直连,且能按订单号幂等处理(通知会重复)。
. _ - |;
商品名称 name 超过 127 字节按 UTF-8 边界截断,不会劈开汉字。签名规则
待签名串拼法(除 V1 的 api.php?act= 不签名外,所有签名接口与回调共用):
- 剔除
sign、sign_type,以及值为空或全空白的参数; - 剩余参数按参数名 ASCII 升序,拼成
k1=v1&k2=v2,值不做 URL 编码; - MD5:
sign = md5(待签名串 + 商户密钥),32 位小写; - RSA:
sign = base64(SHA256withRSA(待签名串, 商户私钥))。
应答与通知侧是同一条拼串规则,签名方换成平台私钥(仅 RSA 一支),且 timestamp 参与拼串。
V2 请求必须带 timestamp(秒),与服务器相差 300 秒内有效。
// 参考实现,其它语言照这几步复刻即可
function sign_content(params) {
delete params.sign; delete params.sign_type;
return Object.keys(params)
.filter(k => String(params[k]).trim() !== '')
.sort()
.map(k => k + '=' + params[k])
.join('&');
}
sign = md5(sign_content(params) + merchantKey); // MD5
// RSA:商户私钥做 sha256WithRSAEncryption 后 base64
支付方式列表
type 参数取下面这些名称之一(不是数字 id)。列表读后台实时配置,停用或新增会同步出现在这里。
| type 取值 | 显示名 |
|---|---|
| alipay | 支付宝 |
| wxpay | 微信支付 |
| usdt | USDT(链上收款) |
页面下单(submit.php / pay/submit)可以不传 type:留空时买家在平台收银台自己选。 接口下单必须传,且该方式要对你的商户号开通,否则下单被拒。
错误码与订单状态
对外订单状态统一三个数字,内部终态不透出:
| status | 含义 |
|---|---|
| 0 | 未支付(含已关闭、失败) |
| 1 | 已支付 |
| 2 | 已退款 |
| code | 出现位置 | 含义 |
|---|---|---|
| 1 | V1 各 act、mapi 下单 | 成功(唯 V1 的 act=refund 例外,它用 0) |
| 0 | V2 全部、V1 的 act=refund | 成功 |
| -1 | 两代都有 | 业务失败或订单不存在;接口单下单失败也折成 -1 |
| -2 | V1 refund、V2 transfer/* | 该商户没开这个接口的权限 |
| -3 | 鉴权 | 商户号 / 密钥 / 签名 / 时间戳问题 |
| -4 | 参数 | 必填缺失,或站点级开关关闭 |
| -5 | V2 路由 | URL 形状不对(URL Error!)或方法未开通(接口方法不存在) |
V2 的失败分两种:参数与鉴权类回裸的 {code,msg}、不带签名;
业务本身拒绝(状态不对、超过可退)回 {code:-1,msg} 且照样带平台签名。
V3 不用这张表,它回五位错误码 + error_code 机读名,见「V3 错误码」。
V3 原生接口
/v3/pay/create · /v3/pay/query · /v3/pay/refund · /v3/pay/close · /v3/merchant/info · /v3/merchant/balanceV3 总览
V3 是这套网关自己的协议:请求体 JSON、应答 JSON、金额用分的整数、签名是 HMAC-SHA256, 再配幂等键和每请求一个 request_id。它不改动任何资金与订单口径——打的是同一套订单、同一本账, 所以 V1 下出去的单可以在 V3 查、在 V3 退。
V3 比 V1/V2 多的东西
| 能力 | V1 / V2 | V3 |
|---|---|---|
| 报文 | 表单 k=v,应答字段平铺 | JSON 请求 + JSON 应答,业务字段收在 data 里 |
| 金额 | 「元」的字符串,最多两位小数 | 分的整数,没有小数点也就没有四舍五入 |
| 鉴权 | V1 明文密钥;V2 参数拼串 MD5/RSA | 请求体哈希 + HMAC-SHA256,密钥不进待签名串 |
| 重放防护 | 仅时间戳 | 时间戳 + nonce,窗口内同一个 nonce 只用一次 |
| 重复提交 | 靠 out_trade_no 唯一约束兜住 | Idempotency-Key:网络超时后原样重试不会二次扣钱 |
| 失败判定 | 读中文 msg | 五位错误码 + error_code 机读名,msg 仍然能直接给买家看 |
| 排障 | 报 trade_no | 每个请求带 request_id,报一个号就能定位那次调用 |
公共请求头
| 请求头 | 必填 | 示例值 | 描述 |
|---|---|---|---|
| X-Spay-App-Id | 是 | app_6f1c2d3e4a5b6c7d | 接入号,V3 专用,在商户中心「API 信息」页发放;它决定用哪把 AppSecret 验签 |
| X-Spay-Timestamp | 是 | 1761370000 | 秒;与服务器相差 300 秒内有效 |
| X-Spay-Nonce | 是 | n-8f3c1a2b9d | 8~64 位,字母数字与短横线;窗口内不可重复 |
| X-Spay-Sign | 是 | 64 位十六进制 | HMAC-SHA256,见「V3 签名」 |
| Content-Type | 是 | application/json | 请求体上限 64 KB,超了直接拒 |
| Idempotency-Key | 写接口建议(退款必填) | 2f6b9c41-d0a5-… | 下单/关单建议带,退款不带直接回 40001;见「幂等键」 |
应答信封
{"code":0,"message":"success","request_id":"req_2f6b9c41d0a53e17","data":{ … }}
| 变量名 | 类型 | 描述 |
|---|---|---|
| code | Int | 0 成功;非 0 就是五位错误码,见「V3 错误码」 |
| message | String | 成功为 success,失败是一句中文原因,可以原样给买家看 |
| request_id | String | 每个请求一个,同时回在响应头 X-Spay-Request-Id;找平台排障就报这个号 |
| data | Object | 业务字段,只有成功时才出现 |
| error_code | String | 机读错误名(如 ORDER_NOT_FOUND),只有失败时才出现;代码里判它,不要判中文 |
"amount": 1234。
这和 V1/V2 相反(那边是「元」的字符串),跨代迁移最容易出错的就是它——
上线前建议下一笔最小金额,用查单接口核对一次金额与入账值。V3 接入凭据
V3 不吃 V1/V2 那把商户密钥,用的是单独发放的接入凭据。一个商户号下可以开多个接入号, 每个接入号一把密钥、一份权限、一份 IP 白名单——哪个程序泄露了就停用哪个,其它业务照跑。
| 项目 | 长什么样 | 怎么用 |
|---|---|---|
| 接入号 AppID | app_ 开头 16 位十六进制 |
放请求头 X-Spay-App-Id,明文传输,不是秘密 |
| AppSecret | spayv3_ 开头 40 位十六进制 |
只用来算 HMAC 签名,永不进请求;发放时只显示一次,忘了只能重置 |
| 能力范围 scopes | pay / read / refund |
新建默认给 pay,read;退款要单独勾,没勾调退款回 40301 / ACCESS_DENIED |
| IP 白名单 | 逗号分隔,支持网段(如 1.2.3.4,5.6.7.0/24) |
留空表示不限来源;填了就只有名单内的服务器能用这把密钥,密钥泄露也调不动 |
| 状态 | 启用 / 停用 | 停用即刻生效,所有该接入号的请求回 40301,已下的单不受影响 |
| 最近调用 | 时间 | 商户中心展示,用来判断这把密钥还在不在被人用 |
轮换(换密钥不断单)
点「重置密钥」后,新密钥即刻可用,旧密钥再保留 24 小时:这期间两把都能验签, 你可以先把一部分服务器切过去、再切剩下的,不用停机也不用两边同时改配置。24 小时一过旧密钥作废。
应答验签(可选)
每个 V3 应答都额外带两个响应头,用同一个 AppSecret 可验证应答确实来自平台、没被中间人改过:
X-Spay-Resp-Timestamp: 1761370001
X-Spay-Resp-Sign: HMAC-SHA256("v3." + timestamp + "
" + request_id + "
" + sha256_hex(应答体), AppSecret)
查单、对账这类动钱的调用建议验;普通的下单请求看一眼 code 也够用。
V3 签名
待签名串是三行拼成的一个字符串,下面的 \n 是真正的换行符:
v3.<timestamp>
<nonce>
<sha256_hex(请求体原始字节)>
- 第一段固定前缀
v3.,timestamp 与 nonce 取请求头里的原值; - 第三段是请求体原始字节的 SHA-256 十六进制小写:不是按字段排序、不是格式化后的 JSON。算完签名就不要再重新序列化一次;
- 签名 = HMAC-SHA256(待签名串, 该接入号的 AppSecret),输出 64 位十六进制小写,放
X-Spay-Sign; - 异步通知用同一套算法,密钥是这笔下单时所用接入号的那把 AppSecret(通知体里的
app_id会告诉你是哪个接入号), 只是签名方换成平台,请求侧和验签侧代码共用一份; - 应答头里的
X-Spay-Resp-Sign也用它,见「V3 接入凭据」;轮换窗口内旧 AppSecret 仍可验签,新签出来的永远用当前这把。
import hashlib, hmac, json, time, uuid, requests
BASE = "https://pay.your-site.com"
APP_ID = "app_6f1c2d3e4a5b6c7d" # 商户中心「API 信息」页发的接入号
SECRET = "spayv3_...40位十六进制..." # 与它配对的 AppSecret,只放服务器
body = json.dumps({"out_trade_no": "P202610250001", "amount": 1234,
"subject": "VIP会员", "pay_type": "alipay",
"notify_url": "https://your.site/pay/notify"},
separators=(',', ':')).encode('utf-8')
ts, nonce = str(int(time.time())), "n-" + uuid.uuid4().hex[:12]
content = "v3.%s\n%s\n%s" % (ts, nonce, hashlib.sha256(body).hexdigest())
sign = hmac.new(SECRET.encode(), content.encode(), hashlib.sha256).hexdigest()
resp = requests.post(BASE + "/v3/pay/create", data=body, headers={
"X-Spay-App-Id": APP_ID, "X-Spay-Timestamp": ts,
"X-Spay-Nonce": nonce, "X-Spay-Sign": sign,
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4())})
print(resp.json()) # 先判 code == 0,再取 data
/v3/pay/create统一下单 V3
服务端对服务端下单。带 pay_type 时直接拿到买家要用的收款信息;不带时落平台收银台,让买家自己选支付方式。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 商户订单号 | out_trade_no | 是 | String | P202610250001 | 同一商户下唯一;字母数字与 . _ - |,最长 64 |
| 金额 | amount | 是 | Int | 1234 | 单位:分,正整数;低于起收额或高于单笔上限直接拒 |
| 商品名称 | subject | 是 | String | VIP会员 | 超过 127 字节按 UTF-8 边界截断,不劈开汉字 |
| 支付方式 | pay_type | 否 | String | alipay | 见支付方式列表;留空进平台收银台让买家选 |
| 异步通知地址 | notify_url | 是 | String | https://your.site/pay/notify | 该单的通知按 V3 格式发 JSON POST(见「V3 异步通知」) |
| 跳转通知地址 | return_url | 否 | String | https://your.site/order/1 | 买家付完跳回你的页面 |
| 买家IP | client_ip | 否 | String | 1.2.3.4 | 不传取当前连接来源 IP;同 IP 限速与风控全靠它 |
| 业务扩展参数 | param | 否 | String | uid=88 | 原样回传在通知与查单里 |
| 买家账号 | buyer | 否 | String | oUpDtx… | 公众号/小程序 openid |
返回参数(data)
| 变量名 | 类型 | 描述 |
|---|---|---|
| trade_no | String | 平台订单号 |
| out_trade_no | String | 你的订单号,原样回显 |
| amount | Int | 本单应收(分) |
| status | String | 新建单为 pending(未指定支付方式)或 paying(已派通道,等买家付) |
| pay_mode | String | qrcode 二维码内容 / html 自提交表单 / jump 跳转地址 / hosted 平台收银台 |
| pay_info | String | 与 pay_mode 对应的那份内容,渲染方式见下表 |
| cashier_url | String | 平台收银台地址,任何时候都有值;拿不准就把它发给买家 |
| created_at | String | 下单时间 Y-m-d H:i:s |
pay_mode 怎么用
| pay_mode | pay_info 是什么 | 你该怎么做 |
|---|---|---|
| qrcode | 二维码的原始内容(不是图片地址) | 自己把它画成二维码给买家扫 |
| html | 一段自动提交表单 | 原样输出到页面,不要包装、不要转义 |
| jump | 上游收款页 URL | 302 或让买家点开 |
| hosted | 平台收银台 URL | 直接跳转,买家在收银台选支付方式并付款 |
请求示例
curl -X POST https://spay.dlam.cc/v3/pay/create \
-H "Content-Type: application/json" \
-H "X-Spay-App-Id: app_6f1c2d3e4a5b6c7d" -H "X-Spay-Timestamp: 1761370000" \
-H "X-Spay-Nonce: n-8f3c1a2b9d" -H "X-Spay-Sign: 64位十六进制" \
-H "Idempotency-Key: 2f6b9c41-d0a5-4b1e-9a37-1c0d7e5f2a44" \
-d '{"out_trade_no":"P202610250001","amount":1234,"subject":"VIP会员","pay_type":"alipay","notify_url":"https://your.site/pay/notify"}'
返回示例
{"code":0,"message":"success","request_id":"req_2f6b9c41d0a53e17",
"data":{"trade_no":"202610250751412050917c98acfe03","out_trade_no":"P202610250001",
"amount":1234,"status":"paying","pay_mode":"jump",
"pay_info":"https://…","cashier_url":"https://spay.dlam.cc/pay/202610250751412050917c98acfe03",
"created_at":"2026-10-25 07:51:41"}}
幂等键
下单、退款、关单三个写接口支持 Idempotency-Key,解决的是同一件事:
请求发出去了、响应没收到(超时、连接被切断、网关 502),你不知道服务端到底做没做。
| 情况 | 服务端行为 | 你收到什么 |
|---|---|---|
| 第一次带这个 key | 正常执行,应答存下来 | 正常应答 |
| 同 key、同请求体重试 | 不再执行业务,把存下来的应答原样回给你 | 应答与首次一致,响应头多一个 X-Spay-Idempotent: true |
| 同 key、不同请求体 | 拒绝 | 409 + 40901 / IDEMPOTENCY_CONFLICT |
| 同 key,首次请求还在处理中 | 不并发执行第二遍 | 409 + 40901,message 提示稍后重试 |
- key 用 UUID 或你的雪花号,至少 8 位、最长 64 位;每个 key 只在服务端保留 24 小时。
- 一个 key 只能用在同一个端点:服务端把它连同端点一起存,拿
pay/create用过的 key 去打pay/refund直接判 40901——否则回的会是那次建单的应答,等于替你确认了一笔没做的退款。换端点请换 key。 - 重试要三个数都不变:Idempotency-Key、请求体、商户号;请求体变了就是另一次业务,服务端会拒绝而不是替你猜。
- key 只管「同一次业务调用别执行两遍」,订单唯一性仍由 out_trade_no 兜底——两道都在,缺一道理赔都说不清。
- 不带 key 也能调通,只是超时后你只能先查单确认,再决定要不要重下。
/v3/pay/query订单查询 V3
查单笔订单,只查得到自己商户的单。请求体两个单号任填其一,都填时按 trade_no。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 平台订单号 | trade_no | 二选一 | String | 20261025075141… | — |
| 商户订单号 | out_trade_no | 二选一 | String | P202610250001 | 别人的单和没见过的单回同一句话 |
返回参数(data)
| 变量名 | 类型 | 描述 |
|---|---|---|
| trade_no / out_trade_no | String | 平台单号 / 你的单号 |
| merchant_no | String | 商户号 |
| subject | String | 商品名称 |
| pay_type | String | 支付方式名称,未选时为 cashier |
| amount | Int | 应收(分) |
| real_amount | Int | 买家实付(分);未支付为 0 |
| get_amount | Int | 商户应得(分);未支付为 0 |
| refund_amount | Int | 已退金额(分) |
| status | String | 见下表;收了钱的状态是 paid / notified / settled 三个,都算已收款 |
| channel_trade_no | String | 上游交易号,未派通道时为空 |
| bill_trade_no | String | 账单交易号,拿去对账用这个 |
| buyer / param | String | 买家账号 / 下单透传参数 |
| created_at / paid_at | String | 下单时间 / 到账时间;未支付 paid_at 为 null |
status 取值
| status | 含义 | 是否终态 |
|---|---|---|
| pending | 已建单,还没选到通道(多为收银台单) | 否 |
| paying | 已派通道,等买家付款 | 否 |
| paid | 钱已到账,账已落,通知还没成功送达 | 已收款 |
| notified | 已收到平台的支付通知 | 已收款 |
| settled | 已结算(押款也放完了) | 已收款 |
| refunded | 已退款 | 终态 |
| closed | 已关闭(人工或超时) | 终态 |
| failed | 通道全部下单失败 | 终态,可原单号重下 |
V1/V2 的 status 是 0/1/2 三个数字(0 未支付、1 已支付、2 已退款,closed 与 failed 都折进 0);
V3 直接回上面这些字符串,判「钱到了」用 paid / notified / settled 三个。
返回示例
{"code":0,"message":"success","request_id":"req_7c1f0a9e5b2d4318",
"data":{"trade_no":"202610250751412050917c98acfe03","out_trade_no":"P202610250001",
"merchant_no":"1001","subject":"VIP会员","pay_type":"alipay",
"amount":1234,"real_amount":1234,"get_amount":1180,"refund_amount":0,
"status":"paid","channel_trade_no":"20261025075141001","bill_trade_no":"202610252200…",
"buyer":"","param":"uid=88",
"created_at":"2026-10-25 07:51:41","paid_at":"2026-10-25 08:02:11"}}
/v3/pay/refund退款 V3
商户自助退款,从商户余额扣回,需要商户开通退款权限。支持部分退款,每一笔都是独立退款单。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 平台订单号 | trade_no | 二选一 | String | 20261025075141… | 与 out_trade_no 任填其一 |
| 商户订单号 | out_trade_no | 二选一 | String | P202610250001 | — |
| 退款金额 | amount | 是 | Int | 1234 | 单位:分;不超过入账金额与剩余可退 |
| 商户退款单号 | out_refund_no | 是 | String | R20261025001 | 6~64 位;同一单号重复提交不会二次扣钱 |
请求头还必须带 Idempotency-Key:退款是唯一「不带就拒」的写接口,缺了回
40001,因为超时重试时它是第二个凭据——out_refund_no 由你自己生成,
Idempotency-Key 证明「这条报文就是我刚才那条」。两个都带才是稳的。
返回参数(data)
| 变量名 | 类型 | 描述 |
|---|---|---|
| refund_no | String | 平台退款单号,退款查询用它 |
| out_refund_no | String | 你传的退款单号,原样回给你 |
| trade_no / out_trade_no | String | 原订单号 |
| amount | Int | 本次退款金额(分) |
| reduced_amount | Int | 实际从余额扣回的金额(分) |
| refunded_amount | Int | 这一单累计已退(分) |
| status | String | 全额退完为 refunded,部分退款仍为 paid |
| deduplicated | Bool | true 表示这个 out_refund_no 以前退过,本次没有扣款、也没打上游,回的是上次那笔的结果 |
返回示例
{"code":0,"message":"success","request_id":"req_3a8e5d2c1b047f96",
"data":{"refund_no":"20261025081122331156","out_refund_no":"R20261025001",
"trade_no":"202610250751412050917c98acfe03","out_trade_no":"P202610250001",
"amount":1234,"reduced_amount":1210,"refunded_amount":1234,"status":"refunded",
"deduplicated":false}}
deduplicated:true 是告诉你「这笔早退过了,没重复扣」而不是让你再退。40301 / ACCESS_DENIED,余额不足回 42201 / BUSINESS_REJECTED,
状态或额度不对回 42201——判 error_code 之外,msg 那句中文和老接口是一致的。/v3/pay/close关闭订单 V3
关闭一笔未支付订单。先让上游关,上游关掉了本地才落 closed—— 不会本地一关了之,否则上游二维码还活着,买家随后付成功就成了没人认领的收款。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 平台订单号 | trade_no | 二选一 | String | 20261025075141… | 与 out_trade_no 任填其一 |
| 商户订单号 | out_trade_no | 二选一 | String | P202610250001 | — |
成功回 {code:0,message:"success",request_id:"…",data:{trade_no,status:"closed"}}。
已支付或已退款的单不可关,回 42201 / BUSINESS_REJECTED;
聚合类上游没有关单接口,回 42201 且 message 说明「当前支付插件不支持关闭订单」。
/v3/merchant/info · /v3/merchant/balance商户信息与余额 V3
两个接口都无业务参数,请求体传空对象 {}(仍要正常签名)。
data · /v3/merchant/info
| 变量名 | 类型 | 描述 |
|---|---|---|
| merchant_no / name | String | 商户号 / 商户名称 |
| status | String | normal 正常 / reviewing 待审核 / disabled 停用 |
| pay_on / refund_on / transfer_on / settle_on | Bool | 收款 / 退款 / 代付 / 自动结算 四项权限 |
| min_amount / max_amount | Int | 单笔起收与上限(分),0 表示不限 |
| settle_cycle | String | 结算周期,如 D+1 |
| created_at | String | 开户时间 |
data · /v3/merchant/balance
| 变量名 | 类型 | 描述 |
|---|---|---|
| available | Int | 可提现余额(分) |
| pending_settlement | Int | 待结算(分):账期压着的钱,提不了现,到期自动放进 available |
| frozen | Int | 代付已提交、等管理员打款而冻结的钱(分) |
| currency | String | 固定 CNY(分) |
余额口径与商户中心「资金账户」页完全一致:三个数是三本账,加起来不等于「今天收了多少」, 因为结算周期会把今天的收入压到明天才放出 available。
你的 notify_urlV3 异步通知
V3 下的单发 V3 通知:JSON POST 到你的 notify_url,签名在请求头上,正文里没有 sign 字段。
V1/V2 下的单仍按各自那一代的格式发(见「支付结果通知」)。判定送达的方式变了:HTTP 状态码 2xx 就算收到,
不需要再输出 success 这个字。
通知请求头
| 请求头 | 示例值 | 描述 |
|---|---|---|
| X-Spay-Timestamp | 1761370000 | 发送时间(秒),参与签名 |
| X-Spay-Nonce | n-1d0a53e17f6b9c42 | 每次发送都换一个新值,可用来识别重复通知 |
| X-Spay-Sign | 64 位十六进制 | HMAC-SHA256,待签名串算法与请求侧一模一样,用你的商户密钥验 |
| Content-Type | application/json | 正文是 JSON |
通知正文
| 变量名 | 类型 | 描述 |
|---|---|---|
| event | String | 固定 trade.paid |
| notify_id | String | 固定为 np_ + 平台订单号,同一笔单每次重发都不变;去重请用它或 trade_no,别用 nonce |
| app_id | String | 这笔单下单时用的接入号;验签就用它对应的 AppSecret,一个商户号下有多个接入号时靠它区分 |
| trade_no / out_trade_no | String | 平台单号 / 你的单号 |
| merchant_no | String | 商户号,验签后要比对自己的号 |
| pay_type | String | 支付方式名称 |
| subject | String | 商品名称 |
| amount | Int | 应收(分),要和本地订单金额比 |
| real_amount | Int | 买家实付(分) |
| channel_trade_no / bill_trade_no | String | 上游单号 / 账单号,对账用后者 |
| param / buyer | String | 下单传了才带 |
| paid_at | String | 到账时间 Y-m-d H:i:s |
验签与响应
raw = 请求原始字节 // 不要用解析后再序列化的 JSON
content = "v3." + X-Spay-Timestamp + "\n" + X-Spay-Nonce + "\n" + sha256_hex(raw)
signOK = hmac_sha256_hex(merchantKey, content) == X-Spay-Sign
match = body.out_trade_no == 本地单号 and body.amount == 本地应收(分)
and body.merchant_no == 我的商户号 and body.event == 'trade.paid'
if signOK and match and not already_paid(body.out_trade_no): 发货 / 加余额
return 200 # 重复通知同样回 200
重试与 V1/V2 同一套:最多 8 次,按 15 秒、30 秒、2 分钟、5 分钟、15 分钟、30 分钟、1 小时退避。 用完仍未拿到 2xx 时任务转终态不再发送——钱不会因此退回,订单仍是已收款,靠查单接口兜底或请后台手工补发。
V3 错误码
失败时 code 是下面这五个数字之一,error_code 是它的机读名;HTTP 状态码与 code 同源,
所以网关和 CDN 后面的排障也能对上。message 是给人看的中文,可能措辞微调,不要判它。
| code | error_code | HTTP | 含义 | 该怎么办 |
|---|---|---|---|---|
| 40001 | INVALID_PARAM | 400 | 必填字段缺失、类型不对、订单号含非法字符、JSON 解析失败 | 改代码,重试没用 |
| 40002 | INVALID_AMOUNT | 400 | 金额不是正整数、低于起收或高于单笔上限 | 核对金额单位是分 |
| 40101 | MCH_NOT_FOUND | 401 | X-Spay-App-Id 不存在,或它对应的商户号有问题 | 核对商户中心「API 信息」页里的接入号 |
| 40102 | SIGN_INVALID | 401 | 签名与请求体不匹配(含密钥错、请求体重排) | 按「V3 签名」逐步复算 |
| 40103 | REQUEST_EXPIRED | 401 | 时间戳超出 300 秒窗口,或 nonce 已用过 | 同步服务器时钟;每次请求换新 nonce |
| 40301 | ACCESS_DENIED | 403 | 接入号已停用、该接入号没有对应能力(scopes)、来源 IP 不在白名单、商户停用收款、支付方式未对你开放、站点级开关关闭 | 看 message 是哪一道闸 |
| 40401 | ORDER_NOT_FOUND | 404 | 订单不存在,或不是你的单(两种回同一句话) | 核对单号 |
| 40901 | IDEMPOTENCY_CONFLICT | 409 | 同一 Idempotency-Key 带了不同请求体,或首次请求仍在处理 | 换 key 或稍后重试 |
| 42201 | BUSINESS_REJECTED | 422 | 业务规则拒绝:状态不支持、超过可退、余额不足、上游不支持关单 | 按 message 那句中文处理 |
| 42901 | RATE_LIMITED | 429 | 同一来源请求过于频繁 | 指数退避,别固定间隔猛重试 |
| 50001 | INTERNAL_ERROR | 500 | 平台侧异常 | 带上 request_id 来找我们 |
支付相关接口
/submit.php页面跳转支付
由买家浏览器提交表单,验签通过后把人送到上游或平台收银台。也可以拼成 URL 直接跳转,但走 GET 时整套参数都得在查询串里: 系统按老 PHP 的口径取参数,pid 出现在查询串就整份按查询串验签,这时 POST 体里的字段一律不参与签名,混着传必然对不上。 页面单可以不传 type——留空时买家在收银台自己选支付方式。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 商户ID | pid | 是 | Int | 1001 | 商户号 |
| 支付方式 | type | 否 | String | alipay | 见支付方式列表;留空进收银台选 |
| 商户订单号 | out_trade_no | 是 | String | 202610250001 | 同一商户下唯一 |
| 异步通知地址 | notify_url | 是 | String | https://your.site/pay/notify | 服务器异步通知地址 |
| 跳转通知地址 | return_url | 是 | String | https://your.site/order/1 | 买家付款后跳回 |
| 商品名称 | name | 是 | String | VIP会员 | 超过 127 字节自动截取 |
| 商品金额 | money | 是 | String | 1.00 | 单位:元,最多 2 位小数 |
| 业务扩展参数 | param | 否 | String | uid=88 | 原样回传在通知与查单里 |
| 站点名称 | sitename | 否 | String | 我的小站 | 显示在收银台标题 |
| 买家IP | clientip | 否 | String | 1.2.3.4 | 不传取当前连接来源 IP |
| 签名类型 | sign_type | 否 | String | MD5 | MD5(默认)或 RSA |
| 签名 | sign | 是 | String | — | 按签名规则算出 |
返回说明
成功返回一段自动跳转页,把买家送到收款页,没有 JSON。失败返回一张错误提示页,正文是那句中文原因; 缺参数的判定顺序与老接口一致(先「订单号(out_trade_no)不能为空」,再 notify_url、return_url、name、money)。 完全没带 pid 时回纯文本「你还未配置支付接口商户!」——老收银台插件靠这句话判断自己没配好。
请求示例
<form method="post" action="https://spay.dlam.cc/submit.php" id="pay">
<input type="hidden" name="pid" value="1001">
<input type="hidden" name="out_trade_no" value="202610250001">
<input type="hidden" name="type" value="alipay">
<input type="hidden" name="name" value="VIP会员">
<input type="hidden" name="money" value="12.34">
<input type="hidden" name="notify_url" value="https://your.site/pay/notify">
<input type="hidden" name="return_url" value="https://your.site/order/1">
<input type="hidden" name="sign_type" value="MD5">
<input type="hidden" name="sign" value="算出来的签名">
</form>
<script>document.getElementById('pay').submit()</script>
V2 等价入口:POST /api.php?s=pay/submit(或 /api/pay/submit),参数相同另加 timestamp 与 sign;
区别是失败回 {code:-1,msg} 的 JSON 而不是提示页——调用方是服务器时更好处理。
/mapi.php统一下单接口
服务器对服务器下单,返回 JSON,由你自己的页面渲染二维码或跳转。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 商户ID | pid | 是 | Int | 1001 | 商户号 |
| 支付方式 | type | 是 | String | alipay | 接口单必须指定 |
| 商户订单号 | out_trade_no | 是 | String | 202610250001 | 同一商户下唯一 |
| 异步通知地址 | notify_url | 是 | String | https://your.site/pay/notify | 服务器异步通知地址 |
| 商品名称 | name | 是 | String | VIP会员 | 超过 127 字节自动截取 |
| 商品金额 | money | 是 | String | 1.00 | 单位:元,最多 2 位小数 |
| 买家IP | clientip | 是 | String | 1.2.3.4 | 必须是合法 IP;同 IP 限速与黑名单全靠它 |
| 业务扩展参数 | param | 否 | String | uid=88 | 原样回传 |
| 站点名称 | sitename | 否 | String | 我的小站 | 收银台标题 |
| 买家账号 | sub_openid | 否 | String | oUpDtx… | 公众号/小程序 openid,存订单并在 RSA 通知回传 |
| 实名号 | cert_no | 否 | String | — | 格式非法直接拒单 |
| 最低年龄 | min_age | 否 | Int | 18 | 格式非法直接拒单 |
| 签名类型 | sign_type | 否 | String | MD5 | MD5(默认)或 RSA |
| 签名 | sign | 是 | String | — | 按签名规则算出 |
返回参数
| 变量名 | 类型 | 描述 |
|---|---|---|
| code | Int | 1 成功;-1 失败(含验签失败);-4 一个参数都没传 |
| trade_no | String | 平台订单号 |
| payurl | String | 上游跳转地址,或兜底的平台收银台地址,直接跳转 |
| qrcode | String | 直连通道只给二维码内容时回这个,需自己渲染二维码(此时无 payurl) |
| html | String | 上游只给一段自提交表单时回这个,原样输出即可 |
| msg | String | 失败原因,中文原句 |
请求示例
curl -d "pid=1001&out_trade_no=202610250001&type=alipay&name=VIP会员&money=12.34" \
-d "notify_url=https://your.site/pay/notify&clientip=1.2.3.4" \
-d "sign_type=MD5&sign=算出来的签名" \
https://spay.dlam.cc/mapi.php
返回示例
{"code":1,"trade_no":"202610250751412050917c98acfe03","payurl":"https://…"}
/api.php?s=pay/create统一下单 V2
推荐的下单方式:真签名、带时间戳防重放、应答带平台签名。等价写法 POST /api/pay/create(不需要 nginx 重写)。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 商户ID | pid | 是 | Int | 1001 | 只从 POST 体取,query 里带不算 |
| 时间戳 | timestamp | 是 | Int | 1761370000 | 秒;与服务器相差 300 秒内有效 |
| 支付方式 | type | 是 | String | alipay | 见支付方式列表 |
| 商户订单号 | out_trade_no | 是 | String | 202610250001 | 同一商户下唯一 |
| 异步通知地址 | notify_url | 是 | String | https://your.site/pay/notify | 该单的通知改用 RSA 平台签名 |
| 商品名称 | name | 是 | String | VIP会员 | 超过 127 字节自动截取 |
| 商品金额 | money | 是 | String | 1.00 | 单位:元,最多 2 位小数 |
| 买家IP | clientip | 是 | String | 1.2.3.4 | 风控依据 |
| 业务扩展参数 | param | 否 | String | uid=88 | 原样回传 |
| 签名类型 | sign_type | 否 | String | RSA | MD5 或 RSA;keytype=1 的商户只收 RSA |
| 签名 | sign | 是 | String | — | timestamp 参与拼串 |
返回参数
| 变量名 | 类型 | 描述 |
|---|---|---|
| code | Int | 0 成功 |
| trade_no | String | 平台订单号 |
| pay_type | String | jump(跳转地址)/ html(自提交表单)/ qrcode(二维码内容) |
| pay_info | String | 对应 pay_type 的内容;都没有时是平台收银台地址 |
| timestamp | String | 应答时间戳,参与应答验签 |
| sign_type | String | 固定 RSA |
| sign | String | 平台私钥签名,用平台公钥验 |
请求示例
params = {pid: 1001, timestamp: now(), sign_type: 'RSA',
out_trade_no: '202610250001', type: 'alipay',
name: 'VIP会员', money: '12.34', clientip: '1.2.3.4',
notify_url: 'https://your.site/pay/notify'}
params.sign = base64(sha256WithRSA(sign_content(params), merchantPrivateKey))
// POST 表单到 https://spay.dlam.cc/api.php?s=pay/create
返回示例
{"code":0,"trade_no":"202610250751412050917c98acfe03","pay_type":"jump",
"pay_info":"https://…","timestamp":"1761370001","sign_type":"RSA","sign":"…"}
/api.php?s=pay/query订单查询
查单笔订单。V1 等价入口:GET /api.php?act=order(pid + 明文 key,字段平铺在顶层、无 data 层)。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 平台订单号 | trade_no | 二选一 | String | 20261025075141… | 与 out_trade_no 任填其一 |
| 商户订单号 | out_trade_no | 二选一 | String | 202610250001 | 只查得到自己的单 |
返回参数
| 变量名 | 类型 | 描述 |
|---|---|---|
| code | Int | 0 成功(V1 为 1 且 msg=succ) |
| trade_no / out_trade_no | String | 平台单号 / 你的单号 |
| api_trade_no | String | 上游交易号,空则不出现 |
| bill_trade_no | String | 账单交易号,对账用 |
| type | String | 支付方式名称 |
| pid | String | 商户号 |
| addtime / endtime | String | 下单时间 / 支付时间;未支付时 endtime 为空(V1 为 null) |
| name / money | String | 商品名称 / 应付金额(元) |
| param / buyer / clientip | String | 透传参数 / 买家账号 / 买家 IP,空则不出现 |
| status | Int | 0 未支付 / 1 已支付 / 2 已退款 |
| refundmoney | String | 已退金额(元) |
请求示例
curl -d "pid=1001×tamp=1761370000&out_trade_no=202610250001&sign_type=MD5&sign=…" \
https://spay.dlam.cc/api.php?s=pay/query
# V1:curl "https://spay.dlam.cc/api.php?act=order&pid=1001&key=商户密钥&out_trade_no=202610250001"
你的 notify_url支付结果通知
到账后由平台主动通知你的服务器。先记三句话:通知是 GET,参数拼在 notify_url 的查询串上(地址已带 ? 就用 & 续);
响应正文含 success(不区分大小写)才算送达;重复通知是常态,必须按订单号幂等。
通知参数
| 变量名 | 类型 | 描述 |
|---|---|---|
| pid | String | 商户号,验签后要比对自己的号 |
| trade_no | String | 平台订单号 |
| out_trade_no | String | 你的订单号 |
| type | String | 支付方式名称 |
| name | String | 商品名称 |
| money | String | 应付金额(元),要和本地订单比对 |
| trade_status | String | 固定 TRADE_SUCCESS |
| api_trade_no | String | 上游交易号,有则带 |
| param | String | 下单传了才带 |
| buyer | String | 仅 V2 下单、且有买家账号时带 |
| timestamp | String | 仅 V2 下单带,参与验签 |
| sign / sign_type | String | 签名与签名类型 |
签名口径(跟着下单入口那一代走)
| 下单入口 | 通知签名 | 额外字段 | 商户用什么验 |
|---|---|---|---|
submit.php / mapi.php | MD5 + 商户密钥 | 无 timestamp、无 buyer | 商户自己的密钥 |
V2 pay/create | RSA + 平台私钥 | 带 timestamp;有买家账号时带 buyer | 平台公钥 |
重试与验签示例
最多 8 次,按 15 秒、30 秒、2 分钟、5 分钟、15 分钟、30 分钟、1 小时退避。 用完仍未收到 success 时任务转终态不再发送——但钱不会因此退回:订单仍是已支付,靠查单接口兜底,或请后台在订单详情页手工补发。
p = 请求的查询参数 // 通知是 GET 查询串
signOK = p.sign_type == 'RSA'
? rsaVerifyWithPlatformKey(sign_content(p), p.sign)
: p.sign == md5(sign_content(p) + merchantKey)
match = p.out_trade_no == 本地单号 && p.money == 本地应付金额
&& p.trade_status == 'TRADE_SUCCESS' && p.pid == 我的商户号
if signOK && match && 本地单未入账 { 发货 / 加余额 }
response 'success' // 重复通知也一样回 success
/api.php?s=pay/refund订单退款
商户自助退款,从商户余额扣回。V1 等价入口 GET /api.php?act=refund,只有那里 code=0 算成功。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 平台订单号 | trade_no | 二选一 | String | 20261025075141… | 与 out_trade_no 任填其一 |
| 商户订单号 | out_trade_no | 二选一 | String | 202610250001 | — |
| 退款金额 | money | 是 | String | 12.34 | 不超过入账金额与剩余可退 |
| 商户退款单号 | out_refund_no | 否 | String | R20261025001 | 重复提交同一单号不会二次扣钱 |
返回参数
| 变量名 | 类型 | 描述 |
|---|---|---|
| code | Int | 0 成功;-1 业务失败(状态不对、超可退、余额不足) |
| refund_no | String | 平台退款单号 |
| out_refund_no | String | 你传的退款单号,传了才回 |
| trade_no / out_trade_no | String | 原订单号 |
| money | String | 本次退款金额(元) |
| reducemoney | String | 实际从余额扣回的金额(元) |
| msg | String | 「退款成功!退款金额¥12.34」;重复单号为「已存在相同退款单号!…」 |
返回示例
{"code":0,"refund_no":"202610250811223311","trade_no":"20261025075141…",
"money":"12.34","reducemoney":"12.10","msg":"退款成功!退款金额¥12.34", …}
/api.php?s=pay/refundquery订单退款查询
查一笔退款单。数据源是退款流水表,部分退款的每一笔都查得到。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 平台退款单号 | refund_no | 二选一 | String | 20261025081122… | 与 out_refund_no 任填其一 |
| 商户退款单号 | out_refund_no | 二选一 | String | R20261025001 | — |
返回参数
| 变量名 | 类型 | 描述 |
|---|---|---|
| code | Int | 0 成功;查不到回 -1「退款记录不存在」 |
| refund_no / out_refund_no | String | 平台退款单号 / 你的退款单号 |
| trade_no / out_trade_no | String | 原订单号 |
| money / reducemoney | String | 退款金额 / 实扣金额(元) |
| status | String | 退款单状态 |
| addtime / endtime | String | 发起时间 / 完成时间 |
/api.php?s=pay/close关闭订单
关闭一笔未支付订单。先让上游关,上游关掉了本地才落关闭——不会本地一关了之, 否则上游二维码还活着,买家随后付成功就成了没人认领的收款。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 平台订单号 | trade_no | 二选一 | String | 20261025075141… | 与 out_trade_no 任填其一 |
| 商户订单号 | out_trade_no | 二选一 | String | 202610250001 | — |
返回说明
成功 {"code":0,"msg":"订单关闭成功"}。业务失败回带签名的 {code:-1,msg},三句话各管一档:
「当前订单不存在!」/「当前支付插件不支持关闭订单」(聚合类上游没有关单接口)/「该订单状态不支持关闭!」。
商户相关接口
/api.php?s=merchant/info查询商户信息
商户资料、余额与单量统计。V1 等价入口 GET /api.php?act=query(会原样回显明文 key,老脚本拿它做配置自检)。
请求参数
无业务参数,带公共鉴权参数即可。
返回参数
| 变量名 | 类型 | 描述 |
|---|---|---|
| code | Int | 0 成功 |
| pid | String | 商户号 |
| status | Int | 账号状态:1 正常 / 0 停用 / 2 未审核 |
| pay_status | Int | 支付权限:0 停用 / 1 正常 / 2 待审核 |
| settle_status | Int | 1 已开启自动结算 |
| money | String | 可用余额(元) |
| settle_type / settle_account / settle_name | String | 提现方式 / 账号 / 姓名,空则不出现 |
| order_num | Int | 累计订单数 |
| order_num_today / order_num_lastday | Int | 今日 / 昨日已支付笔数 |
| order_money_today / order_money_lastday | String | 今日 / 昨日已支付金额(元,按应付口径) |
/api.php?s=merchant/orders查询订单列表
分页拉自己的订单。V1 等价入口 GET /api.php?act=orders(多回 count 与 total)。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 每页条数 | limit | 否 | Int | 10 | 默认 10,上限 50 |
| 偏移 | offset | 否 | Int | 0 | — |
| 状态筛选 | status | 否 | Int | 1 | 0 未支付 / 1 已支付 / 2 已退款;不传查全部 |
返回参数
code=0,data 为订单数组,元素字段与「订单查询」一致
(trade_no / out_trade_no / api_trade_no / type / pid / addtime / endtime / name / money / param / buyer / clientip / status / refundmoney)。
/api.php?act=settle结算记录与收款账户
结算记录 = 提现申请到打款完成的那本账(手动申请与每日自动结算都写在这里)。只支持 V1 入口。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 商户ID | pid | 是 | Int | 1001 | — |
| 商户密钥 | key | 是 | String | — | 明文,等同密码,只在服务端使用 |
| 每页条数 | limit | 否 | Int | 10 | 上限 50 |
| 偏移 | offset | 否 | Int | 0 | — |
返回参数(data 数组元素)
| 变量名 | 类型 | 描述 |
|---|---|---|
| id / uid / batch | Int / String | 记录号 / 商户号 / 结算批次号 |
| type / account / username | String | 结算方式 / 账号 / 姓名 |
| money / realmoney / fee | String | 申请金额 / 实到金额 / 手续费(元) |
| addtime / endtime | String | 申请时间 / 打款时间;未打款 endtime 为 null |
| status | Int | 0 未结算 / 1 已结算 |
修改收款账户 POST /api.php?s=merchant/settle
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 结算方式 | settle_type | 是 | Int | 1 | 1支付宝 2微信 3QQ 4银行卡 5USDT-TRC20 |
| 收款账号 | settle_account | 是 | String | 13800000000 | 支付宝须为手机号或邮箱;USDT 须 T 开头 34 位 |
| 收款人姓名 | settle_name | 是 | String | 张三 | — |
每次修改写一条审计日志。成功回 {code:0,msg:"提现信息已保存",settle_type,settle_account,settle_name}。
代付相关接口
/api.php?s=transfer/submit转账发起
用商户余额付给第三人。
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 代付方式 | type | 是 | String | alipay | alipay / wxpay / qqpay / bank / usdt |
| 收款账号 | account | 是 | String | 13800000000 | 银行卡为卡号、usdt 为 TRC20 地址 |
| 收款人姓名 | name | 否 | String | 张三 | — |
| 金额 | money | 是 | String | 100.00 | 单位:元 |
| 商户转账单号 | out_biz_no | 否 | String | T20261025001 | 字母数字与 . _ - | |
| 备注 | remark | 否 | String | 10月结算 | 最多 32 字 |
| 开户行 | bank_name | 否 | String | 招商银行 | type=bank 时填 |
| 收款码 | qr_image | 否 | String | https://… | type=wxpay 时填收款码图片地址 |
返回参数
| 变量名 | 类型 | 描述 |
|---|---|---|
| code | Int | 0 提交成功(不等于到账) |
| status | Int | 固定 3:待人工转账 |
| msg | String | 「提交成功!请等待管理员审核转账。」 |
| biz_no / out_biz_no | String | 平台转账单号 / 你的单号 |
| amount | String | 转账本金(元) |
| cost_money | String | 本金加手续费合计(元),提交即冻结 |
| usdt_amount / usdt_rate | String | 仅 type=usdt:上链数量与成交汇率 |
/api.php?s=transfer/query转账查询
请求参数
| 字段名 | 变量名 | 必填 | 类型 | 示例值 | 描述 |
|---|---|---|---|---|---|
| 平台转账单号 | biz_no | 二选一 | String | TF20261025… | 与 out_biz_no 任填其一 |
| 商户转账单号 | out_biz_no | 二选一 | String | T20261025001 | — |
返回参数
| 变量名 | 类型 | 描述 |
|---|---|---|
| code | Int | 0 查到;查不到回 -1「转账交易号不存在」 |
| biz_no / out_biz_no | String | 库里那一双单号(不是你传的那个) |
| status | Int | 1 成功 / 2 失败 / 3 处理中 |
| amount / cost_money | String | 本金 / 合计;失败单 cost_money 回本金(钱已整笔退回余额) |
| type / account / name | String | 方式 / 收款账号 / 收款人 |
| remark / paydate | String | 备注 / 打款时间,未打款 paydate 为空 |
| msg | String | 「转账成功!」/「转账失败:…」/「转账处理中,请等待管理员审核转账。」 |
| pay_order_no | String | 仅成功:打款流水号,核对凭证用 |
| errmsg | String | 仅失败:失败原因 |
| usdt_amount / usdt_rate | String | 仅 usdt 单:上链数量与成交时汇率快照 |
/api.php?s=transfer/proof转账凭证
请求参数同转账查询(biz_no)。当前出款为人工转账、没有上游电子回单,
接口直说:回 {code:-1,msg:"当前为人工转账,暂无电子回单,请凭打款流水号核对"}——不会回一个空链接让人以为系统坏了。
打款流水号用转账查询的 pay_order_no 拿。
/api.php?s=transfer/balance可用余额查询
返回参数
| 变量名 | 类型 | 描述 |
|---|---|---|
| code | Int | 0 成功 |
| available_money | String | 可代付余额(元);D+1 压着的钱不在内 |
| transfer_rate | String | 代付费率 |
聚合收款
/collect?code=…收款链接与二维码
商户在商户中心「聚合收款」页生成一条固定链接和一张收款码海报:任何人打开都能自己输入金额付款, 钱直接进该商户余额,订单在商户中心正常出现(标为聚合收款)。链接自带签名商户令牌,别贴到不想收款的地方。
- 收款方名称可在商户中心自定义,改完立即生效;留空显示商户名称。
- 海报在浏览器里现画:三族模板 × 每族四套版式 × 九套配色,手机长按图片可存、也能打印贴柜台。
- 买家能用的支付方式与限额,和该商户正常下单一致。
平台收银台地址是 https://spay.dlam.cc/pay/{trade_no}:页面单没指定支付方式时买家落在这里,
页面持续查单、付完不用手动刷新;它也是 payurl 的兜底值,可以直接发给买家。
附录
对接检查清单
- 服务器时间已同步(V2 偏差超 300 秒直接拒单)。
- 签名串剔除了 sign、sign_type 和空值,参数按 key 升序,值未编码。
- notify_url 公网可达且用 HTTPS,且不会因重复通知重复发货。
- 通知里校验签名、pid、金额与订单号,成功只回 success。
- 下单失败按 msg 那句中文提示买家,而不是笼统一句「支付失败」。
- 掉单兜底:定时查 pay/query(或 V1 act=order),别只等通知。
- 退款按「上限 = 入账金额」设计,全额退的差额自己补。
- 密钥只在服务端,前端页面与 App 里都不出现。
- (V3)确认金额单位是分:下单 1 元传 100,不是 "1.00"。
- (V3)写接口带 Idempotency-Key,超时后用同一个 key、同一个请求体重试;40901 先查单再决定重下。
- (V3)失败判 error_code 与 HTTP 状态码,不要判 message 的中文。
- (V3)排障记下 request_id,找平台时报这一个号。
接口疑问请带上 trade_no 来问,比发截图快。
示例代码
三种语言的签名与验签参考实现,复制即可跑。签名拼串规则见「签名规则」一节。
PHP(MD5 下单 + 验回调)
function sign_content(array $p): string {
unset($p['sign'], $p['sign_type']);
$p = array_filter($p, fn($v) => trim((string)$v) !== '');
ksort($p);
$s = '';
foreach ($p as $k => $v) $s .= $k . '=' . $v . '&';
return rtrim($s, '&');
}
$params = ['pid'=>1001,'type'=>'alipay','out_trade_no'=>'202610250001',
'notify_url'=>'https://your.site/pay/notify','return_url'=>'https://your.site/order/1',
'name'=>'VIP会员','money'=>'12.34','sign_type'=>'MD5'];
$params['sign'] = md5(sign_content($params) . $key);
// 回调验签(notify.php)
$ok = $_GET['sign'] === md5(sign_content($_GET) . $key)
&& $_GET['trade_status'] === 'TRADE_SUCCESS'
&& $_GET['pid'] == $pid && $_GET['money'] == $local_amount;
if ($ok && !already_paid($_GET['out_trade_no'])) { deliver($_GET['out_trade_no']); }
exit('success');
Go(V2 RSA 下单)
func signContent(p map[string]string) string {
keys := make([]string, 0, len(p))
for k, v := range p {
if k == "sign" || k == "sign_type" || strings.TrimSpace(v) == "" { continue }
keys = append(keys, k)
}
sort.Strings(keys)
var b strings.Builder
for i, k := range keys {
if i > 0 { b.WriteByte('&') }
b.WriteString(k + "=" + p[k])
}
return b.String()
}
h := sha256.Sum256([]byte(signContent(p)))
sig, _ := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, h[:])
p["sign"] = base64.StdEncoding.EncodeToString(sig)
// POST 表单到 API + "/api.php?s=pay/create"
PHP(V3 签名与下单)
$appId = 'app_6f1c2d3e4a5b6c7d'; // 接入号
$appSecret = 'spayv3_...'; // 它对应的 AppSecret
$base = 'https://pay.your-site.com';
$body = json_encode([
'out_trade_no' => 'P202610250001', 'amount' => 1234, // 分
'subject' => 'VIP会员', 'pay_type' => 'alipay',
'notify_url' => 'https://your.site/pay/notify',
], JSON_UNESCAPED_UNICODE); // 签完名就原样发出去,别再动它
$ts = (string) time();
$nonce = 'n-' . bin2hex(random_bytes(8));
$content = "v3.$ts\n$nonce\n" . hash('sha256', $body);
$sign = hash_hmac('sha256', $content, $appSecret);
$ch = curl_init($base . '/v3/pay/create');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ["Content-Type: application/json", "X-Spay-App-Id: $appId",
"X-Spay-Timestamp: $ts", "X-Spay-Nonce: $nonce", "X-Spay-Sign: $sign",
"Idempotency-Key: $uuid"],
CURLOPT_RETURNTRANSFER => true]);
$res = json_decode(curl_exec($ch), true);
if ($res['code'] !== 0) { /* 判 $res['error_code'],别判 $res['message'] */ }
JavaScript(验 V2 应答与 RSA 通知)
// 用平台公钥验应答/通知;timestamp 参与拼串
const ok = crypto.verify(
'SHA256', Buffer.from(signContent(body)),
crypto.createPublicKey(platformPubPem),
Buffer.from(body.sign, 'base64'));
// body 为应答或通知的原始字段(去掉 sign 本身不参与拼串,sign_type 同样剔除)