商户接口文档

商户接口文档

本文面向第一次对接的商户程序员。文中的 https://api.example.com 是示例地址,所有接口路径都以 /v1 开头。实际的接口网关、商户后台地址、AppID 和平台的通知来源 IP,请登录商户后台在「对接资料」页面查看,或者看上级发给你的对接信息。

1. 对接步骤

  1. 拿到 AppID 和密钥。由平台或你的上级在商户后台为你生成一组 API 凭证:app_id(形如 app_xxxxxxxxxxxxxxxx)和密钥(64 位十六进制字符串)。密钥明文只在生成时显示一次,之后只能看到指纹,请立即保存到服务端的配置或密钥管理系统中,不要写进前端代码、App 安装包或代码仓库。
  2. 在商户后台设置 IP 白名单。登录商户后台,把你服务器调用接口时使用的出口公网 IP 填进这把密钥的白名单。设置白名单后,其他 IP 即使拿到密钥也无法调用(返回 403 IP_NOT_ALLOWED)。代付接口强制要求设置白名单,没设置时返回 403 IP_WHITELIST_REQUIRED。
  3. 下单。按 签名 规则给请求签名,调用 下单接口。把返回的 cashier_url(收银页链接)或 pay_payload 交给用户去支付。用 在线签名调试器 或 SDK 可以快速验证签名是否正确。
  4. 处理通知。订单状态变化后,平台会向你下单时填的 notify_url 发送签名的 HTTP POST 通知。你的服务要验签、按 event_id 去重、更新自己的订单,然后返回 HTTP 2xx。详见 事件通知。如果平台出口 IP 需要加到你的防火墙白名单,见 出口 IP。
  5. 查询兜底。通知可能延迟或丢失(你的服务宕机、网络不通)。对长时间没有收到通知的订单,用 查询接口 主动查询,以查询结果为准。调用接口超时时,用同一个商户单号查询或重试,不要换单号重新下单。
金额单位一律是分,使用整数。例如 100 元写 10000。

2. 通用约定

项目说明
接口地址https://api.example.com/v1/...,使用 HTTPS。
编码请求和响应都是 JSON,UTF-8 编码。有请求体时请求头带 Content-Type: application/json。
请求头X-App-Id:你的 app_id。
X-Timestamp:秒级 Unix 时间戳,和平台时间相差不能超过 300 秒。
X-Nonce:每次请求不同,16 到 64 位,只能包含 0-9 A-Z a-z _ -,10 分钟内不能重复。
X-Signature:签名,小写十六进制。
请求体大小最大 65536 字节(64KB),超过返回 413 BODY_TOO_LARGE。
未知字段请求体有接口没定义的字段时返回 400 INVALID_PARAM,不会被忽略。字段名写错能马上发现。
金额单位是分,整数。
时间格式响应里的时间是 RFC3339 格式,例如 2026-01-01T08:30:00+08:00。
响应格式{"code":"OK","message":"","request_id":"...","data":{...}}。成功时 HTTP 200,code 为 OK;失败时 HTTP 状态码为 4xx/5xx,code 是稳定的英文错误码,message 是中文说明。request_id 请在联系平台排查问题时提供。
响应签名验签通过的请求,响应头带 X-Signature、X-Timestamp、X-Request-Id,可以验证响应确实来自平台,见 响应验签。验签失败(401 等)的响应没有签名。
幂等商户单号(out_trade_no、out_refund_no)在你名下唯一。用相同的单号和相同的参数重复提交,返回原订单(HTTP 200),不会重复下单;单号相同但参数不同,返回 409 DUPLICATE_OUT_TRADE_NO 或 DUPLICATE_OUT_REFUND_NO。
限流同一来源 IP 每秒最多 200 次(验签前检查);同一个 app_id 默认每秒最多 50 次,超过返回 429 RATE_LIMITED,降低频率后重试。
查询范围只能查询自己名下的订单。别人的订单和不存在的订单返回同样的 404,不会泄露是否存在。

3. 签名

3.1 请求签名

签名覆盖整个请求体,不需要对 JSON 字段排序或拼接。先拼出待签名串,再用密钥做 HMAC-SHA256:

待签名串 = 大写METHOD + "\n" + PATH + "\n" + 排序后的QUERY + "\n" + TIMESTAMP + "\n" + NONCE + "\n" + hex(sha256(BODY))
签名     = hex(HMAC-SHA256(密钥, 待签名串))      // 小写十六进制

查询串的编码规则

和 Go 语言的 url.QueryEscape 完全一致:

各语言自带的编码函数和这个规则有差异,必须修正:PHP 的 urlencode 会把 ~ 编成 %7E;Python 的 quote_plus 默认保留 /(要传 safe='');Java 的 URLEncoder 不编码 * 并把 ~ 编成 %7E。建议实际发出的 URL 查询串就用排序编码后的这个串,保证签名和请求一致。

请求体签名用的字节必须和实际发出的字节完全一致:先把请求对象序列化成 JSON 字符串,再用这个字符串计算签名并原样发送。JSON 序列化时不必转义中文和 /(转义了也能验签,只是日志不易读)。

3.2 完整示例

下面是一个可以直接对照的示例,调试器里点“载入示例”可以得到相同的结果。

密钥(示例用,不是真实密钥):

0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

请求:POST /v1/payin/orders,X-Timestamp: 1767225600,X-Nonce: n0nce-0000000001,没有查询参数。请求体(一行,无多余空白):

{"out_trade_no":"M202601010001","product_code":"ALIPAY_H5","amount":10000,"subject":"测试商品","notify_url":"https://m.example.com/notify"}

第 1 步,计算请求体的 sha256(十六进制小写):

8ea26eef89ccd7f6c7624fe428283f32d9af2db43327e505f3f124ccba52427c

第 2 步,按顺序用换行符 \n 连接 6 项(第 3 行是空的,因为没有查询参数),得到待签名串,共 6 行:

POST
/v1/payin/orders

1767225600
n0nce-0000000001
8ea26eef89ccd7f6c7624fe428283f32d9af2db43327e505f3f124ccba52427c

注意第 3 行是空行(排序后的 QUERY 为空),不含任何字符;最后一行结尾没有换行符。

第 3 步,用密钥对待签名串做 HMAC-SHA256,得到签名:

d596c056bc211383eb3c208fb707a234931c7c7660f919a7af9dbf5d195cd547

第 4 步,发送请求:

POST https://api.example.com/v1/payin/orders
Content-Type: application/json
X-App-Id: app_xxxxxxxxxxxxxxxx
X-Timestamp: 1767225600
X-Nonce: n0nce-0000000001
X-Signature: d596c056bc211383eb3c208fb707a234931c7c7660f919a7af9dbf5d195cd547

{"out_trade_no":"M202601010001","product_code":"ALIPAY_H5","amount":10000,"subject":"测试商品","notify_url":"https://m.example.com/notify"}

带查询参数的例子:GET /v1/payouts?z=2&out_trade_no=A+B~C%2A%E4%B8%AD&a=b&a=a&empty=,“排序后的 QUERY”是 a=a&a=b&empty=&out_trade_no=A+B~C%2A%E4%B8%AD&z=2(时间戳 1767225600,Nonce n0nce_0000000003,签名 7228739edeaf0c6dff64ee73d745163a60d54faf5c4db6cc3c1ae8e98410b0a2)。其中参数值 A B~C*中 的空格、*、中文都按上面的规则编码。这个例子也在页面加载时用来自检调试器,见下一节。

没有请求体的 POST(关闭订单):POST /v1/payin/orders/P2026010100000001/close,不要发送请求体,待签名串里 BODY 部分用空串的 sha256。

3.3 响应验签

验签通过的请求,平台的响应带 3 个头:X-Signature、X-Timestamp、X-Request-Id。响应的待签名串是 4 行:

响应待签名串 = HTTP状态码 + "\n" + X-Request-Id + "\n" + X-Timestamp + "\n" + hex(sha256(响应体原始字节))
签名         = hex(HMAC-SHA256(密钥, 响应待签名串))

示例(HTTP 200,X-Request-Id: 01JGZ8Q4R5S6T7V8W9X0Y1Z2A3,X-Timestamp: 1767225600,响应体 {"code":"OK","message":"","request_id":"01JGZ8Q4R5S6T7V8W9X0Y1Z2A3","data":{"order_no":"P1"}}):

200
01JGZ8Q4R5S6T7V8W9X0Y1Z2A3
1767225600
bd1480965b3c4bdcaed90c0f5fe83abae05e010d2cf2740dfcf6866f91b4370c

对应签名 0cee6bfb3f7566624bc5e28aecee85e5789038652546252046fd8483c04a0de9。响应带 X-Signature 时建议验签,不一致就丢弃这次响应,不要使用里面的数据。响应体要用收到的原始字节计算,不要先解析再重新序列化。

4. 在线签名调试器

填入和你的请求相同的内容,就能看到排序后的查询串、待签名串和签名,用来和你程序算出的结果对比。密钥只在你的浏览器里计算,不会发送出去:本页的调试器不发起任何网络请求,计算使用浏览器自带的 WebCrypto(HMAC-SHA256、SHA-256)。即便如此,仍建议只在这里使用测试密钥。

载入示例:

排序并编码后的查询串

请求体 sha256

待签名串(逐行,行号后的竖线只是分隔,不属于内容)

签名(X-Signature)

5. 接口

下面每个接口的请求都要按 签名 规则带 4 个请求头。表格里“必填”列为“是”的字段不能缺少。

接口说明
POST /v1/payin/orders下单
GET /v1/payin/orders/{order_no}
GET /v1/payin/orders?out_trade_no=
查询订单
POST /v1/payin/orders/{order_no}/close关闭订单
POST /v1/refunds申请退款
GET /v1/refunds/{refund_no}
GET /v1/refunds?out_refund_no=
查询退款
POST /v1/payouts/fee-quote代付手续费试算
POST /v1/payouts发起代付
GET /v1/payouts/{order_no}
GET /v1/payouts?out_trade_no=
查询代付

POST5.1 下单

POST https://api.example.com/v1/payin/orders

请求字段

字段类型必填说明
out_trade_nostring是你的订单号,最长 64 个字符,在你名下唯一。重复提交且参数相同时返回原订单。
product_codestring是支付产品编码,例如 ALIPAY_H5。可用的产品编码向平台或上级确认,未开通的产品返回 PRODUCT_NOT_ENABLED。
amountint是订单金额,单位分,必须大于 0。超出产品或通道允许的金额范围时没有通道可用,返回 CHANNEL_UNAVAILABLE,message 里写明原因。
subjectstring是商品名称或订单标题,最长 128 个字。
notify_urlstring否支付结果通知地址,最长 512 个字符。只允许 http/https,端口只能是 80、443、8080、8443,不能是内网地址。不填则平台不会发通知,你只能靠查询接口得知结果。
return_urlstring否用户支付完成后跳转回你网站的地址,只能是 http 或 https 地址,最长 512 个字符。
client_ipstring否用户的 IP 地址,最长 45 个字符。填写后可以让风控更准确,部分通道要求提供。
attachstring否附加数据,最长 512 个字符,原样放在通知的 data.attach 里(下单和查询的响应里没有这个字段)。
expire_secondsint否订单有效期,单位秒,不填或填 0 使用通道和产品的默认有效期;填写时不能小于 60,且只能比默认值短,不会延长。

请求示例

{
  "out_trade_no": "M202601010001",
  "product_code": "ALIPAY_H5",
  "amount": 10000,
  "subject": "测试商品",
  "notify_url": "https://m.example.com/notify",
  "return_url": "https://m.example.com/return",
  "client_ip": "203.0.113.8",
  "attach": "uid=1001",
  "expire_seconds": 900
}

响应字段(data)

下单、查询订单、关闭订单返回相同结构:

字段类型说明
order_nostring平台订单号。
out_trade_nostring你的订单号。
statusstring订单状态,见 状态说明。
order_amountint订单金额,单位分,等于你下单时的 amount。
pay_amountint用户实际应付金额,单位分。有立减活动时小于 order_amount,否则相等。
pay_payloadobject通道返回的支付参数,内容随通道不同(常见字段如 pay_url)。没有时不出现。
cashier_urlstring平台收银页链接,把用户引导到这个地址即可支付。只有待支付的订单才有;产品配置为直接返回上游链接时没有,这时使用 pay_payload。
expire_atstring订单过期时间。过期后收银页停止受理。
final_close_atstring订单最终关闭时间,等于过期时间加上通道的迟到回调宽限期。宽限期内如果收到上游的支付成功,订单仍会变成已支付。
paid_atstring支付成功时间,未支付时不出现。
error_codestring下单失败时的错误码,没有失败时不出现。
error_msgstring下单失败时的原因,没有失败时不出现。
上游下单失败(例如上游拒绝、超时)时,接口仍返回 HTTP 200,订单 status 为 failed,原因在 error_code、error_msg。请判断 status,不要只看 HTTP 状态码。平台不会自动换通道重试;需要重试请换一个新的商户单号下单。

响应示例

{
  "code": "OK",
  "message": "",
  "request_id": "01JGZ8Q4R5S6T7V8W9X0Y1Z2A3",
  "data": {
    "order_no": "P2026010100000001",
    "out_trade_no": "M202601010001",
    "status": "pending",
    "order_amount": 10000,
    "pay_amount": 10000,
    "pay_payload": {"pay_url": "https://pay.example.com/xxxx"},
    "cashier_url": "https://cashier.example.com/c/xxxx",
    "expire_at": "2026-01-01T08:15:00+08:00",
    "final_close_at": "2026-01-01T08:20:00+08:00"
  }
}

可能的错误码:INVALID_PARAM、MERCHANT_FROZEN、PAYIN_DISABLED、PRODUCT_NOT_ENABLED、RISK_REJECTED、CHANNEL_UNAVAILABLE、FEE_NOT_CONFIGURED、DUPLICATE_OUT_TRADE_NO、SERVICE_UNAVAILABLE,见 错误码。

GET5.2 查询订单

GET https://api.example.com/v1/payin/orders/{order_no}
GET https://api.example.com/v1/payin/orders?out_trade_no={out_trade_no}

按平台订单号或你的商户单号查询。没有请求体。用商户单号查询时,参数名是 out_trade_no,缺少时返回 400 INVALID_PARAM。响应字段和 下单 相同;订单待支付时响应带 cashier_url。订单不存在或不属于你返回 404 ORDER_NOT_FOUND。

{
  "code": "OK",
  "message": "",
  "request_id": "01JGZ8Q4R5S6T7V8W9X0Y1Z2A5",
  "data": {
    "order_no": "P2026010100000001",
    "out_trade_no": "M202601010001",
    "status": "paid",
    "order_amount": 10000,
    "pay_amount": 10000,
    "expire_at": "2026-01-01T08:15:00+08:00",
    "final_close_at": "2026-01-01T08:20:00+08:00",
    "paid_at": "2026-01-01T08:03:21+08:00"
  }
}

POST5.3 关闭订单

POST https://api.example.com/v1/payin/orders/{order_no}/close

商户主动关闭待支付订单。没有请求体,签名时 BODY 用空串。返回关单后的订单(结构同下单响应),状态有几种可能:

订单已经是 closed、expired、failed 时重复关闭,直接返回当前状态。订单已支付(含部分退款、全额退款)时返回 409 ORDER_NOT_CLOSABLE,需要退款请用 退款接口;订单正在提交上游(created)时也返回 ORDER_NOT_CLOSABLE,稍后重试。订单最终关闭时会收到 payin.closed 通知。

POST5.4 申请退款

POST https://api.example.com/v1/refunds

支持部分退款,用 out_refund_no 保证幂等。同一张订单同一时间只能有一笔退款在处理中。

请求字段

字段类型必填说明
order_nostring二选一平台订单号。和 out_trade_no 必须且只能填一个。
out_trade_nostring二选一你的订单号。
out_refund_nostring是你的退款单号,最长 64 个字符,在你名下唯一。
amountint是退款金额,单位分,必须大于 0,不能超过订单剩余可退金额。
reasonstring否退款原因,最长 255 个字。
{
  "order_no": "P2026010100000001",
  "out_refund_no": "RF202601010001",
  "amount": 3000,
  "reason": "用户申请退款"
}

响应字段(data)

字段类型说明
refund_nostring平台退款单号。
out_refund_nostring你的退款单号。
order_nostring平台订单号。
out_trade_nostring你的订单号。
amountint退款金额,单位分。
statusstring退款状态:created、processing、success、failed、unknown,见 状态说明。
error_codestring退款失败的错误码,没有失败时不出现。
error_msgstring退款失败的原因,没有失败时不出现。
created_atstring退款单创建时间。
finished_atstring退款完成时间,未完成时不出现。
{
  "code": "OK",
  "message": "",
  "request_id": "01JGZ8Q4R5S6T7V8W9X0Y1Z2A6",
  "data": {
    "refund_no": "R2026010100000001",
    "out_refund_no": "RF202601010001",
    "order_no": "P2026010100000001",
    "out_trade_no": "M202601010001",
    "amount": 3000,
    "status": "processing",
    "created_at": "2026-01-01T09:00:00+08:00"
  }
}
退款结果不一定是立即确定的。status 为 processing 或 unknown 时,以之后的 refund.succeeded / refund.failed 通知或查询结果为准。

可能的错误码:INVALID_PARAM、ORDER_NOT_FOUND、ORDER_NOT_REFUNDABLE、REFUND_NOT_SUPPORTED、REFUND_IN_PROGRESS、REFUND_AMOUNT_EXCEEDED、INSUFFICIENT_BALANCE、DUPLICATE_OUT_REFUND_NO。

GET5.5 查询退款

GET https://api.example.com/v1/refunds/{refund_no}
GET https://api.example.com/v1/refunds?out_refund_no={out_refund_no}

没有请求体,响应字段同 申请退款。退款单不存在或不属于你返回 404 REFUND_NOT_FOUND。

POST5.6 代付手续费试算

POST https://api.example.com/v1/payouts/fee-quote

按当前生效的费率试算,不冻结资金、不生成订单。

字段类型必填说明
product_codestring是代付产品编码。
amountint是代付金额,单位分,必须大于 0。

响应字段(data)

字段类型说明
amountint代付金额,单位分。
feeint手续费,单位分。
fee_modestring手续费收取方式:external(外扣:冻结 amount + fee,收款人收到 amount);internal(内扣:冻结 amount,收款人收到 amount − fee)。
hold_amountint需要从你的账户冻结的金额,单位分。
payee_amountint收款人实际到账金额,单位分。
请求:
{"product_code": "BANK_PAYOUT", "amount": 50000}

响应:
{
  "code": "OK",
  "message": "",
  "request_id": "01JGZ8Q4R5S6T7V8W9X0Y1Z2A7",
  "data": {
    "amount": 50000,
    "fee": 300,
    "fee_mode": "external",
    "hold_amount": 50300,
    "payee_amount": 50000
  }
}

可能的错误码:INVALID_PARAM(金额不大于 0,或内扣时手续费超过金额)、PAYOUT_DISABLED、PRODUCT_NOT_ENABLED。

POST5.7 发起代付

POST https://api.example.com/v1/payouts

调用代付接口前,必须已在商户后台给这把 API 密钥设置 IP 白名单,否则返回 403 IP_WHITELIST_REQUIRED。请求必须来自白名单内的 IP。

请求字段

字段类型必填说明
out_trade_nostring是你的代付单号,最长 64 个字符,在你名下唯一。
product_codestring是代付产品编码。
amountint是代付金额,单位分,必须大于 0。
payeeobject是收款人,字段见下。
payee.namestring是收款人户名,最长 64 个字。
payee.accountstring是银行卡号,12 到 30 位数字。
payee.typestring是personal(对私)或 corporate(对公)。
payee.cnaps_codestring对公必填12 位联行号。对公(corporate)必须填;对私填写时也必须是 12 位。
payee.bank_codestring否银行编码,最长 16 个字符。
payee.bank_namestring否开户行名称,最长 64 个字。
notify_urlstring否代付结果通知地址,规则同下单的 notify_url。
attachstring否附加数据,最长 512 个字符,原样放在通知里。
{
  "out_trade_no": "W202601010001",
  "product_code": "BANK_PAYOUT",
  "amount": 50000,
  "payee": {
    "name": "张三",
    "account": "6222020200112233445",
    "type": "personal",
    "bank_code": "ICBC",
    "bank_name": "中国工商银行"
  },
  "notify_url": "https://m.example.com/payout-notify",
  "attach": "batch=7"
}

卡号和姓名平台收到后立即加密保存,响应和查询只返回打码后的姓名和卡号后四位。

响应字段(data)

发起代付和查询代付返回相同结构:

字段类型说明
order_nostring平台代付单号。
out_trade_nostring你的代付单号。
statusstring代付状态,见 状态说明。
amountint代付金额,单位分。
feeint手续费,单位分。
fee_modestringexternal(外扣)或 internal(内扣)。
hold_amountint冻结金额,单位分。
payee_amountint收款人实际到账金额,单位分;内扣时为 amount − fee。
payee_namestring收款人姓名(已打码)。
payee_account_last4string收款账号后四位。
failure_codestring失败原因码,未失败时不出现,见 状态说明。
failure_msgstring失败原因说明,未失败时不出现。
created_atstring创建时间。
finished_atstring完成时间,未完成时不出现。
{
  "code": "OK",
  "message": "",
  "request_id": "01JGZ8Q4R5S6T7V8W9X0Y1Z2A8",
  "data": {
    "order_no": "D2026010100000001",
    "out_trade_no": "W202601010001",
    "status": "processing",
    "amount": 50000,
    "fee": 300,
    "fee_mode": "external",
    "hold_amount": 50300,
    "payee_amount": 50000,
    "payee_name": "张*",
    "payee_account_last4": "3445",
    "created_at": "2026-01-01T10:00:00+08:00"
  }
}
被风控拒绝、余额不足、超过单日上限、没有可用通道的代付单同样会生成订单,接口返回 HTTP 200,status 为 failed,failure_code 写明原因(这类单没有冻结资金)。请判断 status。status 为 processing、reviewing、unknown 时,以之后的 payout.succeeded / payout.failed 通知或查询结果为准,不要当作失败重复提交。

可能的错误码:IP_WHITELIST_REQUIRED、INVALID_PARAM、MERCHANT_NOT_FOUND、MERCHANT_FROZEN、PAYOUT_DISABLED、PRODUCT_NOT_ENABLED、DUPLICATE_OUT_TRADE_NO、SERVICE_UNAVAILABLE。

GET5.8 查询代付

GET https://api.example.com/v1/payouts/{order_no}
GET https://api.example.com/v1/payouts?out_trade_no={out_trade_no}

没有请求体,响应字段同 发起代付。不存在或不属于你返回 404 ORDER_NOT_FOUND。

6. 状态说明

代收订单 status

值含义是否终态
created已生成订单,还在向上游下单。通常很快变化。否
pending下单成功,等待用户支付。否
paid已支付并入账。收到 payin.paid 通知。是(可退款)
partial_refunded已部分退款。可继续退款
refunded已全额退款。是
expired已过有效期,收银页不再受理,但宽限期内仍接受迟到的支付成功。否,到 final_close_at 变为 closed
closed最终关闭。收到 payin.closed 通知。是
failed上游下单失败,看 error_code、error_msg。平台不自动换通道。是

退款单 status

值含义
created退款单已生成,还没有提交上游。
processing处理中,等待通知或查询。
success退款成功(终态)。
failed退款失败(终态),看 error_code、error_msg。
unknown结果不明(例如调用上游超时),等待平台查单确认,以通知或查询结果为准。

代付单 status

值含义
accepted已受理,正在风控和冻结(短暂状态)。
reviewing等待人工审核,资金已冻结。
processing已提交或等待提交上游。
unknown当前结果不明,平台会查单确认,不要重复提交。
success代付成功(终态)。
failed代付失败(终态),看 failure_code、failure_msg。

代付失败原因 failure_code

值含义
RISK_REJECTED被风控规则拒绝,failure_msg 写明规则原因。
PAYEE_RISK_REJECTED收款账户存在风险,暂不能代付。
CHANNEL_UNAVAILABLE没有可用的代付通道。
INSUFFICIENT_BALANCE可用余额不足以冻结所需金额。
DAILY_CAP_EXCEEDED超过单日代付上限。
REVIEW_REJECTED人工审核未通过。
REVIEW_TIMEOUT人工审核超时。
NOT_SUBMITTED审核通过后通道已经不可用,没有提交上游。

除上表外,上游通道返回失败时,failure_code 可能是上游给出的错误码,请以 failure_msg 为准。

7. 事件通知

7.1 格式

订单状态变化时,平台向订单的 notify_url 发送 HTTP POST。退款通知发到原订单的 notify_url。请求头:

请求头说明
Content-Typeapplication/json; charset=utf-8
X-App-Id签名所用密钥对应的 app_id(用你最新的一把有效密钥签名;轮换密钥期间,验签代码要按 X-App-Id 选择对应的密钥)。
X-Timestamp秒级时间戳。
X-Nonce随机串。
X-Signature签名。
X-Event-Id事件 ID,同请求体里的 event_id。

请求体:

字段类型说明
event_idstring事件唯一 ID。同一个事件重试时 event_id 不变。
event_typestring事件类型,见 事件和 data。
created_atstring事件产生时间(UTC,RFC3339)。
dataobject事件内容。
{
  "event_id": "01JGZ9A1B2C3D4E5F6G7H8J9K0",
  "event_type": "payin.paid",
  "created_at": "2026-01-01T00:03:21Z",
  "data": {
    "order_no": "P2026010100000001",
    "out_trade_no": "M202601010001",
    "status": "paid",
    "order_amount": 10000,
    "pay_amount": 10000,
    "merchant_fee": 60,
    "merchant_credit": 9940,
    "paid_at": "2026-01-01T08:03:21+08:00",
    "attach": "uid=1001"
  }
}

平台不跟随重定向,你的地址返回 3xx 按失败处理。通知地址只允许 http/https,端口只能是 80、443、8080、8443,不能解析到内网地址。平台发送通知的超时时间是 10 秒。

7.2 验签和处理

通知的签名方法和你给平台发请求时完全一样(3.1),你可以复用同一套签名代码:

示例(密钥同上,X-Timestamp: 1767225602,X-Nonce: n0nce-0000000005):请求体 {"event_id":"E1","event_type":"payin.paid","created_at":"2026-01-01T00:00:02Z","data":{"order_no":"P1","status":"paid"}},待签名串:

POST
/pay/notify
shop=1
1767225602
n0nce-0000000005
fc2c16e95e3338fb0830456c947d7ff9d5919115b3e3d3b1816d64e02e0ac281

签名为 f759856d695aa15f2dbafe968be0972314fb84ea6a5eb400bce4573f2ef9523e。

收到通知后按下面顺序处理:

  1. 检查 X-App-Id 等于你自己的 app_id。
  2. 检查 X-Timestamp 与你服务器时间相差不超过 300 秒。
  3. 按上面方法计算签名,和 X-Signature 比较(用常量时间比较函数)。不一致就拒绝(返回 4xx),不要处理数据。
  4. 按 event_id 去重:把已处理的 event_id 记下来,重复收到时不再重复处理业务,但仍要返回 2xx。
  5. 更新你自己的订单状态。金额以通知里的 data 为准,必要时再调用 查询接口 核对。
  6. 返回 HTTP 2xx(响应内容任意,例如 success)。返回非 2xx、3xx 或超时,平台都会重试。
收到通知后先落库再返回,业务处理放到后台做。处理慢于 10 秒会被当作失败并重试,同一个事件可能重复到达,所以必须去重。

7.3 重试规则

通知失败(连接失败、超时、返回非 2xx)后按下面的间隔重试。第 1 次投递在事件产生后立即进行,之后每次失败等待对应的间隔再投递:

失败次数下一次投递的等待时间
第 1 次失败后30 秒
第 2 次失败后1 分钟
第 3 次失败后5 分钟
第 4 次失败后10 分钟
第 5 次失败后30 分钟
第 6 次失败后1 小时
第 7 次失败后2 小时
第 8 次失败后6 小时
第 9 次失败后放弃,平台生成异常单交给运营人员处理

也就是一个事件最多投递 9 次。放弃之后平台不会再自动发送,请用 查询接口 兜底,也可以联系平台处理。

7.4 事件和 data

event_type触发时机data 字段
payin.paid订单支付成功order_no、out_trade_no、status(paid)、order_amount、pay_amount、merchant_fee(手续费,分)、merchant_credit(入账金额,分)、paid_at、attach
payin.closed订单最终关闭order_no、out_trade_no、status(closed)、order_amount、pay_amount、closed_at、attach
refund.succeeded退款成功refund_no、out_refund_no、order_no、out_trade_no、amount、status(success)、finished_at
refund.failed退款失败同上,status 为 failed,另有 error_code、error_msg
payout.succeeded代付成功order_no、out_trade_no、status(success)、amount、fee、payee_amount、finished_at、attach
payout.failed代付失败order_no、out_trade_no、status(failed)、amount、fee、failure_code、failure_msg、attach
test.ping在商户后台点击“发送测试通知”message:测试说明文字

金额字段单位都是分。建议按 event_type 分支处理,对不认识的 event_type 返回 2xx 即可(避免平台持续重试)。test.ping 用于联调:验签通过并返回 2xx 即算成功。

7.5 出口 IP 白名单

如果你的服务器有防火墙或只接收白名单 IP 的请求,请放行下面这些平台发通知所用的出口 IP:

平台的通知来源 IP 在商户后台「对接资料」页面查看。平台换服务器时 IP 可能变化,以后台显示的为准。

注意这是平台访问你的出口 IP;你访问平台接口时使用的 IP 则要加到商户后台的 API 白名单里,两者方向不同。

8. 错误码

失败时响应为 {"code":"错误码","message":"中文说明","request_id":"..."}(INVALID_SIGNATURE 另带 data.sign_base)。HTTP 状态码和错误码一起判断。网络错误或超时不是业务错误,见 常见问题。

验签和通用错误

验签阶段的错误,响应没有签名头。

错误码HTTP含义处理建议
MISSING_AUTH_HEADER401缺少 X-App-Id、X-Timestamp、X-Nonce 或 X-Signature。补全 4 个请求头;检查网关或代理是否丢弃了自定义头。
INVALID_TIMESTAMP401X-Timestamp 不是秒级 Unix 时间戳。使用秒(10 位)而不是毫秒(13 位)。
TIMESTAMP_EXPIRED401X-Timestamp 与平台时间相差超过 300 秒,message 里写明相差的秒数。校准服务器时间(NTP),每次请求取当前时间重新签名,不要缓存签名。
INVALID_NONCE401X-Nonce 不是 16 到 64 位字母、数字、下划线或横线。用 32 位随机十六进制字符串。
INVALID_APP_ID401app_id 不存在或已停用(商户被关闭时同样返回)。核对 app_id;联系平台确认密钥状态。
INVALID_SIGNATURE401签名不一致。data.sign_base 是平台算出的待签名串。把 data.sign_base 和你的待签名串逐行对比,见 常见问题。
NONCE_REUSED401X-Nonce 已经使用过。每次请求(包括重试)都生成新的 Nonce。
IP_NOT_ALLOWED403来源 IP 不在这把密钥的白名单内,message 里写明平台看到的来源 IP。把 message 里的 IP 加入商户后台白名单;如果经过了代理或 NAT,加入实际出口 IP。
IP_WHITELIST_REQUIRED403调用代付接口,但这把密钥没有设置 IP 白名单。先在商户后台设置 IP 白名单再调用代付。
RATE_LIMITED429请求太频繁(同一 IP 每秒 200 次,同一 app_id 默认每秒 50 次)。降低频率,稍后重试。
BODY_TOO_LARGE413请求体超过 65536 字节。精简请求体。
SERVICE_UNAVAILABLE503服务暂时不可用(例如平台依赖的服务临时故障)。稍后用相同的商户单号重试,不会重复下单。
INTERNAL_ERROR500平台内部错误。用相同的商户单号查询或重试;持续出现时带上 request_id 联系平台。

业务错误

错误码HTTP含义处理建议
INVALID_PARAM400参数不合法、缺少必填字段、请求体不是合法 JSON 或有未知字段,message 写明具体原因。按 message 修正请求,不要原样重试。
MERCHANT_NOT_FOUND403商户不存在。联系平台。
MERCHANT_FROZEN403商户已被冻结或关闭,不能下单、代付。查询接口仍可使用。联系平台或上级。
PAYIN_DISABLED403商户未开通代收。联系平台开通。
PAYOUT_DISABLED403商户未开通代付。联系平台开通。
PRODUCT_NOT_ENABLED403产品不存在、不可用,或商户未开通该产品。核对 product_code,联系平台开通。
RISK_REJECTED403下单被风控规则拒绝,message 写明原因。不要原样重试;有疑问联系平台。
CHANNEL_UNAVAILABLE503没有可用的通道(例如金额超出通道范围、通道暂停),message 写明原因。核对金额;稍后重试或联系平台。
FEE_NOT_CONFIGURED503该产品没有配置费率。联系平台或上级配置费率。
DUPLICATE_OUT_TRADE_NO409商户单号已存在,且参数与原单不同(代收比较产品、金额、标题、通知地址、返回地址、附加数据;代付比较产品、金额、收款人、通知地址、附加数据)。要重新下单请换新单号;要查原单用查询接口。
ORDER_NOT_FOUND404订单(或代付单)不存在或不属于你。核对订单号。
ORDER_NOT_CLOSABLE409订单已支付不能关闭,或订单正在提交上游。已支付请走退款;正在提交上游请稍后重试。
ORDER_NOT_REFUNDABLE409订单状态不允许退款(例如未支付),message 写明当前状态。只有已支付或部分退款的订单可以退款。
REFUND_NOT_SUPPORTED400该订单所用的通道不支持退款。联系平台线下处理。
REFUND_IN_PROGRESS409这张订单有一笔退款正在处理。等它完成(成功或失败)后再发起。
REFUND_AMOUNT_EXCEEDED400退款金额超过剩余可退金额,message 写明可退金额。调整退款金额。
INSUFFICIENT_BALANCE409退款需要冻结的余额不足。等待余额充足后再退款。
DUPLICATE_OUT_REFUND_NO409退款单号已存在,且参数与原退款单不同。换新的退款单号;查原单用查询接口。
REFUND_NOT_FOUND404退款单不存在或不属于你。核对退款单号。

代付被风控拒绝、余额不足、超过单日上限、没有可用通道不是 HTTP 错误,而是 HTTP 200 加 status: failed,原因见 代付失败原因。

9. SDK 下载

SDK 只依赖各语言标准库,封装了签名、请求、响应验签和通知验签,包内的 README 有下单、查询、处理通知的完整示例。

也可以登录商户后台,在「对接资料」页面下载。

10. 常见问题

签名不一致(INVALID_SIGNATURE)怎么排查?

响应的 data.sign_base 是平台根据你的请求算出的待签名串(不含密钥)。把它和你程序里的待签名串逐行对比(共 6 行:METHOD、PATH、QUERY、TIMESTAMP、NONCE、BODY 的 sha256),也可以把它粘贴到上面调试器的“平台返回的 sign_base”里自动对比。常见原因:

报 TIMESTAMP_EXPIRED 怎么办?

说明你的服务器时间和平台相差超过 300 秒。用 NTP 校准时间;每次请求都取当前时间生成时间戳并重新签名;用的是秒,不是毫秒。

报 NONCE_REUSED 怎么办?

X-Nonce 在 10 分钟内重复了。每次请求,包括超时后的重试,都要生成新的随机 Nonce(建议用安全随机数生成 32 位十六进制)。不要用固定值,也不要用只精确到秒的时间戳当 Nonce。

为什么代付必须设置 IP 白名单?

代付是出款操作,为了避免密钥泄露后被人直接转走资金,平台要求代付所用的密钥必须在商户后台设置 IP 白名单,并且只接受白名单内 IP 的请求。没设置时返回 403 IP_WHITELIST_REQUIRED;IP 不在白名单内返回 403 IP_NOT_ALLOWED。

网络超时了,要不要换单号重新下单?

不要。超时只说明你没收到响应,不代表平台没有收到请求。正确做法:

通知收不到怎么办?

检查:下单时是否填了 notify_url;地址是否是公网可访问的 http/https,端口是否为 80、443、8080、8443;你的防火墙是否放行了 平台出口 IP;服务是否在 10 秒内返回了 2xx;是否返回了 3xx(平台不跟随重定向)。无论如何,都要用 查询接口 兜底。

重复收到同一个通知怎么办?

正常现象(重试、你的响应超时)。按 event_id 去重,已处理的直接返回 2xx。

pay_amount 和 order_amount 为什么不一样?

有立减活动时,用户实付金额 pay_amount 会小于订单金额 order_amount。收款以 pay_amount 为准。