快速开始
XOX Wallet Payment API 允许外部应用使用 XOX 余额完成支付。每个支付应用都有独立的 App ID、App Secret、订单空间和支付通知地址。
完整接入顺序:
- 用户登录 XOX Wallet,进入“支付应用”。
- 填写应用名称和公网 HTTPS 支付通知 URL,创建支付应用。
- 立即保存只显示一次的
App ID和App Secret。 - 在应用服务端实现 HMAC-SHA256 请求签名和通知验签。
- 服务端调用创建支付接口,取得
paymentUrl。 - 浏览器跳转到
paymentUrl,付款用户使用 XOXConnect 登录并输入支付密码。 - 钱包将订单置为
AUTHORIZED,通过浏览器返回参数和支付通知返回一次性授权码。 - 应用服务端调用捕获接口,订单进入
CAPTURED,支付完成。 - 后续可按应用查询、关闭、退款和对账。
基础地址示例:
API Base URL
钱包支付页
App Secret 只能保存在服务端。禁止将它放入浏览器代码、移动端安装包、日志、错误响应或公开代码仓库。
应用与凭据
创建支付应用
进入 XOX Wallet 的“支付应用”,点击“创建支付应用”,填写:
| 字段 | 是否必填 | 说明 |
|---|---|---|
| 应用名称 | 是 | 用于付款页和后台识别接入应用 |
| 支付通知 URL | 是 | 接收支付状态通知的公网 HTTPS 地址 |
创建成功后会立即显示:
| 凭据 | 示例 | 用途 |
|---|---|---|
| App ID | xwa_xxxxxxxxx | 请求头 X-App-Id 和通知来源标识 |
| App Secret | xwa_secret_xxxxxxxxx | 请求签名和支付通知验签 |
App Secret 仅在创建和重置时完整显示一次。遗失后必须在应用详情中重置,旧密钥会立即失效。
应用状态
| 状态 | API 行为 |
|---|---|
| 启用 | 可以创建、查询、捕获、关闭、退款和对账 |
| 用户停用 | 所有 Payment API 请求返回应用不可用 |
| 平台封禁 | 所有 Payment API 请求返回应用不可用,用户无法自行解除 |
| 已删除 | App Secret 和通知立即失效,应用不再显示 |
应用存在 CREATED 或 AUTHORIZED 未结订单时不能删除。
请求鉴权
所有 /v1/payment-api/** 请求必须携带以下请求头:
X-App-Id: xwa_xxxxxxxxx
X-Timestamp: 1787800000
X-Nonce: 8b5684f8-8df5-4cab-8fd2-377359a1ed32
X-Signature: Base64URL(HMAC-SHA256(...))
签名原文
METHOD\nPATH\nTIMESTAMP\nNONCE\nSHA256_HEX(RAW_BODY)
规则:
METHOD必须转为大写,例如POST、GET。PATH只包含原始请求路径,不包含域名和查询参数。TIMESTAMP是 Unix 秒时间戳,只接受当前时间前后五分钟。NONCE每个 App ID 下不可重复,最长 128 字符。RAW_BODY必须使用实际发送的 UTF-8 请求体字节;GET 或无请求体时使用空字符串。SHA256_HEX使用小写十六进制。- HMAC 结果使用无填充的 Base64URL 编码。
Node.js 签名示例
import { createHash, createHmac, randomUUID } from "node:crypto"
export function signWalletRequest(
appSecret: string,
method: string,
path: string,
rawBody = ""
) {
const timestamp = Math.floor(Date.now() / 1000).toString()
const nonce = randomUUID()
const bodyHash = createHash("sha256").update(rawBody, "utf8").digest("hex")
const canonical = [method.toUpperCase(), path, timestamp, nonce, bodyHash].join("\n")
const signature = createHmac("sha256", appSecret)
.update(canonical, "utf8")
.digest("base64url")
return { timestamp, nonce, signature }
}
发起请求示例
const path = "/v1/payment-api/payments"
const body = JSON.stringify({
outTradeNo: "ORDER-20260827-001",
subject: "团队专业版",
amount: "12.50",
returnUrl: "https://example.com/payment/result",
expiresInSeconds: 900,
})
const auth = signWalletRequest(process.env.XOX_APP_SECRET!, "POST", path, body)
const response = await fetch(`https://wallet.xoxmax.com${path}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-App-Id": process.env.XOX_APP_ID!,
"X-Timestamp": auth.timestamp,
"X-Nonce": auth.nonce,
"X-Signature": auth.signature,
},
body,
})
const result = await response.json()
完整支付流程
1. 创建支付
应用服务端创建支付,取得钱包支付地址。
POST /v1/payment-api/payments
Content-Type: application/json
{
"outTradeNo": "ORDER-20260827-001",
"subject": "团队专业版",
"amount": "12.50",
"returnUrl": "https://example.com/payment/result",
"expiresInSeconds": 900,
"payee": {
"issuer": "https://connect.example.com",
"subject": "收款人的 XOXConnect sub",
"displayName": "收款商户",
"email": "merchant@example.com"
}
}
{
"data": {
"outTradeNo": "ORDER-20260827-001",
"publicToken": "ff7e15e6-4f35-4666-9309-76a7a8070e4b",
"paymentUrl": "https://wallet.xoxmax.com/pay/ff7e15e6-4f35-4666-9309-76a7a8070e4b",
"appId": "xwa_xxxxxxxxx",
"appName": "官网商城",
"subject": "团队专业版",
"amount": "12.50",
"payableAmount": "11.25",
"discountAmount": "1.25",
"rebateAmount": "0.56",
"status": "CREATED",
"returnUrl": "https://example.com/payment/result",
"expiresAt": "2026-08-27T13:00:00Z",
"authorizedAt": null,
"capturedAt": null,
"refundedAmount": "0.00"
}
}
amount 是应用提交的原始订单金额,payableAmount 是钱包用户实际支付金额。活动开启时,discountAmount 会直接减少冻结和扣款金额,商户仍按原始订单金额收款;rebateAmount 在订单捕获成功后真实发放到付款钱包。优惠规则在订单创建时固化,后续后台调整不会改变已创建订单。
2. 跳转钱包确认
将用户浏览器跳转到 paymentUrl。付款用户在钱包完成 XOXConnect 登录并输入六位支付密码。钱包会冻结订单金额,订单进入 AUTHORIZED。
3. 获取一次性授权码
钱包通过两种方式返回授权码:
- 浏览器跳回
returnUrl,并附加xox_order_no和xox_authorization_code查询参数。 - 向应用的支付通知 URL 发送
PAYMENT_AUTHORIZED,消息中包含authorizationCode。
授权码仅用于对应订单的捕获,应用服务端应立即使用且不得写入前端日志。
4. 捕获支付
POST /v1/payment-api/payments/ORDER-20260827-001/capture
Content-Type: application/json
{
"authorizationCode": "一次性授权码",
"idempotencyKey": "CAPTURE-ORDER-20260827-001"
}
捕获成功后,提供 payee 的订单会把原始订单金额转入收款人的钱包可用余额;未提供 payee 时转入应用收入账户。限时折扣差额由钱包活动账户补足,支付返利和达到门槛的累计返利会自动进入付款钱包。订单进入 CAPTURED,相同 idempotencyKey 的重试不会重复扣款或重复返利。退款会按原始退款金额从收款账户扣回,并按活动折扣比例分别退回付款钱包和活动账户。
5. 处理结果
- 以服务端查询或签名通知为最终支付结果,不要只依赖浏览器跳转。
- 应用收到
PAYMENT_CAPTURED后更新自己的业务订单。 - 超时未捕获的授权订单会进入
EXPIRED并释放冻结余额。
API 端点
端点总览
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | /v1/payment-api/payments | 创建支付 |
| GET | /v1/payment-api/payments/{outTradeNo} | 查询支付 |
| POST | /v1/payment-api/payments/{outTradeNo}/capture | 捕获已授权支付 |
| POST | /v1/payment-api/payments/{outTradeNo}/close | 关闭未完成支付 |
| POST | /v1/payment-api/payments/{outTradeNo}/refunds | 部分或全额退款 |
| POST | /v1/payment-api/reconciliation | 应用维度对账 |
创建支付
POST /v1/payment-api/payments
| 字段 | 类型 | 必填 | 约束 |
|---|---|---|---|
| outTradeNo | string | 是 | 当前应用内唯一,最长 64 字符 |
| subject | string | 是 | 商品或订单说明,最长 127 字符 |
| amount | string | 是 | 正数,两位小数,例如 12.50 |
| returnUrl | string | 否 | 支付完成后的 HTTPS 返回地址 |
| expiresInSeconds | integer | 否 | 60~1800,默认 900 |
| payee | object | 否 | 指定收款 XOXConnect 身份;提供后捕获资金直接进入其钱包可用余额 |
订单号只能包含字母、数字、点、下划线、冒号和连字符。同一个应用使用相同订单号和相同金额、说明重试时返回原订单;参数不一致会返回冲突错误。
查询支付
GET /v1/payment-api/payments/{outTradeNo}
返回当前应用下订单的金额、状态、支付地址、时间和累计退款金额。应用不能查询其他应用的订单。
捕获支付
POST /v1/payment-api/payments/{outTradeNo}/capture
{
"authorizationCode": "一次性授权码",
"idempotencyKey": "CAPTURE-001"
}
只有 AUTHORIZED 状态可捕获。授权码错误、过期或属于其他订单时拒绝请求。
关闭支付
POST /v1/payment-api/payments/{outTradeNo}/close
请求体为空。CREATED 订单直接关闭;AUTHORIZED 订单关闭时释放冻结余额;已捕获订单不能关闭。
退款
POST /v1/payment-api/payments/{outTradeNo}/refunds
{
"refundNo": "REFUND-20260827-001",
"amount": "2.50",
"reason": "部分退款",
"idempotencyKey": "REFUND-IDEMPOTENCY-001"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| refundNo | string | 是 | 当前应用内唯一的退款单号 |
| amount | string | 是 | 退款金额,不得超过可退余额 |
| reason | string | 否 | 退款原因 |
| idempotencyKey | string | 是 | 退款幂等键 |
支持多次部分退款;全部可退金额退完后订单进入 REFUNDED。
对账
POST /v1/payment-api/reconciliation
{
"from": "2026-08-01T00:00:00Z",
"to": "2026-08-27T00:00:00Z"
}
单次时间窗不能超过 31 天。结果只包含当前支付应用的捕获订单和成功退款,包括笔数、总金额和明细。
支付状态
| 状态 | 说明 | 可执行操作 |
|---|---|---|
| CREATED | 已创建,等待用户确认 | 查询、关闭 |
| AUTHORIZED | 用户已确认,金额已冻结 | 查询、捕获、关闭 |
| CAPTURED | 已捕获,支付完成 | 查询、退款 |
| CLOSED | 已关闭 | 查询 |
| EXPIRED | 已过期,冻结金额已释放 | 查询 |
| REFUNDED | 已全额退款 | 查询 |
支付通知
钱包向应用创建时配置的支付通知 URL 发送 POST application/json 请求。
通知请求头:
X-App-Id
X-Event-Id
X-Event-Type
X-Timestamp
X-Nonce
X-Signature
通知使用当前 App Secret 和与 API 请求相同的 HMAC-SHA256 规范签名。验签时 PATH 使用通知 URL 的路径部分,RAW_BODY 使用收到的原始 JSON 字节。
事件类型
| X-Event-Type | 触发时机 | 重要字段 |
|---|---|---|
| PAYMENT_AUTHORIZED | 用户确认并冻结金额 | outTradeNo、authorizationCode、amount、status |
| PAYMENT_CAPTURED | 应用捕获成功 | outTradeNo、amount、status |
| PAYMENT_CLOSED | 用户或应用关闭订单 | outTradeNo、status |
| PAYMENT_EXPIRED | 支付超时 | outTradeNo、status |
| PAYMENT_REFUNDED | 退款成功 | outTradeNo、refundNo、amount、status |
处理要求:
- 先读取原始请求体并完成签名验证。
- 以
X-Event-Id作为幂等键,重复通知只处理一次。 - 校验
X-App-Id、订单号、金额和预期业务状态。 - 业务处理成功后返回任意
2xx。 - 非
2xx或网络错误会触发指数退避重试,最长间隔一小时。
通知验签失败时必须拒绝请求,不能根据通知内容更新业务订单。
错误与幂等
所有响应使用统一结构:
{
"error": {
"code": "invalid_signature",
"message": "签名验证失败",
"request_id": "请求追踪 ID"
}
}
常见错误:
| HTTP | code | 说明 |
|---|---|---|
| 400 | invalid_json | JSON 请求体无效 |
| 400 | order_conflict | 相同订单号对应的金额或说明不一致 |
| 400 | payment_not_authorized | 订单尚未授权,不能捕获 |
| 400 | refund_exceeds_payment | 退款金额超过可退余额 |
| 403 | invalid_app | App ID 不存在 |
| 403 | payment_application_unavailable | 应用已停用、删除或被平台封禁 |
| 403 | invalid_signature | 请求签名错误 |
| 403 | signature_expired | 请求时间戳超出允许窗口 |
| 403 | replayed_request | Nonce 已使用 |
| 403 | invalid_authorization_code | 一次性授权码无效 |
| 404 | payment_not_found | 当前应用下不存在该订单 |
幂等建议:
outTradeNo是创建支付的应用级幂等键。- 捕获和退款必须提供稳定的
idempotencyKey,网络重试时复用原值。 refundNo是退款业务单号,同一应用内不可重复用于不同金额。- 收到未知结果时先调用查询接口,不要直接创建新订单或退款单。