首页 / 开发文档

开发文档

对接地址 https://spay.dlam.cc。每个接口的地址、参数、返回字段与示例都逐条对着服务端代码写, 后台改配置(支付方式、重试次数、时间戳窗口)本页同步变化。

易支付 V1 老接口零改动 V2 签名接口 V3 JSON + 幂等 三代接口同一本账

接口说明

同一套能力有三个入口世代,新接入建议用 V3(JSON、金额以分为单位、幂等键、机读错误码); V2 是给已经按 V2 写过代码的人留的;V1 保留是为了让老代码不换一行就能迁过来。

世代入口鉴权成功码适用
V3POST /v3/…(JSON) 接入号 + 该接入号专属 AppSecret 的 HMAC-SHA256 签名 + 时间戳 + nonce0新接入、需要幂等与机读错误码
V1/submit.php、/mapi.php、/api.php?act=… pid + 明文商户密钥 key1(退款例外为 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 必须公网可直连,且能按订单号幂等处理(通知会重复)。
商户订单号 out_trade_no 只允许字母、数字和 . _ - |; 商品名称 name 超过 127 字节按 UTF-8 边界截断,不会劈开汉字。

签名规则

待签名串拼法(除 V1 的 api.php?act= 不签名外,所有签名接口与回调共用):

  1. 剔除 sign、sign_type,以及值为空或全空白的参数;
  2. 剩余参数按参数名 ASCII 升序,拼成 k1=v1&k2=v2,值不做 URL 编码;
  3. MD5:sign = md5(待签名串 + 商户密钥),32 位小写;
  4. 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
两个最常踩的坑:把空值参数留在签名串里(我们剔除,你多带一个空字段就对不上); 先 URL 编码再签名(编码是发请求那一步才做的事,拼串用原始值)。

支付方式列表

type 参数取下面这些名称之一(不是数字 id)。列表读后台实时配置,停用或新增会同步出现在这里。

type 取值显示名
alipay支付宝
wxpay微信支付
usdtUSDT(链上收款)

页面下单(submit.php / pay/submit)可以不传 type:留空时买家在平台收银台自己选。 接口下单必须传,且该方式要对你的商户号开通,否则下单被拒。

错误码与订单状态

对外订单状态统一三个数字,内部终态不透出:

status含义
0未支付(含已关闭、失败)
1已支付
2已退款
code出现位置含义
1V1 各 act、mapi 下单成功(唯 V1 的 act=refund 例外,它用 0)
0V2 全部、V1 的 act=refund成功
-1两代都有业务失败或订单不存在;接口单下单失败也折成 -1
-2V1 refund、V2 transfer/*该商户没开这个接口的权限
-3鉴权商户号 / 密钥 / 签名 / 时间戳问题
-4参数必填缺失,或站点级开关关闭
-5V2 路由URL 形状不对(URL Error!)或方法未开通(接口方法不存在)

V2 的失败分两种:参数与鉴权类回裸的 {code,msg}、不带签名; 业务本身拒绝(状态不对、超过可退)回 {code:-1,msg} 且照样带平台签名。 V3 不用这张表,它回五位错误码 + error_code 机读名,见「V3 错误码」。

V3 原生接口

POST/v3/pay/create · /v3/pay/query · /v3/pay/refund · /v3/pay/close · /v3/merchant/info · /v3/merchant/balance

V3 总览

V3 是这套网关自己的协议:请求体 JSON、应答 JSON、金额用分的整数、签名是 HMAC-SHA256, 再配幂等键和每请求一个 request_id。它不改动任何资金与订单口径——打的是同一套订单、同一本账, 所以 V1 下出去的单可以在 V3 查、在 V3 退。

V3 比 V1/V2 多的东西

能力V1 / V2V3
报文表单 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-8f3c1a2b9d8~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":{ … }}
变量名类型描述
codeInt0 成功;非 0 就是五位错误码,见「V3 错误码」
messageString成功为 success,失败是一句中文原因,可以原样给买家看
request_idString每个请求一个,同时回在响应头 X-Spay-Request-Id;找平台排障就报这个号
dataObject业务字段,只有成功时才出现
error_codeString机读错误名(如 ORDER_NOT_FOUND),只有失败时才出现;代码里判它,不要判中文
V3 的金额一律是分的整数:收 12.34 元就传 "amount": 1234。 这和 V1/V2 相反(那边是「元」的字符串),跨代迁移最容易出错的就是它—— 上线前建议下一笔最小金额,用查单接口核对一次金额与入账值。

V3 接入凭据

V3 不吃 V1/V2 那把商户密钥,用的是单独发放的接入凭据。一个商户号下可以开多个接入号, 每个接入号一把密钥、一份权限、一份 IP 白名单——哪个程序泄露了就停用哪个,其它业务照跑。

项目长什么样怎么用
接入号 AppIDapp_ 开头 16 位十六进制 放请求头 X-Spay-App-Id,明文传输,不是秘密
AppSecretspayv3_ 开头 40 位十六进制 只用来算 HMAC 签名,永不进请求;发放时只显示一次,忘了只能重置
能力范围 scopespay / 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(请求体原始字节)>
  1. 第一段固定前缀 v3.,timestamp 与 nonce 取请求头里的原值;
  2. 第三段是请求体原始字节的 SHA-256 十六进制小写:不是按字段排序、不是格式化后的 JSON。算完签名就不要再重新序列化一次;
  3. 签名 = HMAC-SHA256(待签名串, 该接入号的 AppSecret),输出 64 位十六进制小写,放 X-Spay-Sign;
  4. 异步通知用同一套算法,密钥是这笔下单时所用接入号的那把 AppSecret(通知体里的 app_id 会告诉你是哪个接入号), 只是签名方换成平台,请求侧和验签侧代码共用一份;
  5. 应答头里的 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
三个最常踩的:签名后又用框架重新序列化一次请求体(键序或空格一变,哈希就对不上); timestamp 传成毫秒;同一进程里复用了 nonce(判 40103)。
POST/v3/pay/create

统一下单 V3

服务端对服务端下单。带 pay_type 时直接拿到买家要用的收款信息;不带时落平台收银台,让买家自己选支付方式。

请求参数

字段名变量名必填类型示例值描述
商户订单号out_trade_no是StringP202610250001同一商户下唯一;字母数字与 . _ - |,最长 64
金额amount是Int1234单位:分,正整数;低于起收额或高于单笔上限直接拒
商品名称subject是StringVIP会员超过 127 字节按 UTF-8 边界截断,不劈开汉字
支付方式pay_type否Stringalipay见支付方式列表;留空进平台收银台让买家选
异步通知地址notify_url是Stringhttps://your.site/pay/notify该单的通知按 V3 格式发 JSON POST(见「V3 异步通知」)
跳转通知地址return_url否Stringhttps://your.site/order/1买家付完跳回你的页面
买家IPclient_ip否String1.2.3.4不传取当前连接来源 IP;同 IP 限速与风控全靠它
业务扩展参数param否Stringuid=88原样回传在通知与查单里
买家账号buyer否StringoUpDtx…公众号/小程序 openid

返回参数(data)

变量名类型描述
trade_noString平台订单号
out_trade_noString你的订单号,原样回显
amountInt本单应收(分)
statusString新建单为 pending(未指定支付方式)或 paying(已派通道,等买家付)
pay_modeStringqrcode 二维码内容 / html 自提交表单 / jump 跳转地址 / hosted 平台收银台
pay_infoString与 pay_mode 对应的那份内容,渲染方式见下表
cashier_urlString平台收银台地址,任何时候都有值;拿不准就把它发给买家
created_atString下单时间 Y-m-d H:i:s

pay_mode 怎么用

pay_modepay_info 是什么你该怎么做
qrcode二维码的原始内容(不是图片地址)自己把它画成二维码给买家扫
html一段自动提交表单原样输出到页面,不要包装、不要转义
jump上游收款页 URL302 或让买家点开
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"}}
同一笔业务只允许一个 out_trade_no:换一个单号重下就是另一笔应收, 买家两边都付了就得两边都退。超时重试请用同一个 Idempotency-Key(见下一节),别换 out_trade_no。

幂等键

下单、退款、关单三个写接口支持 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 也能调通,只是超时后你只能先查单确认,再决定要不要重下。
查单接口是幂等的,不需要 key。买家侧真正的钱只在上游收到,任何应答丢失都不会凭空多扣一笔—— 所以「怕重复」的正确做法是带 key 重试,而不是干脆不重试、把这一单忘掉。
POST/v3/pay/query

订单查询 V3

查单笔订单,只查得到自己商户的单。请求体两个单号任填其一,都填时按 trade_no。

请求参数

字段名变量名必填类型示例值描述
平台订单号trade_no二选一String20261025075141…—
商户订单号out_trade_no二选一StringP202610250001别人的单和没见过的单回同一句话

返回参数(data)

变量名类型描述
trade_no / out_trade_noString平台单号 / 你的单号
merchant_noString商户号
subjectString商品名称
pay_typeString支付方式名称,未选时为 cashier
amountInt应收(分)
real_amountInt买家实付(分);未支付为 0
get_amountInt商户应得(分);未支付为 0
refund_amountInt已退金额(分)
statusString见下表;收了钱的状态是 paid / notified / settled 三个,都算已收款
channel_trade_noString上游交易号,未派通道时为空
bill_trade_noString账单交易号,拿去对账用这个
buyer / paramString买家账号 / 下单透传参数
created_at / paid_atString下单时间 / 到账时间;未支付 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"}}
掉单兜底请定时轮询这个接口,别只等异步通知:通知发完 8 次仍未成功会转终态不再发, 钱却不会因此退回,只有查单能看到那一笔真的到了。
POST/v3/pay/refund

退款 V3

商户自助退款,从商户余额扣回,需要商户开通退款权限。支持部分退款,每一笔都是独立退款单。

请求参数

字段名变量名必填类型示例值描述
平台订单号trade_no二选一String20261025075141…与 out_trade_no 任填其一
商户订单号out_trade_no二选一StringP202610250001—
退款金额amount是Int1234单位:分;不超过入账金额与剩余可退
商户退款单号out_refund_no是StringR202610250016~64 位;同一单号重复提交不会二次扣钱

请求头还必须带 Idempotency-Key:退款是唯一「不带就拒」的写接口,缺了回 40001,因为超时重试时它是第二个凭据——out_refund_no 由你自己生成, Idempotency-Key 证明「这条报文就是我刚才那条」。两个都带才是稳的。

返回参数(data)

变量名类型描述
refund_noString平台退款单号,退款查询用它
out_refund_noString你传的退款单号,原样回给你
trade_no / out_trade_noString原订单号
amountInt本次退款金额(分)
reduced_amountInt实际从余额扣回的金额(分)
refunded_amountInt这一单累计已退(分)
statusString全额退完为 refunded,部分退款仍为 paid
deduplicatedBooltrue 表示这个 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}}
重试用尽后换了新的 out_refund_no 再来一次,就是第二笔退款: 同一笔退款重发时单号不许换,deduplicated:true 是告诉你「这笔早退过了,没重复扣」而不是让你再退。
退款上限是入账金额(real_amount)不是订单金额:加费或补贴单要给买家全额退时, 差额由你自己补。未开退款权限回 40301 / ACCESS_DENIED,余额不足回 42201 / BUSINESS_REJECTED, 状态或额度不对回 42201——判 error_code 之外,msg 那句中文和老接口是一致的。
POST/v3/pay/close

关闭订单 V3

关闭一笔未支付订单。先让上游关,上游关掉了本地才落 closed—— 不会本地一关了之,否则上游二维码还活着,买家随后付成功就成了没人认领的收款。

请求参数

字段名变量名必填类型示例值描述
平台订单号trade_no二选一String20261025075141…与 out_trade_no 任填其一
商户订单号out_trade_no二选一StringP202610250001—

成功回 {code:0,message:"success",request_id:"…",data:{trade_no,status:"closed"}}。 已支付或已退款的单不可关,回 42201 / BUSINESS_REJECTED; 聚合类上游没有关单接口,回 42201 且 message 说明「当前支付插件不支持关闭订单」。

未支付单不必你来关:平台每 20 秒扫一次超时单,超过分配时限(默认 180 秒)且重试用完的单会自动关闭, 原因是「超时未付,自动关闭」。关单和买家付款撞在一起时以钱为准——已到账的单关不掉。
POST/v3/merchant/info · /v3/merchant/balance

商户信息与余额 V3

两个接口都无业务参数,请求体传空对象 {}(仍要正常签名)。

data · /v3/merchant/info

变量名类型描述
merchant_no / nameString商户号 / 商户名称
statusStringnormal 正常 / reviewing 待审核 / disabled 停用
pay_on / refund_on / transfer_on / settle_onBool收款 / 退款 / 代付 / 自动结算 四项权限
min_amount / max_amountInt单笔起收与上限(分),0 表示不限
settle_cycleString结算周期,如 D+1
created_atString开户时间

data · /v3/merchant/balance

变量名类型描述
availableInt可提现余额(分)
pending_settlementInt待结算(分):账期压着的钱,提不了现,到期自动放进 available
frozenInt代付已提交、等管理员打款而冻结的钱(分)
currencyString固定 CNY(分)

余额口径与商户中心「资金账户」页完全一致:三个数是三本账,加起来不等于「今天收了多少」, 因为结算周期会把今天的收入压到明天才放出 available。

POST你的 notify_url

V3 异步通知

V3 下的单发 V3 通知:JSON POST 到你的 notify_url,签名在请求头上,正文里没有 sign 字段。 V1/V2 下的单仍按各自那一代的格式发(见「支付结果通知」)。判定送达的方式变了:HTTP 状态码 2xx 就算收到, 不需要再输出 success 这个字。

通知请求头

请求头示例值描述
X-Spay-Timestamp1761370000发送时间(秒),参与签名
X-Spay-Noncen-1d0a53e17f6b9c42每次发送都换一个新值,可用来识别重复通知
X-Spay-Sign64 位十六进制HMAC-SHA256,待签名串算法与请求侧一模一样,用你的商户密钥验
Content-Typeapplication/json正文是 JSON

通知正文

变量名类型描述
eventString固定 trade.paid
notify_idString固定为 np_ + 平台订单号,同一笔单每次重发都不变;去重请用它或 trade_no,别用 nonce
app_idString这笔单下单时用的接入号;验签就用它对应的 AppSecret,一个商户号下有多个接入号时靠它区分
trade_no / out_trade_noString平台单号 / 你的单号
merchant_noString商户号,验签后要比对自己的号
pay_typeString支付方式名称
subjectString商品名称
amountInt应收(分),要和本地订单金额比
real_amountInt买家实付(分)
channel_trade_no / bill_trade_noString上游单号 / 账单号,对账用后者
param / buyerString下单传了才带
paid_atString到账时间 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 时任务转终态不再发送——钱不会因此退回,订单仍是已收款,靠查单接口兜底或请后台手工补发。

响应要快:超时的通知会被判失败并继续重试,买家看到的是一次次重复推送。 业务处理放队列,接口先回 200。

V3 错误码

失败时 code 是下面这五个数字之一,error_code 是它的机读名;HTTP 状态码与 code 同源, 所以网关和 CDN 后面的排障也能对上。message 是给人看的中文,可能措辞微调,不要判它。

codeerror_codeHTTP含义该怎么办
40001INVALID_PARAM400必填字段缺失、类型不对、订单号含非法字符、JSON 解析失败改代码,重试没用
40002INVALID_AMOUNT400金额不是正整数、低于起收或高于单笔上限核对金额单位是分
40101MCH_NOT_FOUND401X-Spay-App-Id 不存在,或它对应的商户号有问题核对商户中心「API 信息」页里的接入号
40102SIGN_INVALID401签名与请求体不匹配(含密钥错、请求体重排)按「V3 签名」逐步复算
40103REQUEST_EXPIRED401时间戳超出 300 秒窗口,或 nonce 已用过同步服务器时钟;每次请求换新 nonce
40301ACCESS_DENIED403接入号已停用、该接入号没有对应能力(scopes)、来源 IP 不在白名单、商户停用收款、支付方式未对你开放、站点级开关关闭看 message 是哪一道闸
40401ORDER_NOT_FOUND404订单不存在,或不是你的单(两种回同一句话)核对单号
40901IDEMPOTENCY_CONFLICT409同一 Idempotency-Key 带了不同请求体,或首次请求仍在处理换 key 或稍后重试
42201BUSINESS_REJECTED422业务规则拒绝:状态不支持、超过可退、余额不足、上游不支持关单按 message 那句中文处理
42901RATE_LIMITED429同一来源请求过于频繁指数退避,别固定间隔猛重试
50001INTERNAL_ERROR500平台侧异常带上 request_id 来找我们
重试策略只有一条要记:42901 和 50001 可以带同一个 Idempotency-Key 重试,其余错误码重试都不会变。 40901 说明服务端可能已经执行过了,务必先查单确认,再决定重下。

支付相关接口

POST/submit.php

页面跳转支付

由买家浏览器提交表单,验签通过后把人送到上游或平台收银台。也可以拼成 URL 直接跳转,但走 GET 时整套参数都得在查询串里: 系统按老 PHP 的口径取参数,pid 出现在查询串就整份按查询串验签,这时 POST 体里的字段一律不参与签名,混着传必然对不上。 页面单可以不传 type——留空时买家在收银台自己选支付方式。

请求参数

字段名变量名必填类型示例值描述
商户IDpid是Int1001商户号
支付方式type否Stringalipay见支付方式列表;留空进收银台选
商户订单号out_trade_no是String202610250001同一商户下唯一
异步通知地址notify_url是Stringhttps://your.site/pay/notify服务器异步通知地址
跳转通知地址return_url是Stringhttps://your.site/order/1买家付款后跳回
商品名称name是StringVIP会员超过 127 字节自动截取
商品金额money是String1.00单位:元,最多 2 位小数
业务扩展参数param否Stringuid=88原样回传在通知与查单里
站点名称sitename否String我的小站显示在收银台标题
买家IPclientip否String1.2.3.4不传取当前连接来源 IP
签名类型sign_type否StringMD5MD5(默认)或 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 而不是提示页——调用方是服务器时更好处理。

POST/mapi.php

统一下单接口

服务器对服务器下单,返回 JSON,由你自己的页面渲染二维码或跳转。

请求参数

字段名变量名必填类型示例值描述
商户IDpid是Int1001商户号
支付方式type是Stringalipay接口单必须指定
商户订单号out_trade_no是String202610250001同一商户下唯一
异步通知地址notify_url是Stringhttps://your.site/pay/notify服务器异步通知地址
商品名称name是StringVIP会员超过 127 字节自动截取
商品金额money是String1.00单位:元,最多 2 位小数
买家IPclientip是String1.2.3.4必须是合法 IP;同 IP 限速与黑名单全靠它
业务扩展参数param否Stringuid=88原样回传
站点名称sitename否String我的小站收银台标题
买家账号sub_openid否StringoUpDtx…公众号/小程序 openid,存订单并在 RSA 通知回传
实名号cert_no否String—格式非法直接拒单
最低年龄min_age否Int18格式非法直接拒单
签名类型sign_type否StringMD5MD5(默认)或 RSA
签名sign是String—按签名规则算出

返回参数

变量名类型描述
codeInt1 成功;-1 失败(含验签失败);-4 一个参数都没传
trade_noString平台订单号
payurlString上游跳转地址,或兜底的平台收银台地址,直接跳转
qrcodeString直连通道只给二维码内容时回这个,需自己渲染二维码(此时无 payurl)
htmlString上游只给一段自提交表单时回这个,原样输出即可
msgString失败原因,中文原句

请求示例

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://…"}
接口单的下单失败一律折成 -1,验签失败也是 -1:想区分「签名错」和「金额超限」要看 msg 那句中文。
POST/api.php?s=pay/create

统一下单 V2

推荐的下单方式:真签名、带时间戳防重放、应答带平台签名。等价写法 POST /api/pay/create(不需要 nginx 重写)。

请求参数

字段名变量名必填类型示例值描述
商户IDpid是Int1001只从 POST 体取,query 里带不算
时间戳timestamp是Int1761370000秒;与服务器相差 300 秒内有效
支付方式type是Stringalipay见支付方式列表
商户订单号out_trade_no是String202610250001同一商户下唯一
异步通知地址notify_url是Stringhttps://your.site/pay/notify该单的通知改用 RSA 平台签名
商品名称name是StringVIP会员超过 127 字节自动截取
商品金额money是String1.00单位:元,最多 2 位小数
买家IPclientip是String1.2.3.4风控依据
业务扩展参数param否Stringuid=88原样回传
签名类型sign_type否StringRSAMD5 或 RSA;keytype=1 的商户只收 RSA
签名sign是String—timestamp 参与拼串

返回参数

变量名类型描述
codeInt0 成功
trade_noString平台订单号
pay_typeStringjump(跳转地址)/ html(自提交表单)/ qrcode(二维码内容)
pay_infoString对应 pay_type 的内容;都没有时是平台收银台地址
timestampString应答时间戳,参与应答验签
sign_typeString固定 RSA
signString平台私钥签名,用平台公钥验

请求示例

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":"…"}
POST/api.php?s=pay/query

订单查询

查单笔订单。V1 等价入口:GET /api.php?act=order(pid + 明文 key,字段平铺在顶层、无 data 层)。

请求参数

字段名变量名必填类型示例值描述
平台订单号trade_no二选一String20261025075141…与 out_trade_no 任填其一
商户订单号out_trade_no二选一String202610250001只查得到自己的单

返回参数

变量名类型描述
codeInt0 成功(V1 为 1 且 msg=succ)
trade_no / out_trade_noString平台单号 / 你的单号
api_trade_noString上游交易号,空则不出现
bill_trade_noString账单交易号,对账用
typeString支付方式名称
pidString商户号
addtime / endtimeString下单时间 / 支付时间;未支付时 endtime 为空(V1 为 null)
name / moneyString商品名称 / 应付金额(元)
param / buyer / clientipString透传参数 / 买家账号 / 买家 IP,空则不出现
statusInt0 未支付 / 1 已支付 / 2 已退款
refundmoneyString已退金额(元)

请求示例

curl -d "pid=1001&timestamp=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"
掉单兜底请定时轮询这个接口,别只等异步通知。
GET你的 notify_url

支付结果通知

到账后由平台主动通知你的服务器。先记三句话:通知是 GET,参数拼在 notify_url 的查询串上(地址已带 ? 就用 & 续); 响应正文含 success(不区分大小写)才算送达;重复通知是常态,必须按订单号幂等。

通知参数

变量名类型描述
pidString商户号,验签后要比对自己的号
trade_noString平台订单号
out_trade_noString你的订单号
typeString支付方式名称
nameString商品名称
moneyString应付金额(元),要和本地订单比对
trade_statusString固定 TRADE_SUCCESS
api_trade_noString上游交易号,有则带
paramString下单传了才带
buyerString仅 V2 下单、且有买家账号时带
timestampString仅 V2 下单带,参与验签
sign / sign_typeString签名与签名类型

签名口径(跟着下单入口那一代走)

下单入口通知签名额外字段商户用什么验
submit.php / mapi.phpMD5 + 商户密钥无 timestamp、无 buyer商户自己的密钥
V2 pay/createRSA + 平台私钥带 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
不要把业务写在验签之前;响应要快,超时会被判失败并继续重试。
POST/api.php?s=pay/refund

订单退款

商户自助退款,从商户余额扣回。V1 等价入口 GET /api.php?act=refund,只有那里 code=0 算成功。

请求参数

字段名变量名必填类型示例值描述
平台订单号trade_no二选一String20261025075141…与 out_trade_no 任填其一
商户订单号out_trade_no二选一String202610250001—
退款金额money是String12.34不超过入账金额与剩余可退
商户退款单号out_refund_no否StringR20261025001重复提交同一单号不会二次扣钱

返回参数

变量名类型描述
codeInt0 成功;-1 业务失败(状态不对、超可退、余额不足)
refund_noString平台退款单号
out_refund_noString你传的退款单号,传了才回
trade_no / out_trade_noString原订单号
moneyString本次退款金额(元)
reducemoneyString实际从余额扣回的金额(元)
msgString「退款成功!退款金额¥12.34」;重复单号为「已存在相同退款单号!…」

返回示例

{"code":0,"refund_no":"202610250811223311","trade_no":"20261025075141…",
 "money":"12.34","reducemoney":"12.10","msg":"退款成功!退款金额¥12.34", …}
退款上限是入账金额不是订单金额:要给买家全额退的差额由你自己补。 商户未开退款权限回 -2「商户未开启订单退款API接口」;站点级关闭回「管理员未开启商户后台自助退款」。
POST/api.php?s=pay/refundquery

订单退款查询

查一笔退款单。数据源是退款流水表,部分退款的每一笔都查得到。

请求参数

字段名变量名必填类型示例值描述
平台退款单号refund_no二选一String20261025081122…与 out_refund_no 任填其一
商户退款单号out_refund_no二选一StringR20261025001—

返回参数

变量名类型描述
codeInt0 成功;查不到回 -1「退款记录不存在」
refund_no / out_refund_noString平台退款单号 / 你的退款单号
trade_no / out_trade_noString原订单号
money / reducemoneyString退款金额 / 实扣金额(元)
statusString退款单状态
addtime / endtimeString发起时间 / 完成时间
POST/api.php?s=pay/close

关闭订单

关闭一笔未支付订单。先让上游关,上游关掉了本地才落关闭——不会本地一关了之, 否则上游二维码还活着,买家随后付成功就成了没人认领的收款。

请求参数

字段名变量名必填类型示例值描述
平台订单号trade_no二选一String20261025075141…与 out_trade_no 任填其一
商户订单号out_trade_no二选一String202610250001—

返回说明

成功 {"code":0,"msg":"订单关闭成功"}。业务失败回带签名的 {code:-1,msg},三句话各管一档: 「当前订单不存在!」/「当前支付插件不支持关闭订单」(聚合类上游没有关单接口)/「该订单状态不支持关闭!」。

商户相关接口

POST/api.php?s=merchant/info

查询商户信息

商户资料、余额与单量统计。V1 等价入口 GET /api.php?act=query(会原样回显明文 key,老脚本拿它做配置自检)。

请求参数

无业务参数,带公共鉴权参数即可。

返回参数

变量名类型描述
codeInt0 成功
pidString商户号
statusInt账号状态:1 正常 / 0 停用 / 2 未审核
pay_statusInt支付权限:0 停用 / 1 正常 / 2 待审核
settle_statusInt1 已开启自动结算
moneyString可用余额(元)
settle_type / settle_account / settle_nameString提现方式 / 账号 / 姓名,空则不出现
order_numInt累计订单数
order_num_today / order_num_lastdayInt今日 / 昨日已支付笔数
order_money_today / order_money_lastdayString今日 / 昨日已支付金额(元,按应付口径)
POST/api.php?s=merchant/orders

查询订单列表

分页拉自己的订单。V1 等价入口 GET /api.php?act=orders(多回 count 与 total)。

请求参数

字段名变量名必填类型示例值描述
每页条数limit否Int10默认 10,上限 50
偏移offset否Int0—
状态筛选status否Int10 未支付 / 1 已支付 / 2 已退款;不传查全部
老接口有个必须照搬的怪处:筛不筛状态看请求里有没有 status,取值却读 query 上的 status。 只 POST 一个 status 会被当成 0(未支付列表)。要筛选请把 status 同时放在 query 上。

返回参数

code=0,data 为订单数组,元素字段与「订单查询」一致 (trade_no / out_trade_no / api_trade_no / type / pid / addtime / endtime / name / money / param / buyer / clientip / status / refundmoney)。

GET/api.php?act=settle

结算记录与收款账户

结算记录 = 提现申请到打款完成的那本账(手动申请与每日自动结算都写在这里)。只支持 V1 入口。

请求参数

字段名变量名必填类型示例值描述
商户IDpid是Int1001—
商户密钥key是String—明文,等同密码,只在服务端使用
每页条数limit否Int10上限 50
偏移offset否Int0—

返回参数(data 数组元素)

变量名类型描述
id / uid / batchInt / String记录号 / 商户号 / 结算批次号
type / account / usernameString结算方式 / 账号 / 姓名
money / realmoney / feeString申请金额 / 实到金额 / 手续费(元)
addtime / endtimeString申请时间 / 打款时间;未打款 endtime 为 null
statusInt0 未结算 / 1 已结算

修改收款账户 POST /api.php?s=merchant/settle

字段名变量名必填类型示例值描述
结算方式settle_type是Int11支付宝 2微信 3QQ 4银行卡 5USDT-TRC20
收款账号settle_account是String13800000000支付宝须为手机号或邮箱;USDT 须 T 开头 34 位
收款人姓名settle_name是String张三—

每次修改写一条审计日志。成功回 {code:0,msg:"提现信息已保存",settle_type,settle_account,settle_name}。

代付相关接口

代付是往外付钱:站点总开关与商户自己的代付权限两道都开着才放行,任何一道关着都按原文案拒绝。 提交即冻结本金加手续费,回 status=3 待人工转账,管理员登记流水号后才算成功——不是实时到账。
POST/api.php?s=transfer/submit

转账发起

用商户余额付给第三人。

请求参数

字段名变量名必填类型示例值描述
代付方式type是Stringalipayalipay / wxpay / qqpay / bank / usdt
收款账号account是String13800000000银行卡为卡号、usdt 为 TRC20 地址
收款人姓名name否String张三—
金额money是String100.00单位:元
商户转账单号out_biz_no否StringT20261025001字母数字与 . _ - |
备注remark否String10月结算最多 32 字
开户行bank_name否String招商银行type=bank 时填
收款码qr_image否Stringhttps://…type=wxpay 时填收款码图片地址

返回参数

变量名类型描述
codeInt0 提交成功(不等于到账)
statusInt固定 3:待人工转账
msgString「提交成功!请等待管理员审核转账。」
biz_no / out_biz_noString平台转账单号 / 你的单号
amountString转账本金(元)
cost_moneyString本金加手续费合计(元),提交即冻结
usdt_amount / usdt_rateString仅 type=usdt:上链数量与成交汇率
POST/api.php?s=transfer/query

转账查询

请求参数

字段名变量名必填类型示例值描述
平台转账单号biz_no二选一StringTF20261025…与 out_biz_no 任填其一
商户转账单号out_biz_no二选一StringT20261025001—

返回参数

变量名类型描述
codeInt0 查到;查不到回 -1「转账交易号不存在」
biz_no / out_biz_noString库里那一双单号(不是你传的那个)
statusInt1 成功 / 2 失败 / 3 处理中
amount / cost_moneyString本金 / 合计;失败单 cost_money 回本金(钱已整笔退回余额)
type / account / nameString方式 / 收款账号 / 收款人
remark / paydateString备注 / 打款时间,未打款 paydate 为空
msgString「转账成功!」/「转账失败:…」/「转账处理中,请等待管理员审核转账。」
pay_order_noString仅成功:打款流水号,核对凭证用
errmsgString仅失败:失败原因
usdt_amount / usdt_rateString仅 usdt 单:上链数量与成交时汇率快照
POST/api.php?s=transfer/proof

转账凭证

请求参数同转账查询(biz_no)。当前出款为人工转账、没有上游电子回单, 接口直说:回 {code:-1,msg:"当前为人工转账,暂无电子回单,请凭打款流水号核对"}——不会回一个空链接让人以为系统坏了。 打款流水号用转账查询的 pay_order_no 拿。

POST/api.php?s=transfer/balance

可用余额查询

返回参数

变量名类型描述
codeInt0 成功
available_moneyString可代付余额(元);D+1 压着的钱不在内
transfer_rateString代付费率

聚合收款

GET/collect?code=…

收款链接与二维码

商户在商户中心「聚合收款」页生成一条固定链接和一张收款码海报:任何人打开都能自己输入金额付款, 钱直接进该商户余额,订单在商户中心正常出现(标为聚合收款)。链接自带签名商户令牌,别贴到不想收款的地方。

  • 收款方名称可在商户中心自定义,改完立即生效;留空显示商户名称。
  • 海报在浏览器里现画:三族模板 × 每族四套版式 × 九套配色,手机长按图片可存、也能打印贴柜台。
  • 买家能用的支付方式与限额,和该商户正常下单一致。

平台收银台地址是 https://spay.dlam.cc/pay/{trade_no}:页面单没指定支付方式时买家落在这里, 页面持续查单、付完不用手动刷新;它也是 payurl 的兜底值,可以直接发给买家。

附录

对接检查清单

  1. 服务器时间已同步(V2 偏差超 300 秒直接拒单)。
  2. 签名串剔除了 sign、sign_type 和空值,参数按 key 升序,值未编码。
  3. notify_url 公网可达且用 HTTPS,且不会因重复通知重复发货。
  4. 通知里校验签名、pid、金额与订单号,成功只回 success。
  5. 下单失败按 msg 那句中文提示买家,而不是笼统一句「支付失败」。
  6. 掉单兜底:定时查 pay/query(或 V1 act=order),别只等通知。
  7. 退款按「上限 = 入账金额」设计,全额退的差额自己补。
  8. 密钥只在服务端,前端页面与 App 里都不出现。
  9. (V3)确认金额单位是分:下单 1 元传 100,不是 "1.00"。
  10. (V3)写接口带 Idempotency-Key,超时后用同一个 key、同一个请求体重试;40901 先查单再决定重下。
  11. (V3)失败判 error_code 与 HTTP 状态码,不要判 message 的中文。
  12. (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 同样剔除)
SPay 聚合支付 智能助手
回答依据官网开发文档、服务条款与平台当前配置;费率与到账时间以客服报价和实际结算为准。