接入指南

接入指南

CNIXPay 商户接口 V2 接入指南:Bearer 认证、REST 订单接口、四种订单状态、回调事件与验签、错误码和 OpenAPI。

概述

CNIXPay 商户接口 V2 用于在你的服务端创建稳定币收款订单。每笔订单分配独立的收款地址;一笔订单可以同时挂多个「网络 × 币种」收款方式,用户在哪条网络付款都会计入订单。订单状态只有 4 个,只看 status 就知道能不能发货、要不要人工处理。

Base URL:https://api.cnixpay.com/api/v2

网络chain标准token
TRONtronTRC20USDT
BNB Smart ChainbscBEP20USDT / USDC
EthereumethERC20USDT / USDC
PolygonpolygonERC20USDT / USDC
SolanasolanaSPLUSDT / USDC

可用的网络与币种由平台配置,以接口返回为准。

快速开始

  1. 注册商户账号,在商户后台创建应用并提交上线。
  2. 在应用详情中获取 API Key 与 API Secret,配置 https 回调地址,按需配置 IP 白名单。
  3. 在你的服务端调用 创建订单,把返回的收款地址展示给用户,或让用户跳转到 cashierUrl。
  4. 实现回调接口:验签 后按 status 处理,返回 HTTP 2xx。
  5. 用一笔小额订单在真实网络上走通全流程,即可上线。
curl -X POST https://api.cnixpay.com/api/v2/orders \
  -H "Authorization: Bearer $CNIXPAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "merchantOrderNo": "ORDER_10086",
  "amount": "100.00",
  "paymentOptions": [
    { "chain": "tron", "token": "USDT" },
    { "chain": "bsc",  "token": "USDC" }
  ],
  "callbackUrl": "https://shop.example.com/notify"
}'

认证与限流

请求头 Authorization: Bearer <API Key> 传递应用的正式 API Key。API Secret 只用于验证回调签名,不要在请求里传,也不要放进前端代码。

  • V2 只接受正式 Key;带 test_ 前缀的 Key 返回 401。平台不提供模拟沙箱,联调请用小额真实订单。
  • 应用配置了 IP 白名单时,只有白名单内的 IP 可以调用。
  • 限流按应用计,超限返回 HTTP 429,并带 Retry-After(秒),等待后重试即可。
接口限流
创建订单5 次/秒
查询订单、订单列表共享 20 次/秒
追加收款方式10 次/秒
关闭订单5 次/秒

响应格式

响应体固定为 { code, message, data, requestId },code = 0 为成功。HTTP 状态码跟随错误类型(错误码前三位即 HTTP 状态码),失败时 HTTP 状态码和 code 都可以用来判断。

HTTP/1.1 200
X-Request-Id: req_1c9e...

{ "code": 0, "message": "success", "data": { ... }, "requestId": "req_1c9e..." }
HTTP/1.1 400
X-Request-Id: req_8f2a...

{
  "code": 40001,
  "message": "paymentOptions[1].token 不能为空",
  "data": { "field": "paymentOptions[1].token" },
  "requestId": "req_8f2a..."
}
  • requestId:每个请求唯一,同时在响应头 X-Request-Id 里。反馈问题时提供它,可直接定位到那一次请求。
  • message:按请求头 Accept-Language 返回中文或英文,默认中文。
  • 失败时 data 为 null,或只包含 field(出错的字段名)、orderNo(商户订单号重复时已有的订单)。

金额与币种

  • 金额一律是十进制字符串,请求和响应都是,如 "100.50"。请求里传 JSON 数字(100.5)会被拒绝(40002)。做加减或比较请用 Decimal 库,不要转成浮点数。
  • 报价币种 currency:USD(默认),或原生币 TRX / BNB / ETH / POL / SOL。USD 订单按 USDT / USDC 1:1 收款;原生币订单只收该币。
  • USD 金额 2 位小数,原生币 6 位;到账明细 transactions[].amount 按链上精度。
  • USD 按分记账:入账金额取整到分(不足一分的零头不入账),手续费四舍五入到分,因此 feeAmount + netAmount = paidAmount。
  • 同一收款地址上,USD 订单接受 USDT 和 USDC,都按 1:1 计入。付错币(如 USD 订单收到原生币)不计入订单,由平台人工处理。
  • 时间一律是 Unix 秒。

订单对象

查询、列表、创建、追加、关闭的返回和回调都是同一个订单对象,字段齐全。

{
  "orderNo": "20261017105142a1b2c3d4e5f6",
  "merchantOrderNo": "ORDER_10086",
  "status": "paid",
  "closeReason": "",
  "version": 3,
  "amount": "100.00",
  "currency": "USD",
  "paidAmount": "100.00",
  "feeAmount": "0.60",
  "netAmount": "99.40",
  "paymentOptions": [
    { "chain": "tron", "token": "USDT", "address": "TR7NHq...", "amount": "100.00" },
    { "chain": "bsc",  "token": "USDC", "address": "0x55d3...", "amount": "100.00" }
  ],
  "transactions": [
    { "chain": "tron", "token": "USDT", "txHash": "abc123...", "fromAddress": "TUser...",
      "toAddress": "TR7NHq...", "amount": "100.000000", "status": "confirmed", "blockTime": 1760670190 }
  ],
  "txCount": 1,
  "cashierUrl": "https://pay.cnixpay.com/pay/20261017105142a1b2c3d4e5f6",
  "description": "VIP",
  "extra": "user_id=1001",
  "createTime": 1760668600,
  "expireTime": 1760670400,
  "paidTime": 1760670200,
  "updateTime": 1760670200
}
字段说明
status订单状态,见 订单状态
closeReasonstatus 为 closed 时:timeout 超时未付款,merchant 商户关闭;其他为空字符串
version订单版本号,创建时为 1,对外可见的字段每变化一次加 1。回调可能乱序或重试送达,以 version 大的为准
amount / currency应收金额和币种
paidAmount累计已到账(只算已确认的交易)。少付或多付多少,用 amount 与 paidAmount 相减
feeAmount / netAmount平台手续费 / 商户实收(实际进入商户余额的金额)。钱在 paid 或 underpaid 时入账,pending 时都为 0
paymentOptions全部收款方式,amount 为该方式应付的链上资产数量
transactions链上到账明细,最多最近 50 条,可能有多笔、跨多条链。付款人实际用哪条链、哪个币(token 是实际到账的币,可能与下单时选的不同)、付到哪个地址,都以这里为准。status 为 pending(确认中)或 confirmed(已确认,计入 paidAmount)
txCount到账明细总笔数
cashierUrl平台收银台链接
extra下单时透传的内容,原样返回
updateTime最近一次变化的时间,与 version 同时更新

订单状态

只有 4 个状态,除 pending 外正常情况下都是最终结果(唯一例外见下方「人工补单」):

status含义你该做什么
pending还没有结果:待付款、已收到部分款、确认中,以及过期后的宽限期内等待;paidAmount > 0 时可提示用户补足
paid已足额支付(多付也算)发货
underpaid宽限期结束,只收到部分款,已按实收入账到商户余额人工处理:补发部分商品、联系用户补款或退款
closed没收到钱,订单已关闭(closeReason 区分超时和商户关闭)作废订单
pending ──足额到账──▶ paid
   │
   ├──商户关闭(仅未收到付款时)──▶ closed(closeReason = merchant)
   │
   └──宽限期结束仍未付足
        ├──收到过付款──▶ underpaid
        └──没收到钱────▶ closed(closeReason = timeout)
  • 宽限期:订单过了 expireTime 后还有一段宽限期(默认 60 分钟)。付款人在过期前转账但链上到得晚,或在宽限期内补足,订单仍会变为 paid。宽限期内对外仍是 pending,所以你只需要记住:pending 就等待,其他状态就处理。
  • 出结果的时间:宽限期结束后约 10 分钟内给出结果(underpaid 或 closed);如果这时还有链上确认中的到账,会等它确认完成或失败后再给出结果,期间订单保持 pending。
  • 链上确认:确认所需时间由平台按订单金额设定,小额通常几秒到 1 分钟,大额需要更多确认。确认完成前,transactions 里这笔为 pending、不计入 paidAmount,订单仍为 pending。
  • 宽限期结束后才到的钱、商户关闭后才到的钱,不会自动入账,由平台人工处理:退回付款人,或核实后补单。
  • 人工补单是结果唯一可能变化的情况:补单后按实收重新判定,closed 可能变为 paid 或 underpaid,underpaid 可能变为 paid,并推送对应事件。所以已作废的订单也可能收到 order.paid,请按 version 处理,不要直接丢弃。
  • paid 之后又到账的钱(如重复付款)照常入账,状态保持 paid,金额和 version 更新,不再推送回调。
  • paid 是最终结果:之后即使链上发生回滚,订单对你也完全不变,损失由平台承担。
收到回调(或查询订单):
  按请求头 X-Webhook-Id 去重;order.version 小于本地记录的直接忽略
  switch order.status:
    paid       → 发货(paidAmount > amount 时可考虑退还多付部分)
    underpaid  → 人工处理(补发部分商品、联系用户补款或退款)
    closed     → 作废订单
    pending    → 等待(paidAmount > 0 时可提示用户补足 amount − paidAmount)

创建订单

POST/orders

字段类型必填说明
merchantOrderNostring ≤64是商户订单号,应用内唯一
amountstring是应收金额,十进制字符串,大于 0。USD 最多 2 位小数,原生币最多 6 位
currencystring否USD(默认)或 TRX / BNB / ETH / POL / SOL
paymentOptionsarray否收款方式,1–10 条。不传时 USD 订单由收银台让付款人选择,原生币订单自动用该币所在的链
paymentOptions[].chainstring是网络,见 概述
paymentOptions[].tokenstring是USDT / USDC / 原生币符号
expireMinutesinteger否有效期(分钟),5–1440,默认 30
callbackUrlstring ≤500否回调地址,只允许 https,不能是本地或内网地址;不传则用应用配置(也须为 https)
returnUrlstring ≤500否收银台支付完成后跳转,只允许 https
descriptionstring ≤200否订单摘要,收银台展示
extrastring ≤500否透传,查询和回调原样返回

成功返回完整的订单对象(version = 1)。两种用法:

  • 自建收银台:传 paymentOptions,用返回的 paymentOptions[].address 和 amount 渲染支付页。
  • 平台收银台:不传 paymentOptions,把用户跳转到 cashierUrl,由付款人选择网络和币种。

商户订单号重复返回 409 / 40901,并在 data.orderNo 带上已有订单。下单超时、不确定是否成功时,用它查询确认即可,不要换订单号重下。

HTTP/1.1 409

{ "code": 40901, "message": "商户订单号已存在", "data": { "orderNo": "20261017105142a1b2c3d4e5f6" }, "requestId": "req_..." }

查询与列表

接口说明
GET/orders/{orderNo}按系统订单号查询,返回订单对象(含 transactions)。不存在返回 404 / 40401
GET/orders订单列表,返回 { items, total, page, pageSize },列表项是完整的订单对象(含 transactions)
参数说明
merchantOrderNo按商户订单号查(精确),可重复传多个,最多 100 个:?merchantOrderNo=A&merchantOrderNo=B
orderNo按系统订单号查,同上
status订单状态,可重复传多个
createTimeFrom / createTimeTo按创建时间筛选(Unix 秒,左闭右开)
page / pageSize默认 1 / 20,pageSize 最大 100

按创建时间倒序返回。回调兜底:每隔几分钟,把本地还没出结果的订单按 merchantOrderNo 批量查一次(每次最多 100 个),状态变了就按 订单状态 处理。这样即使回调接口宕机、重试用尽,也不会漏发货。

GET https://api.cnixpay.com/api/v2/orders?merchantOrderNo=ORDER_10086&merchantOrderNo=ORDER_10087&pageSize=100
Authorization: Bearer $CNIXPAY_API_KEY

追加收款方式

POST/orders/{orderNo}/payment-options

请求体 { "paymentOptions": [{ "chain": "polygon", "token": "USDC" }] },返回订单对象。

  • 只有 pending、还没收到付款、未过 expireTime 的订单可以追加,否则 409 / 40902。原生币订单不能追加。
  • 已存在的收款方式幂等(不报错、不重复添加);同一网络的不同币种共用一个收款地址。
  • 合计不超过 10 条,只能追加,不能删除。

关闭订单

POST/orders/{orderNo}/close

  • 无请求体。只能关闭还没收到付款的 pending 订单(含已过 expireTime、仍在宽限期内的);已收到付款,或有链上确认中的到账(transactions 里 status 为 pending),返回 409 / 40902。
  • 关闭后 status = closed、closeReason = merchant,之后再到账不会自动入账。
  • 已由商户关闭的订单重复关闭视为成功,直接返回当前订单。关闭由你主动触发,不推送回调。

回调事件

平台这边发生、你不知道的变化会推送到 callbackUrl。请求体是 { id, event, createTime, data },data 是事件发生那一刻的订单对象。

event推送时机订单状态你该做什么
order.partially_paid收到一笔付款但还不够pending可选:提示用户补足
order.paid足额到账,立即推送paid发货
order.underpaid宽限期结束,只收到部分款underpaid人工处理
order.closed宽限期结束,没收到钱closed(timeout)作废订单
POST /notify HTTP/1.1
Content-Type: application/json
X-Webhook-Id: evt_7f3c9a...
X-Webhook-Timestamp: 1760670205
X-Webhook-Signature: v2=5b1f0e...

{
  "id": "evt_7f3c9a...",
  "event": "order.paid",
  "createTime": 1760670200,
  "data": { "orderNo": "20261017105142a1b2c3d4e5f6", "status": "paid", "version": 3, "...": "..." }
}
  • 返回 HTTP 2xx 即视为送达。单次请求超时 5 秒,不跟随重定向(3xx 算失败)。失败后按时间表重试:15 秒、1 分钟、5 分钟、10 分钟、30 分钟、1 小时、2 小时、6 小时、12 小时,共约 22 小时。四种事件都会重试。
  • 订单没有回调地址(下单没传 callbackUrl,应用也没配置)时不推送任何事件。
  • 回调经平台的转发服务发出,来源 IP 不固定:请不要按 IP 白名单限制回调接口,用签名校验来源。
  • 正常情况下每个订单只推送一个结果事件(paid / underpaid / closed),宽限期内补足的只会收到 order.paid;平台人工补单改变结果时会再推送 order.paid 或 order.underpaid。
  • 没收到钱的订单要在宽限期结束后约 10 分钟才收到 order.closed(默认即过期后约 70 分钟);需要更早释放库存,可按 expireTime 自行处理。
  • 重试时请求体不变,可能晚于后续事件送达:按 X-Webhook-Id 去重,按 data.version 取最新,或收到后再查询订单。
  • 商户后台的订单详情可查看每个事件的推送记录,并可重新推送。

回调验签

签名只在请求头,对原始请求体整体签名,不用解析 JSON 就能验证,以后新增字段也不影响验签:

  1. 取请求头 X-Webhook-Timestamp,校验与当前时间相差 5 分钟内(防重放;每次重试都会重新生成)。
  2. 以 API Secret 为密钥,计算 HMAC-SHA256(timestamp + "." + 原始请求体),输出小写十六进制,前面加 v2=。
  3. 与请求头 X-Webhook-Signature 做常量时间比较。
import crypto from 'node:crypto'

// rawBody 必须是原始请求体(Buffer),不要先解析成 JSON 再序列化
export function verifyWebhook(rawBody, headers, apiSecret) {
  const timestamp = headers['x-webhook-timestamp']
  const signature = headers['x-webhook-signature'] ?? ''
  if (!/^\d+$/.test(timestamp ?? '') || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false
  const expected =
    'v2=' + crypto.createHmac('sha256', apiSecret).update(`${timestamp}.`).update(rawBody).digest('hex')
  const a = Buffer.from(expected)
  const b = Buffer.from(signature)
  return a.length === b.length && crypto.timingSafeEqual(a, b)
}

// Express: app.post('/notify', express.raw({ type: 'application/json' }), handler)

很多框架默认会先把请求体解析成 JSON。验签必须用原始字节,例如 Express 用 express.raw(),PHP 用 file_get_contents('php://input')。

错误码

codeHTTP说明
40001400参数缺失或格式错误,data.field 为字段名
40002400金额格式错误:不是字符串、不大于 0 或超出小数位
40003400金额超出该链、该币的单笔上限
40004400链或币种不支持 / 未开放
40005400收款方式数量超限
40101401API Key 缺失或无效(含测试 Key)
40301403请求 IP 不在应用白名单
40302403应用未上线、已停用,或商户账户已禁用
40401404订单不存在
40901409商户订单号已存在(data.orderNo 为已有订单)
40902409订单状态不允许此操作
42901429请求过于频繁,按 Retry-After 重试
50001500系统繁忙,可稍后重试

OpenAPI 与 AI

  • OpenAPI 3.1:https://api.cnixpay.com/api/v2/openapi.json(公开,无需认证)。完整描述接口、字段、枚举、错误码和回调格式,可直接导入 Postman / Apifox,或交给 AI 编程助手生成对接代码。
  • llms.txt:/llms.txt(索引)与 /llms-full.txt(本指南全文的纯文本),方便交给 AI 使用。

最佳实践

  1. 回调必须验签。不要信任任何未通过签名校验的通知。
  2. 回调接口保持幂等。按 X-Webhook-Id 去重,按 version 丢弃旧数据。
  3. 快速返回 2xx。耗时逻辑放进异步队列,避免触发重试。
  4. 关键操作前查询确认。发货前可再调一次查询订单;定时按 merchantOrderNo 批量查询未完结的订单,兜底漏收的回调。
  5. 下单超时不要换单号重下。用同一个 merchantOrderNo 重试,收到 40901 就按 data.orderNo 查询。
  6. 金额使用 Decimal。所有金额都是字符串,避免浮点误差。
  7. 记录 requestId。出错时把它连同时间一起记进日志,排查时提供给我们。