收银台 Demo/商户 API 对接

商户 API 对接指南

说明如何通过 XMan 开放接口创建订单、查询账号与流水、发起转账,并接收带签名的异步通知。完整契约以仓库内 services/api/docs/merchant-open-api.md 为准;订单 notify 字段细节另见 services/api/docs/orders-and-notify.md

概览

  • GET /health 外,开放接口路径均带前缀 /api
  • 鉴权 Header:x-api-key(商户后台 apiKey);写操作与验签使用 secretKey(勿暴露到浏览器)
  • 创建订单、发起转账均需 HMAC-SHA256 签名(sign);异步通知由平台签名、商户验签
  • 列表类接口(账号、流水、转账)分页响应为 { items, total, page, limit }
  • 本 Demo:收银台 /、转账 /transfer、notify 接收 POST /api/checkout/notify(端口默认 9005)

API 索引

共 13 个开放接口;本地联调基址默认 test-frontend.xman.com

方法路径权限
GET/health
GET/api/devicesdevices:list
GET/PATCH/api/accounts…accounts:*
POST/GET/api/orders…orders:*
GET/api/transactions…transactions:*
GET/POST/api/transfers…transfers:*

对接流程

  1. 在 Admin 商户详情获取 apiKeysk_api_…)、secretKeysk_sign_…),配置默认 notifyUrl(HTTPS)。系统 Webhook 验签密钥为 sk_hook_…(Admin → Webhooks)。
  2. POST /api/orders 创建订单(带 sign),保存响应中的 id(UUID)、orderId(商户 15 位)、platformOrderId(平台 20 位)、targetAccountreconcileAmount。不浮动金额下同付款账号已有同额待付单时返回 409,用响应 data 继续原单支付。
  3. 引导用户向 targetAccount 转入正确金额(收银台展示 reconcileAmount ?? amount)。
  4. 平台定时拉银行流水对账;匹配成功后订单为 success
  5. 平台 POST 你的 notifyUrl;验签后处理业务,响应体为纯文本 success(非 JSON 字符串)。
  6. 也可用 GET /api/orders/:id 主动查询(注意限频)。

订单 expired 时不会发送 notify。

鉴权

每个请求携带:

Header
x-api-key: <商户 apiKey,如 sk_api_...>

无效或缺失返回 401 Unauthorized

另需商户组勾选接口权限,例如 orders:createorders:readtransfers:create 等;未授权返回 403

创建订单

POST /api/orders · Content-Type: application/json · 成功 201

字段必填说明
orderId15 位数字:UTC YYYYMMDD + 7 位序号;同商户唯一
fromAccount付款账号
amount名义金额,> 0
title订单标题
sign见「签名算法」
notifyUrl本单通知地址;与后台默认二选一,不能都为空
returnUrl浏览器跳转(不参与 notify 签名)
app渠道如 mymo;可选渠道筛选
param扩展 JSON,通知中原样回传

响应 data idorderIdplatformOrderIdtargetAccountreconcileAmountexpiresAtreconcileUntilAtstatus 等。

不浮动金额(base_mode=none)时,同渠道、收款户、付款账号下若已有相同应付金额的待处理订单,返回 409,body 含 error: active_reconcile_order_exists data(前单详情,字段与 201 一致),可据此引导用户继续支付原单。

成功 201 响应示例
{
  "ok": true,
  "data": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "merchantId": "mch_xxx",
    "orderId": "202601011234567",
    "platformOrderId": "202601011234560012",
    "fromAccount": "1234567890",
    "amount": 100,
    "currency": "THB",
    "reconcileAmount": 100,
    "title": "商品充值",
    "targetAccount": "020474502919",
    "app": "mymo",
    "status": "pending",
    "expiresAt": "2026-01-01T12:10:00.000Z",
    "reconcileUntilAt": "2026-01-01T12:11:40.000Z"
  }
}
同额待付冲突 409 响应示例
{
  "ok": false,
  "error": "active_reconcile_order_exists",
  "message": "同收款配置下已存在相同应付金额的待处理订单",
  "data": {
    "id": "660e8400-e29b-41d4-a716-446655440001",
    "orderId": "202601011234500",
    "fromAccount": "1234567890",
    "amount": 100,
    "currency": "THB",
    "reconcileAmount": 100,
    "targetAccount": "020474502919",
    "status": "pending",
    "expiresAt": "2026-01-01T12:10:00.000Z"
  }
}

收到 409 且 error=active_reconcile_order_exists 时,应使用 data.id(publicId)引导用户进入原收银台,勿重复创建。

curl 示例
curl -sS -X POST "test-frontend.xman.com/api/orders" \
  -H "x-api-key: sk_api_xxx" \
  -H "content-type: application/json" \
  -d '{
    "orderId": "202601011234567",
    "fromAccount": "1234567890",
    "amount": 100,
    "title": "商品充值",
    "sign": "<按下文算法计算的小写 hex>",
    "notifyUrl": "https://merchant.example.com/pay/notify",
    "param": { "userId": "u1" }
  }'

业务账号

GET /api/accounts — 分页列表,Query 可选 appenablecitizenIdaccountpagelimit。响应字段含 citizenIdaccountenable(无对外 status)。

GET /api/accounts/:id PATCH /api/accounts/:id(body 仅 enable)— 权限分别为 accounts:read / accounts:write

流水

GET /api/transactions — Query 中 appaccount 均可选;分页 page / limit

GET /api/transactions/:id — 路径为列表 items[].id

发起转账

POST /api/transfers · Content-Type: application/json · 成功 201 · 权限 transfers:create

字段必填参与签名说明
sign见「签名算法」
app非空时渠道;不传则在 gateway 已启用设备中自动选号
accountTo收款账号
amount转账金额(THB),> 0
type默认 bank;未传时签名按 bank
typeCodebank 时必填非空时银行码(如 GSB 为 30)
citizenId非空时付款方证件号;与 account 可指定出款账号
account业务出款账号;须与 citizenId 等在可用账号中唯一匹配
timeoutMs非空时gateway 超时毫秒

成功 data idcitizenIdaccounttransRefCode、出收款账号等。gateway 失败返回 502,body 可含 data.phase(prePost/post)。

本仓库转账 Demo:/transfer → BFF POST /api/transfer(服务端算 sign,body 可含未参与签名的 account)。

curl 示例
curl -sS -X POST "test-frontend.xman.com/api/transfers" \
  -H "x-api-key: sk_api_xxx" \
  -H "content-type: application/json" \
  -d '{
    "accountTo": "1234567890",
    "amount": 1,
    "type": "bank",
    "typeCode": "30",
    "sign": "<按下文算法计算的小写 hex>"
  }'

查询订单

GET /api/orders/:id :id 为创建返回的 UUID(data.id)。

GET /api/orders/:id/callback-logs — 查看每次 notify 投递记录(排障用)。

创建与查询共用 min_interval_ms 限频(默认约 1s),过频返回 429。

异步通知(notifyUrl)

订单进入终态且非 expired 后,平台向 notifyUrl 发起:

请求体示例
{
  "orderId": "202601011234567",
  "fromAccount": "1234567890",
  "platformOrderId": "202601011234560012",
  "status": "success",
  "amount": "100",
  "title": "商品充值",
  "targetAccount": "020474502919",
  "timestamp": "1735689600000",
  "sign": "<平台 HMAC,小写 hex>"
}

商户必须:验签通过后处理业务;HTTP 2xx 且响应 body trim 后等于纯文本 success,平台才停止重试。

重试间隔约 5s / 30s / 120s;用尽后平台侧 callbackStatus 为 failed。通知中 amount字符串,验签须按字符串参与。

验签:对实际收到的 JSON 去掉 sign 后,仅对其中非空字段按下方 buildSign 规则计算(空值键平台不会 POST,例如无扩展参数时不含 param)。可选字段 reconcileAmountplatformOrderId 有则参与;不含 appOrderNo

签名算法

创建订单、发起转账与异步通知使用同一套 buildSign 规则:

  1. sign 外,取 value 非 null 且非空字符串的字段
  2. 键名按字典序排序
  3. 拼接 key=signFieldValue(value),用 & 连接
  4. HMAC-SHA256(secretKey, UTF-8),输出 hex 小写

创建订单参与:orderId、fromAccount、amount、title,及非空的 app、returnUrl、notifyUrl、param(param 用 JSON.stringify)。
发起转账参与:accountTo、amount、type(未传按 bank)、及非空的 app、typeCode、citizenId、timeoutMs;不含 account
异步通知参与:以平台 POST 的 JSON 为准,除 sign 外所有非空字段(见「异步通知」一节)。

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

function signFieldValue(v) {
  if (v === null || v === undefined) return "";
  if (typeof v === "object") return JSON.stringify(v);
  if (typeof v === "string") return v;
  if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint")
    return String(v);
  if (typeof v === "symbol") return v.toString();
  return "";
}

function buildSign(payload, secretKey) {
  const str = Object.keys(payload)
    .filter((k) => k !== "sign" && payload[k] != null && payload[k] !== "")
    .sort()
    .map((k) => `${k}=${signFieldValue(payload[k])}`)
    .join("&");
  return createHmac("sha256", secretKey).update(str).digest("hex");
}

状态说明

status含义
pending待付款 / 待对账
processing对账处理中
success流水匹配成功
failed失败终态
expired超时;不发 notify
anomaly对账前置条件异常等

常见错误

  • 400 — 参数、sign(missing/invalid sign)、notifyUrl 校验失败
  • 401 — apiKey 无效
  • 409 — orderId(商户单号)重复;或 active_reconcile_order_exists (不浮动金额下同付款账号已有同额待付单,响应含前单 data)
  • 429 — 创建/查询过于频繁

notifyUrl 须为 HTTPS,且通过平台 SSRF 校验(生产勿关闭校验)。

联调清单

  • 后台配置 notifyUrl(HTTPS)与 secretKey
  • 商户组开通 orders / accounts / transactions / transfers 等权限
  • 实现 buildSign / 验签,创建订单与 POST /api/transfers 均带 sign
  • 收银台或自有页面展示 targetAccount 与 reconcileAmount
  • notify 接口幂等处理,验签后返回纯文本 success
  • 用 GET /api/orders/:id 与 callback-logs 排障

环境变量 NEXT_PUBLIC_XCORE_API_BASE 可配置文档中的 API 根地址(当前展示:test-frontend.xman.com)。