接入文档

完整的支付应用创建、签名、下单、捕获、退款、对账和通知接入流程。

快速开始

XOX Wallet Payment API 允许外部应用使用 XOX 余额完成支付。每个支付应用都有独立的 App IDApp Secret、订单空间和支付通知地址。

完整接入顺序:

  1. 用户登录 XOX Wallet,进入“支付应用”。
  2. 填写应用名称和公网 HTTPS 支付通知 URL,创建支付应用。
  3. 立即保存只显示一次的 App IDApp Secret
  4. 在应用服务端实现 HMAC-SHA256 请求签名和通知验签。
  5. 服务端调用创建支付接口,取得 paymentUrl
  6. 浏览器跳转到 paymentUrl,付款用户使用 XOXConnect 登录并输入支付密码。
  7. 钱包将订单置为 AUTHORIZED,通过浏览器返回参数和支付通知返回一次性授权码。
  8. 应用服务端调用捕获接口,订单进入 CAPTURED,支付完成。
  9. 后续可按应用查询、关闭、退款和对账。

基础地址示例:

API Base URL

https://wallet.xoxmax.com/v1/payment-api

钱包支付页

https://wallet.xoxmax.com/pay/{publicToken}

App Secret 只能保存在服务端。禁止将它放入浏览器代码、移动端安装包、日志、错误响应或公开代码仓库。

应用与凭据

创建支付应用

进入 XOX Wallet 的“支付应用”,点击“创建支付应用”,填写:

字段是否必填说明
应用名称用于付款页和后台识别接入应用
支付通知 URL接收支付状态通知的公网 HTTPS 地址

创建成功后会立即显示:

凭据示例用途
App IDxwa_xxxxxxxxx请求头 X-App-Id 和通知来源标识
App Secretxwa_secret_xxxxxxxxx请求签名和支付通知验签

App Secret 仅在创建和重置时完整显示一次。遗失后必须在应用详情中重置,旧密钥会立即失效。

应用状态

状态API 行为
启用可以创建、查询、捕获、关闭、退款和对账
用户停用所有 Payment API 请求返回应用不可用
平台封禁所有 Payment API 请求返回应用不可用,用户无法自行解除
已删除App Secret 和通知立即失效,应用不再显示

应用存在 CREATEDAUTHORIZED 未结订单时不能删除。

请求鉴权

所有 /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 必须转为大写,例如 POSTGET
  • 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_noxox_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

字段类型必填约束
outTradeNostring当前应用内唯一,最长 64 字符
subjectstring商品或订单说明,最长 127 字符
amountstring正数,两位小数,例如 12.50
returnUrlstring支付完成后的 HTTPS 返回地址
expiresInSecondsinteger60~1800,默认 900
payeeobject指定收款 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"
}
字段类型必填说明
refundNostring当前应用内唯一的退款单号
amountstring退款金额,不得超过可退余额
reasonstring退款原因
idempotencyKeystring退款幂等键

支持多次部分退款;全部可退金额退完后订单进入 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

处理要求:

  1. 先读取原始请求体并完成签名验证。
  2. X-Event-Id 作为幂等键,重复通知只处理一次。
  3. 校验 X-App-Id、订单号、金额和预期业务状态。
  4. 业务处理成功后返回任意 2xx
  5. 2xx 或网络错误会触发指数退避重试,最长间隔一小时。

通知验签失败时必须拒绝请求,不能根据通知内容更新业务订单。

错误与幂等

所有响应使用统一结构:

{
  "error": {
    "code": "invalid_signature",
    "message": "签名验证失败",
    "request_id": "请求追踪 ID"
  }
}

常见错误:

HTTPcode说明
400invalid_jsonJSON 请求体无效
400order_conflict相同订单号对应的金额或说明不一致
400payment_not_authorized订单尚未授权,不能捕获
400refund_exceeds_payment退款金额超过可退余额
403invalid_appApp ID 不存在
403payment_application_unavailable应用已停用、删除或被平台封禁
403invalid_signature请求签名错误
403signature_expired请求时间戳超出允许窗口
403replayed_requestNonce 已使用
403invalid_authorization_code一次性授权码无效
404payment_not_found当前应用下不存在该订单

幂等建议:

  • outTradeNo 是创建支付的应用级幂等键。
  • 捕获和退款必须提供稳定的 idempotencyKey,网络重试时复用原值。
  • refundNo 是退款业务单号,同一应用内不可重复用于不同金额。
  • 收到未知结果时先调用查询接口,不要直接创建新订单或退款单。