商户支付接口对接文档
用于平台商户创建订单、查询订单并接收支付结果通知。接口地址、商户号、密钥、已授权产品编码及通知出口 IP 均由平台单独提供,以商户后台“对接信息”为准。
1. 对接准备
平台会通过安全渠道向商户提供以下信息:
| 信息 | 用途 |
|---|---|
merchant_no | 商户唯一编号。 |
| API Key | 签名密钥,只在开户或重置时展示一次。 |
| 接口基址 | 下单、查单和通知补发接口的域名与前缀。 |
| 已授权产品编码 | 下单必填。只允许使用平台为当前商户单独授权的编码,本文档不写入任何固定产品编码。 |
| 通知出口 IP | 商户防火墙辅助放行;不能代替通知验签。 |
所有请求必须使用 HTTPS。商户请求 JSON 采用严格字段校验:未在本文档列出的字段会返回 HTTP 400,请勿自行增加平台内部字段。
2. 请求签名
2.1 请求头
| 请求头 | 必填 | 说明 |
|---|---|---|
X-Merchant-No | 是 | 平台提供的商户号。 |
X-Timestamp | 是 | 当前 Unix 秒,允许与平台时间相差不超过 300 秒。 |
X-Nonce | 是 | 16~64 位字母、数字、下划线或短横线;同一商户 5 分钟内不得重复。 |
X-Signature | 是 | 规范串的 HMAC-SHA256 小写十六进制结果。 |
2.2 规范串
HTTP_METHOD
REQUEST_PATH_AND_QUERY
X-Timestamp
X-Nonce
SHA256_HEX(RAW_BODY)
- 每部分使用单个换行符
\n分隔,末尾不追加换行。 - 第二行只使用路径和查询串,不包含协议、域名和端口。
- GET 请求的原始请求体为空字节串。
- POST 必须对实际发送的原始 UTF-8 JSON 字节计算摘要,不能签名后重新序列化 JSON。
- HMAC 密钥是
SHA-256(api_key)的 32 字节结果,不是该结果的十六进制文本。
2.3 Java 签名示例
byte[] bodyBytes = json.getBytes(StandardCharsets.UTF_8);
String bodyHash = hex(MessageDigest.getInstance("SHA-256").digest(bodyBytes));
String canonical = method + "\n" + pathAndQuery + "\n" + timestamp
+ "\n" + nonce + "\n" + bodyHash;
byte[] signingKey = MessageDigest.getInstance("SHA-256")
.digest(apiKey.getBytes(StandardCharsets.UTF_8));
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(signingKey, "HmacSHA256"));
String signature = hex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8)));
2.4 密钥重置
重置后平台返回新密钥及旧密钥验签截止时间。截止时间前,商户调用平台接口可使用当前密钥或紧邻的上一把密钥;平台发给商户的新通知会从重置完成时起立即使用新密钥签名。商户应在过渡期内同时保留两把密钥用于通知验签,完成切换后删除旧密钥。
3. 商户下单
POST /api/v1/gateway/orders
请求头另加 Content-Type: application/json; charset=UTF-8,请求体最大 1MB。
| 字段 | 必填 | 限制 | 说明 |
|---|---|---|---|
merchant_order_no | 是 | 1~64 位;字母、数字、_、- | 商户订单号,在当前商户下永久唯一。 |
product_code | 是 | 2~64 位;大写字母、数字、_、- | 使用平台单独提供且已授权给当前商户的产品编码。 |
total_amount | 是 | 金额字符串,最多 2 位小数 | 人民币元,范围 0.01~100000000.00。 |
notify_url | 是 | 最多 1000 字符 | 本订单通知地址;必须是可解析的公网 HTTPS 443 地址,不允许内网、本机、用户信息或片段。 |
下单请求示例
{
"merchant_order_no": "M202608040001",
"product_code": "<平台单独提供的产品编码>",
"total_amount": "10.00",
"notify_url": "https://merchant.example.com/payment/notify"
}
当前支持的产品流程
| 产品流程 | 平台内部行为 |
|---|---|
| 普通手机网站 | 调用 alipay.trade.wap.pay,不发送直付通二级商户或结算字段。 |
| 普通 JSAPI | 平台取得付款人支付宝 UID 后调用 alipay.trade.create,不发送直付通字段。 |
| 直付通手机网站 | 平台路由已审核二级商户,再调用 alipay.trade.wap.pay 并携带直付通结算字段。 |
| 直付通 JSAPI | 平台完成付款人授权和二级商户路由后调用 alipay.trade.create 并携带直付通结算字段。 |
具体产品流程绑定在平台提供的 product_code 上,商户不能通过请求字段临时切换。
3.1 幂等规则
- 首次创建成功返回 HTTP 201;完全相同的请求重试返回原订单和 HTTP 200。
merchant_order_no相同但金额、产品编码或通知地址不同,返回 HTTP 409。- 请求超时、网络中断、502 或 503 时禁止换新订单号重下。应先查原单,或使用相同订单号和完全相同的请求重试。
3.2 响应字段
| 字段 | 说明 |
|---|---|
merchant_order_no | 商户订单号。 |
platform_trade_no | 平台订单号。 |
alipay_trade_no | 支付宝交易号;尚未生成时为空。 |
trade_status | 当前交易状态。 |
total_amount | 订单金额。 |
product_code | 本订单创建时使用的产品编码快照。 |
cashier_url / pay_url | 平台收银台地址;两个字段值相同。 |
notify_status | 通知状态,与交易状态相互独立。 |
idempotent_replay | 本次下单是否返回已存在的同一订单。 |
4. 商户查单
GET /api/v1/gateway/orders/{merchant_order_no}
按第 2 节对空请求体签名。只能查询当前鉴权商户自己的订单。已经生成支付宝交易号的非终态订单会主动查询支付宝并更新状态;付款授权尚未完成时返回平台当前状态;已确认成功、完成或关闭的终态订单直接返回本地结果。
{
"merchant_order_no": "M202608040001",
"platform_trade_no": "P...",
"alipay_trade_no": "2026...",
"trade_status": "TRADE_SUCCESS",
"total_amount": "10.00",
"product_code": "<本订单创建时使用的产品编码>",
"cashier_url": "https://pay.example.com/pay?...",
"pay_url": "https://pay.example.com/pay?...",
"notify_status": "SUCCESS",
"idempotent_replay": false
}
商户应定时查单兜底,不能把“尚未收到通知”当作“订单未支付”。
5. 异步通知
平台向下单时保存的完整 notify_url 发起 POST JSON。请求头仍为第 2 节的四个签名头,但规范串第二行使用完整通知 URL(包括其查询串),请求体摘要使用平台实际发送的原始 JSON 字节。
支付宝通知验签及交易事实落库的同一事务提交后,平台立即唤醒首发投递;200ms 周期扫描仅作为进程恢复和信号丢失兜底。公网传输、DNS、TLS 和商户处理时间不由平台控制,因此协议不承诺物理意义上的 0 毫秒,但平台没有人为等待窗口。
| 字段 | 说明 |
|---|---|
merchant_no | 平台商户号。 |
merchant_order_no | 商户订单号。 |
platform_trade_no | 平台订单号。 |
alipay_trade_no | 支付宝交易号,部分失败状态可能为空。 |
trade_status | 本次通知的交易状态。 |
total_amount | 订单金额。 |
notify_time | UTC ISO-8601 通知生成时间。 |
{
"merchant_no": "M...",
"merchant_order_no": "M202608040001",
"platform_trade_no": "P...",
"alipay_trade_no": "2026...",
"trade_status": "TRADE_SUCCESS",
"total_amount": "10.00",
"notify_time": "2026-08-04T07:00:00Z"
}
5.1 接收与确认
- 先验签,再校验
merchant_no、商户订单号和金额。 - 使用数据库唯一键实现幂等入账,不能只依赖进程内缓存。
- 处理完成后返回 HTTP 2xx,正文只返回纯文本
success。大小写不敏感,首尾空白会被忽略;建议固定返回小写。 - 超时、网络错误、非 2xx、响应超过 4KB 或正文不是
success均视为失败。
5.2 乱序与状态修正
- 同一订单的不同交易状态分别通知,可能重复或乱序到达。建议以
merchant_order_no + trade_status作为通知幂等键。 TRADE_FINISHED可能是商户收到的第一条成功通知,必须与TRADE_SUCCESS一样触发幂等入账。- 极端并发下可能先收到
TRADE_TIMEOUT,随后收到支付宝确认的TRADE_SUCCESS;最终必须以真实成功状态入账。
5.3 自动重试
| 通知类型 | 总尝试次数 | 失败后的重试间隔 |
|---|---|---|
TRADE_SUCCESS / TRADE_FINISHED | 首次立即+10 次重试,共 11 次 | 5 秒、30 秒、2 分钟、10 分钟、1 小时,随后每 6 小时一次,共 5 次。 |
TRADE_CLOSED / TRADE_TIMEOUT | 首次立即+5 次重试,共 6 次 | 5 秒、30 秒、2 分钟、10 分钟、1 小时。 |
平台可按商户关闭失败类订单通知;此时成功类通知不受影响,失败订单的 notify_status 为 NOT_REQUIRED,商户应使用查单接口兜底。
5.4 商户自助补发
POST /api/v1/gateway/orders/{merchant_order_no}/notify-retry
使用与查单相同的签名方式和空请求体,只允许当前商户自己的终态订单。接口会重新入队最近状态的通知,已在队列中的任务则如实返回当前状态。
| 返回字段 | 说明 |
|---|---|
merchant_order_no | 当前商户提交的订单号。 |
platform_trade_no | 平台订单号。 |
status | 补发任务当前状态,常见为 PENDING、RETRYING 或 PROCESSING。 |
{
"merchant_order_no": "M202608040001",
"platform_trade_no": "P...",
"status": "RETRYING"
}
6. 状态说明
6.1 交易状态
| 状态 | 含义 | 商户处理 |
|---|---|---|
CREATING | 订单创建处理中。 | 稍后查原单,不要换单号。 |
CREATE_UNKNOWN | 上游结果暂时未知。 | 查原单或以完全相同参数重试。 |
CREATE_FAILED | 上游明确创建失败。 | 修正原因后再按业务决定是否创建新订单。 |
WAIT_BUYER_PAY | 等待付款。 | 继续展示有效收银台。 |
TRADE_SUCCESS | 支付成功。 | 幂等入账。 |
TRADE_FINISHED | 交易完成。 | 按成功状态处理并保持已入账。 |
TRADE_CLOSED | 支付宝交易已关闭。 | 不入账。 |
TRADE_TIMEOUT | 平台已超时关闭订单且当时未确认成交。 | 不入账;若后续收到真实成功修正,按成功入账。 |
6.2 通知状态
| 状态 | 含义 |
|---|---|
NOT_REQUIRED | 当前无需通知,或该商户的失败类通知已关闭。 |
PENDING | 等待首次通知。 |
RETRYING | 此前未确认,正在等待自动或人工重试。 |
SUCCESS | 商户已按协议返回 success。 |
FAILED | 自动重试已耗尽,仍可自助或由平台人工补发。 |
交易状态表示订单资金结果,通知状态只表示消息是否被商户确认;通知失败不会把成功交易改成失败。
7. 错误处理
签名、IP 白名单、限流、请求校验及业务处理产生的 JSON 错误均使用以下统一结构:
{
"timestamp": "2026-08-04T07:00:00Z",
"code": "INVALID_REQUEST",
"message": "具体错误原因"
}
| HTTP | 常见代码 | 处理建议 |
|---|---|---|
| 400 | INVALID_REQUEST | 检查必填项、字段格式和是否传入未声明字段。 |
| 401 | UNAUTHORIZED / INVALID_SIGNATURE / SIGNATURE_EXPIRED / REPLAY_REQUEST | 检查商户状态、密钥、服务器时间、原始请求体和 Nonce。 |
| 403 | MERCHANT_IP_NOT_ALLOWED | 白名单为空或实际公网出口 IP 未配置;自行伪造转发头无效。 |
| 404 | NOT_FOUND | 当前商户下不存在该订单。 |
| 409 | IDEMPOTENCY_CONFLICT / DATA_CONFLICT | 同一商户订单号的参数不一致,禁止覆盖原单。 |
| 413 | REQUEST_TOO_LARGE | 请求体超过 1MB。 |
| 415 | UNSUPPORTED_MEDIA_TYPE | POST 应使用 JSON Content-Type。 |
| 429 | RATE_LIMITED | 按 Retry-After 等待,不要换订单号。 |
| 502 | ALIPAY_API_ERROR | 上游处理异常;先查原单,或以原订单号和完全相同参数重试。 |
| 503 | SERVICE_BUSY | 按 Retry-After: 1 短暂退避后查单或重试原请求。 |
8. 上线检查清单
- 已安全保存商户号和 API Key,并配置服务器时钟同步。
- 已向平台提交全部调用服务器的固定公网出口 IP,确认白名单生效。
- 使用平台单独提供且已授权给当前商户的产品编码,不在代码中猜测或共用他人编码。
- 下单超时或非 2xx 时先查原单,不生成新商户订单号盲目重试。
- 通知入口完成验签、金额核对、数据库幂等和纯文本
success应答。 - 已验证重复通知、乱序通知、成功状态修正、自动重试和自助补发。
- 密钥重置期间可同时验证新旧密钥,并确认新通知立即使用新密钥。