加密货币收银台 · 对接文档

下载文档 (.md)

一站式托管支付网关:你只实现两个 HTTP 接口(一个调用创建订单 + 一个接收我们的 webhook),我们负责派发收款地址、监听链上到账、签名回调、自动归集到你的提现地址。你不需要部署任何东西,也不需要自己跑节点。

支持 8 条主流公链 · USDT + USDC · 带重试 HMAC 签名回调 · 自动归集

1项目概览

这是一个托管式加密支付网关——你接我们的 HTTP API,我们负责生成收款地址、监听链上到账、签名回调、自动把款归集转给你的提现地址。你不需要自己跑节点、不需要管理私钥、不需要部署任何东西。

完整支付路径:

  1. 用户在你网站点「加密货币支付」
  2. 你后端 POST /api/v1/orders(带 uid / amount_usd),我们返回一个 pay_url
  3. 你把用户 302 到 pay_url(我们的收银台页面),用户看到 QR 码和收款地址,用任何钱包扫码付款
  4. 我们后台每分钟扫链,检测到用户付款 → 更新订单状态 → POST 带签名 Webhook 到你注册的 callback_url
  5. 你验签成功后给用户加金币/发货,返回 200 OK
  6. 我们把款从收款地址自动归集转发到你在商户中心填的提现地址
  7. 收银台页面轮询到订单已 paid → 显示「收款成功」,用户点「确定」后跳回你传的 return_url(未传则停留在完成页)
核心好处
0 基础设施:你不部署任何东西,只实现两个 HTTP 接口(一个调用 + 一个接收)
链上到账真相由网关监听:不依赖用户浏览器;用户关掉页面/断网也照样结算
每个 uid 一组永久 HD 派生地址:同用户二次支付回同一地址,便于对账
HMAC 签名 Webhook + 8 次指数退避重试(1m/5m/30m/2h/8h/24h/48h)
支持 8 条主流链:TRON / ETH / BSC / Arbitrum / Polygon / Base / Optimism / Solana,USDT + USDC
自动归集:到账后自动把款从一次性地址转到你的提现地址,你只需看着提现地址余额上涨

2服务端 API 接入

基础信息
基础 URL:https://cryptocashier.shop
鉴权方式:Authorization: Bearer <api_key>(每个请求都要带)
内容类型:Content-Type: application/json
回调鉴权:我们发给你的 POST 带 X-SignatureHMAC-SHA256(callback_secret, raw_body) 十六进制)+ X-Timestamp + X-Nonce

第 1 步 · 注册拿到 api_key 和 callback_secret

/merchant.html 用邮箱密码注册,系统立刻签发两样东西(仅出现一次,务必复制保存):

  • api_key:调用我们 API 时用,放在 Authorization: Bearer header
  • callback_secret:我们回调你时用它做 HMAC 签名,你用它来验签

第 2 步 · 配置回调 URL + 提现地址

登录商户中心,完成三项配置(前两项必做,第三项可选但强烈建议):

配置项必填用途
🔔 回调 URL
callback_url
订单状态变更时我们 POST 到这里(paid / overpaid / partial / expired / forwarded)。例 https://site.com/api/crypto-webhook。接收端实现见 第 4 步
💸 提现地址
(每条启用的链一个)
你自己控制的收款地址。我们检测到订单到账后自动把款从一次性收款地址归集转发到这些地址。你之后只需盯这几个提现地址的余额。没填的链,前端会直接禁用,用户无法在该链上支付。
📮 反馈 URL
feedback_url
用户在收银台点「已支付但没反应」查不到、点「上传反馈」时,我们把 用户上传的钱包地址 + 截图 + 留言 POST 到这里。例 https://site.com/api/crypto-feedback不填则复用上面的回调 URL(反馈和支付事件会打到同一端点,你按 event: "feedback" 分流)。详见 §5 末尾 · 用户反馈接口
小提示:回调接收端可以等你实现完第 4 步再填。提现地址必须在创建订单前填好对应链的。反馈 URL 强烈建议配上——用户卡单时你能第一时间拿到诊断信息,人工跟进发货或退款。

支付完整流程

流程图
用户浏览器                  商户后端                    加密网关                  区块链
    |                         |                           |                         |
    | 选择加密货币支付        |                           |                         |
    |------------------------>|                           |                         |
    |                         | POST /api/v1/orders      |                         |
    |                         |  (api_key, uid, amount)  |                         |
    |                         |------------------------->|                         |
    |                         |                           | 派生 HD 地址           |
    |                         |                           | 写入订单表              |
    |                         |   { pay_url, addresses } |                         |
    |                         |<-------------------------|                         |
    |   302 redirect to pay_url                           |                         |
    |<----------------------  |                          |                         |
    |                                                     |                         |
    |   GET /pay/:id                                      |                         |
    |---------------------------------------------------->|                         |
    |                                                     | 302 to /cashier.html    |
    |<----------------------------------------------------|                         |
    |                                                     |                         |
    |   用户用钱包转 USDT                                 |                         |
    |---------------------------------------------------------------------------->|
    |                                                     |                         |
    |                                                     | 后台每分钟轮询链上      |
    |                                                     |<------------------------|
    |                                                     | 检测到到账              |
    |                                                     | 更新订单状态            |
    |                                       POST webhook (带签名)                   |
    |                         |<-------------------------|                         |
    |                         | 验签 + 加金币            |                         |
    |                         |------>200 OK            |                         |
    |                         |------------------------->|                         |

3创建订单

POST /api/v1/orders

请求

curl
curl -X POST https://cryptocashier.shop/api/v1/orders \
  -H "Authorization: Bearer pk_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "merchant_order_id": "ORD-20260419-001",
    "uid": "USER_12345",
    "amount_usd": "9.99",
    "product": "100 金币",
    "return_url": "https://site.com/order/paid?id=ORD-20260419-001",
    "expires_min": 10
  }'

请求字段

字段必填说明
merchant_order_id你系统里订单 ID,唯一。重复调用同一 ID 返回同一订单(幂等)
uid付款用户 ID(你系统里的 user_id)。同一 uid 永远得到同一组收款地址
amount_usd订单金额,按 1:1 当作 USDT/USDC 金额。字符串类型避免浮点精度
product商品名,仅展示
return_url用户在收银台点「确定」后浏览器跳转的地址(通常是你的"订单完成"页)。不传则停在我们的「支付完成」静态页。详见下方「支付成功后用户去哪儿」
expires_min订单有效期(分钟),默认 10,范围 1-180

响应(HTTP 200)

JSON
{
  "id":                "f6c8a97b-4569-4e92-a8fb-7f15b1c5a840",  // 我们的 UUID
  "merchant_order_id": "ORD-20260419-001",
  "uid":               "USER_12345",
  "amount_usd":        "9.99",
  "amount_received":   "0",                              // 当前已到账累计
  "status":            "pending",                        // pending|partial|paid|overpaid|expired|cancelled
  "currency":          null,                              // 到账后填充(USDT|USDC)
  "chain":             null,                              // 到账后填充(trc20|erc20|…)
  "product":           "100 金币",
  "return_url":        "https://site.com/order/paid?id=ORD-20260419-001",
  "pay_url":           "https://cryptocashier.shop/pay/f6c8...",   // 让用户浏览器打开这个
  "addresses": {
    "evm":  "0x9858effd232b4033e47d90003d41ec34ecaeda94",  // ETH / BSC / Arb / Polygon / Base / OP 共用
    "tron": "TUEZSdKsoDHQMeZwihtdoBiN46zxhGWYdH",
    "sol":  "HAgk14JpMQLgt6rVgv7cBQFJWFto5Dqxi472uT3DKpqk",
    "sol_ata": {                                              // Solana 每个代币的 ATA
      "USDT": "6pXYguUixhfdydHDMgsvSy3sAWjStL2eEYh4KeYJAUDk",
      "USDC": "5N3f1tj9v1vc5TUZ8S7mCAnVmjVKrfnzXWhxLaxyZAgt"
    }
  },
  "created_at":        1776613832602,                   // 毫秒时间戳
  "expires_at":        1776614432602,
  "updated_at":        1776613832602
}

拿到 pay_url 后,用户浏览器跳转过去即可(302 redirect 或 location.href = pay_url)。一般情况下你不需要用 addresses——我们的收银台会根据用户选的链展示对应地址。

错误响应

HTTP说明响应 body
400缺少必填字段 / 金额非法(非数字/≤0/>1,000,000){"error": "missing merchant_order_id"}
401Authorization header 缺失或 api_key 无效{"error": "unauthorized"}
402手续费余额不足(新订单需 fee_balance ≥ 阈值{"error":"insufficient fee balance", "fee_balance":0.5, "required":2, "hint":"去商户中心充值"}
429触发限流(每商户有速率限制){"error":"rate limited"}
幂等:用同一个 merchant_order_id 重复调用会返回相同的订单(不会重复创建,不消耗额外手续费)。网络重试安全。

支付成功后,用户去哪儿?

这块有两条互相独立的链路,职责分清楚就不会搞混:

链路干什么你需要做
后端:Webhook(callback_url 我们的服务器 → 你的服务器。通知"这笔订单到账了",触发你发货 / 加金币 / 改订单状态 实现一个 HTTP 端点接收(见第 5 章)。完全看不见浏览器,用户关不关页都照发。
前端:return_url(浏览器跳转) 用户在收银台看到「收款成功」弹窗 → 点「确定」 → 浏览器跳到 return_url(通常是你的"订单完成页")。 创建订单时把 return_url 传上就行,其余全自动。不传则用户停在我们的「支付完成」静态页(他自己关页 / 返回上一页)。
收银台是如何"消失"的?
你什么都不用做——支付成功那一刻,收银台自己会显示成功弹窗。用户点「确定」后:
① 如果你传了 return_url → 浏览器 location.href = return_url(你的页面替换掉我们的收银台);
② 如果没传 → 用户停在我们的「支付完成」页,关闭或返回由用户自己决定。
用户跳去哪由你决定(就是 return_url 这个字段)。我们不替你关闭商户站的 tab,也不替你做任何商户站业务逻辑。
常见疏漏:如果设置了 return_url 而没实现 webhook,那么:用户关掉收银台前(点确定)你就收不到发货通知,用户刷新/关闭前所有订单都不会发货。反之,只实现了 webhook 而没传 return_url,用户会停在我们的"支付完成"页——可能略尴尬但不影响发货。两条都配才完整。

return_url 设计建议

  • 带上订单 IDhttps://site.com/order/paid?id=ORD-20260419-001。用户落地后你能立即识别是哪笔订单并查状态。
  • 不要把它当发货触发器:用户可能直接关浏览器不点确定;也可能把这个 URL 复制出去分享。发货逻辑永远以 webhook 为准
  • return_url 到达 ≠ 订单已发货:用户点确定时我们只是说"前端看到 paid 状态"。真正"订单完成"是你 webhook handler 返回 200 后的状态。落地页建议再查一次你自己的 DB 显示真实订单状态。
  • iframe 嵌入场景(进阶):如果你把 pay_url 嵌进 iframe,可监听 postMessage 代替 return_url
    window.addEventListener("message", e => { if (e.data?.type === "crypto-payment-close") closeIframe(); })

4查询订单

GET /api/v1/orders/:id(id 是创建订单返回的我们的 UUID)

curl
curl https://cryptocashier.shop/api/v1/orders/f6c8a97b-4569-4e92-a8fb-7f15b1c5a840 \
  -H "Authorization: Bearer pk_YOUR_API_KEY"

响应同上,额外带 payments 数组(该订单所有到账记录)。

什么时候用这个接口?正常情况下你是被动接收我们的 webhook 回调,不需要主动查。但可用于:① 订单列表页显示最新状态;② 回调失败后的兜底查询;③ 客服手动核对。

5接收回调(Webhook)

我们在订单状态变化时 POST 到你注册的 callback_url,通知以下事件:

事件 (event)触发时机含义 / 布尔标志
paid累计收到金额恰好等于订单金额fully_paid=true, overpaid=false, underpaid=false
overpaid累计收到金额 > 订单金额(多付了)fully_paid=true, overpaid=true, underpaid=false
partial收到部分款(累计 < 订单金额)fully_paid=false, overpaid=false, underpaid=true
expired订单有效期(默认 10 分钟)结束仍未足额——包括用户完全没付款的情况。由服务端 cron 每分钟扫描触发,不依赖用户是否打开过收银台fully_paid=false, underpaid=true
forwarded款已从一次性收款地址归集转账到你的提现地址(通常在 paid 之后 30 秒-数分钟内到达)fully_paid=true,带 forward_tx / forwarded_to / token_contract / forward_amount_raw / verify 等链上校验字段 → 详见下方 §独立链上校验
如何用这几个事件?简单策略——收到 paidoverpaid 就直接发货(这时钱已上链确认,归集转账是我们的事);forwarded 只作"入账确认"审计用;partial 记录并等后续到账或 expiredexpired 取消订单。不要等 forwarded 才发货——否则用户等得太久。
⚠ 重要:expired 之后仍可能收到 paid ——这是特性,不是 bug
即使订单已标记 expired,如果用户在过期后 24 小时内把钱真的转到了链上(常见场景:用户超时后才点确认、钱包网络拥堵延迟确认、用户"我先把钱付了再联系客服"),我们的 monitor 仍会扫到并补发 paid 回调。我们这么设计是为了不让用户的钱石沉大海——链上的钱我们一定会识别并归集给你。

你需要做的:收到 paid 就发货,不管订单之前是不是 expired 大致实现:
伪代码
if (event === "paid" || event === "overpaid") {
  if (order.status === "cancelled" || order.status === "expired") {
    order.status = "paid_late";      // 解锁订单,标记为"迟付"便于审计
  }
  if (!order.fulfilled) {
    await grantCredits(order.uid, p.amount_received);  // 照常发货
    await markFulfilled(order);
  }
}
概率不高,但理论存在,简单适配一下可彻底避免"用户付了款但我这边已取消订单不发货"的争议。24 小时后的付款我们不再识别(见 LATE_WINDOW_MS)——超过 24h 要处理,请通过 用户反馈接口走人工流程。

回调请求 Headers

Content-Typeapplication/json
X-SignatureHMAC-SHA256(callback_secret, raw body) — 十六进制小写
X-Signature-Alg固定值 HMAC-SHA256
X-Timestamp毫秒时间戳,等于 payload 里的 ts。建议拒绝 |now - X-Timestamp| > 5 分钟 的请求(防重放)
X-Nonce16 字节十六进制随机串,等于 payload 里的 nonce。你可把近期 nonce 缓存几分钟,拒重复

回调 Payload(paid / overpaid / partial / expired 通用结构)

JSON
{
  "event":             "paid",                    // paid|overpaid|partial|expired|forwarded
  "order_id":          "f6c8a97b-...",           // 我们的 UUID
  "merchant_order_id": "ORD-20260419-001",        // 你的订单 ID
  "uid":               "USER_12345",
  "amount_requested":  "9.99",                   // 订单请求金额(字符串)
  "amount_received":   "9.99",                   // 累计到账(字符串)
  "currency":          "USDT",                   // 最后一笔币种,USDT|USDC
  "chain":             "trc20",                  // 最后一笔所在链,见「支持的链」
  "fully_paid":        true,                    // 累计 ≥ 订单金额
  "overpaid":          false,                   // 累计 > 订单金额
  "underpaid":         false,                   // 累计 < 订单金额
  "latest_tx": {                                     // 触发本次回调的那笔 tx(expired 时为 null)
    "tx":       "0xabc...",
    "chain":    "trc20",
    "currency": "USDT",
    "from":     "TXxxxxxxxxxx",
    "amount":   "9.99"
  },
  "created_at":        1776613832602,            // 订单创建时间(毫秒)
  "expires_at":        1776614432602,            // 订单过期时间(毫秒)
  "ts":                1776613890123,            // 本次回调发送时间(毫秒),= X-Timestamp
  "nonce":             "a1b2c3d4e5f6..."          // 16 字节十六进制,= X-Nonce
}

forwarded 事件的额外字段(含链上校验指令)

当款从一次性收款地址归集到你的提现地址成功后,除通用字段外还会带一整套自包含的链上校验信息。目的是让你不依赖我们的服务直接去链上核对这笔钱真的到了——我们的 webhook 有 HMAC 签名保证来源可信,但最终可信的只有链上数据

JSON · forwarded 完整 payload(额外字段)
{
  "event":              "forwarded",
  "forward_tx":         "0xabc123...",              // 归集 tx hash,用它去链上查
  "forward_from":       "0x175fa1...",              // 派生地址(钱的来源)
  "forwarded_to":       "0xb11272...",              // 你的提现地址(钱的去向)
  "forward_amount":     "0.1",                       // 人类可读金额
  "forward_amount_raw": "100000000000000000",        // 链上真实整数(BigInt 字符串)
  "forward_currency":   "USDT",
  "forward_chain":      "bsc",
  "token_contract":     "0x55d398326f99059ff775485246999027b3197955", // BSC USDT
  "token_decimals":     18,
  "explorer_url":       "https://bscscan.com/tx/0xabc123...",
  "forward_delay_ms":   45000,                      // paid → forwarded 的间隔
  "paid_ts":            1776850000000,
  "forwarded_ts":       1776850045000,
  "verify": {                                         // ← 自包含校验块
    "chain_type": "evm",
    "rpc":    "https://bsc-rpc.publicnode.com",
    "method": "eth_getTransactionReceipt",
    "params": ["0xabc123..."],
    "curl":   "curl -s -X POST ... -d '{...}'",
    "expect": {                                       // 所有字段必须全部匹配
      "result.status":                "0x1",
      "result.logs[i].address":       "0x55d398...",
      "result.logs[i].topics[0]":     "0xddf252ad...",
      "result.logs[i].topics[2]":     "0x00...b11272...",
      "BigInt(result.logs[i].data)": "100000000000000000"
    }
  }
}

🔒 独立链上校验(一行代码,强烈建议高价值商户接入)

为什么要自己校验?我们的 webhook 有 HMAC 签名保证来源可信,但最终可信的只有链上数据。极端情况假设——我们的服务器被攻破、内部人员作恶、bug 误判——攻击者可能伪造一条 forwarded webhook 让你误以为收到钱。链上校验是你的最后防线

一个函数搞定:verifyForwardOnChain

我们提供一个零依赖、Node/浏览器通用、覆盖 8 条链的校验函数。下载一次,复制到你项目里即可:

⬇ 下载 verify-forward.cjscurl -O https://cryptocashier.shop/verify-forward.cjs

Node.js · 复制即用模板(改提现地址即可)
const { verifyForwardOnChain } = require("./verify-forward.cjs");

// ═══════════════════════════════════════════════
// ← 改成你自己的提现地址。没用到的链可以删掉或留空字符串。
// ═══════════════════════════════════════════════
const MY_PAYOUTS = {
  bsc:     "0xYourEVMWallet",                 // 0x 开头 42 字符(大小写无关)
  erc20:   "0xYourEVMWallet",                 // Ethereum 主网
  arb:     "0xYourEVMWallet",                 // Arbitrum
  polygon: "0xYourEVMWallet",                 // Polygon
  base:    "0xYourEVMWallet",                 // Base
  op:      "0xYourEVMWallet",                 // Optimism
  trc20:   "TYourTRONWallet",                 // T 开头 34 字符 Base58
  sol:     "YourSolanaWalletBase58Address",   // 32-44 Base58,无前缀
};

// 在你的 webhook handler 里(已验过 HMAC 签名之后):
if (payload.event === "forwarded") {
  const result = await verifyForwardOnChain({
    payload,                                  // 我们发给你的完整 JSON
    myPayouts: MY_PAYOUTS,                    // ← 按链自动匹配(推荐)
    expectedAmountRaw: order.expected_raw,    // ← 你订单上的应付金额(选填但推荐)
  });

  if (result.ok) {
    await markOrderVerified(order.id);        // 链上已确认 ✓
  } else {
    console.error("归集校验失败:", result.reason);
    await disableCryptoGateway();              // ← 立即禁用我方支付通道
    await alertOps(payload, result.reason);
  }
}

返回 { ok: true/false, reason, onChain? }ok=falsereason 会写清楚哪不对(地址被篡改 / tx 不存在 / tx 回滚 / 金额不对 等)。ESM 项目用 createRequire 桥接加载;浏览器用 <script> 即可。

这只是参考实现。你也可以用自己熟悉的方式验证——web3.js / ethers.js / tronweb / @solana/web3.js / 自己信任的 block explorer 都行。核心三步:payload.forwarded_to 是你自己的地址 · ② tx 链上确认成功 · ③ log 里 token 合约 + 接收方 + 金额 对得上。我们的函数只是把这三步写好了。

💡 30 分钟兜底保护

即使没收到 forwarded,我们内部也会在 30 分钟内把钱打给你。建议在收到 paid 时启一个定时器,30 分钟仍未验证成功则告警并禁用通道。

Webhook 接收端完整示例

你的服务器需要挂一个 HTTP POST 路由来接收我们发的 webhook。下面给一个 Node.js + Express 的完整实现(HMAC 验签 + 幂等去重 + 事件分发 + 入账/发货逻辑骨架);抄走改 CALLBACK_SECRET 和业务函数即可。其他语言(Python / PHP 等)逻辑完全相同,参照下载 integration.md 里的示例。

Node.js + Express

Node.js / Express
import express from "express";
import crypto  from "crypto";

const CALLBACK_SECRET = process.env.CALLBACK_SECRET; // 存在 env 里
const app = express();

// 必须用 raw body,否则 HMAC 算出的签名会因字节差异不一致
app.post("/api/crypto-webhook", express.raw({ type: "application/json" }), async (req, res) => {
  const rawBody = req.body;                                      // Buffer
  const sig = req.headers["x-signature"];
  const ts  = Number(req.headers["x-timestamp"] || 0);

  // 1. 防重放:拒绝时间戳偏差 > 5 分钟的请求
  if (Math.abs(Date.now() - ts) > 5 * 60_000) return res.sendStatus(401);

  // 2. 验签:用 timingSafeEqual 防时序攻击
  const expected = crypto.createHmac("sha256", CALLBACK_SECRET)
    .update(rawBody).digest("hex");
  if (!sig || sig.length !== expected.length ||
      !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.sendStatus(401);
  }

  const p = JSON.parse(rawBody.toString());

  // 3. 幂等:按 merchant_order_id + nonce 去重(同一事件可能重复到达)
  const order = await findOrder(p.merchant_order_id);
  if (!order) return res.sendStatus(404);
  if (order.fulfilled) return res.json({ ok: true, dup: true });

  // 4. 分发:收到 paid/overpaid 就直接发货,不用等 forwarded
  if (p.event === "paid" || p.event === "overpaid") {
    // 注意:订单可能之前被标为 expired/cancelled(用户超时后才付款),
    // 此时仍应发货——链上的钱已真实到账,24h 内的迟付我们照常识别。
    if (order.status === "expired" || order.status === "cancelled") {
      await unCancelOrder(order);      // 解锁,标记为迟付
    }
    await grantCredits(p.uid, p.amount_received);
    await markFulfilled(order);
  } else if (p.event === "partial") {
    // 按比例发货(或仅记录,等后续到账):收到 5/9.99 ≈ 50% → 50% 金币
    const ratio = Number(p.amount_received) / Number(p.amount_requested);
    await grantCredits(p.uid, ratio * order.credits);
  } else if (p.event === "expired") {
    await cancelOrder(order);
  } else if (p.event === "forwarded") {
    // 审计/对账用:款已归集到你的提现地址。业务发货应在 paid 时已完成。
    await recordForwarded(order, p.forward_tx, p.forwarded_to);
  }

  res.json({ ok: true });
});
重要约定:
① 必须在 15 秒内返回 2xx。否则我们判定失败并重试。
② 重试策略:1 min / 5 min / 30 min / 2 h / 8 h / 24 h / 48 h(共 8 次)。48h 后放弃,你可以通过查询接口自行核对。
回调可能重复(网络抖动 / 我方重启)。你必须按 merchant_order_id + 是否已处理做幂等。
partial 事件可能收到多次(用户分期付款)。每次 amount_received 会累加。
paid 事件收到后通常不会再有 overpaid(状态机单向流转),但注意 partialpaid/overpaid 是可能的跳转。

用户反馈接口(可选 · 强烈建议配)

是什么:当用户付了款但在收银台没看到到账状态、点「已支付但没反应」自助查询仍无结果时,会看到「上传反馈」按钮。用户填入付款钱包地址 + 可选截图 + 留言,提交后我们 POST 到你指定的反馈接收端。

为什么重要:用户卡单几乎总是诊断性问题(选错链、给错地址、写错数量、网络延迟)。有反馈接口你能第一时间拿到钱包地址和截图,排查后要么补发、要么退款、要么帮用户正确操作。没有它,用户只能关页消失——你甚至不知道丢了单。

路由

  • 如果你在商户中心配了 feedback_url → POST 到该地址
  • 没配 → POST 到 callback_url(和支付回调同一端点,你按 event: "feedback" 分流处理)

请求

POST <你的 feedback_url 或 callback_url>

Headers 与支付回调完全一致:Content-Type: application/json · X-Signature · X-Signature-Alg · X-Timestamp · X-Nonce。用你的 callback_secret 验签,方式与 §接收回调 Webhook 相同。

Payload

JSON · feedback payload
{
  "event":             "feedback",
  "order_id":          "f6c8a97b-...",            // 我们的 UUID
  "merchant_order_id": "ORD-20260419-001",         // 你的订单 ID
  "uid":               "USER_12345",
  "topic":             "no-response",              // no-response | wrong-network | suggest
  "user_address":      "0xe13428d7...",             // 用户自述支付的钱包地址(suggest 时可能为 null)
  "chain":             "bsc",                      // 用户声称付到的链(wrong-network 下可能 ≠ 订单链)
  "currency":          "USDT",
  "amount_requested":  "0.16",                     // 订单原始金额,方便你定位
  "suggestion":        "我的钱包显示已扣款...",        // 用户输入的文字(topic=suggest 时必填)
  "screenshots":       ["data:image/png;base64,..."],  // 用户上传的截图(base64 data URI,最多 6 张,每张一般 < 1MB)
  "ts":                1745000000000,
  "nonce":             "a1b2c3d4e5f6..."
}

topic 取值

topic触发场景典型字段
no-response用户点「已支付但没反应」,填入钱包地址后服务端扫链仍未找到user_address / chain(= 订单链)/ screenshots
wrong-network用户点「支付时选错了网络」,声明实际转到了哪条链user_address / chain(= 用户实际转的链,可能 ≠ 订单链)/ screenshots
suggest用户主动提交的反馈 / 建议(非卡单相关)suggestion(文字内容)/ screenshots(可选)

响应

返回 15 秒内的 2xx即可。失败会按支付回调同样的 8 次指数退避重试。

最简接收端示例(Node.js)

Node.js / Express
// 若单独配了 feedback_url 就直接挂这个路由;
// 若复用 callback_url,就在原 webhook handler 里按 p.event === "feedback" 分流。
app.post("/api/crypto-feedback", express.raw({ type: "application/json" }), async (req, res) => {
  // 验签流程同支付回调(略),略过后:
  const p = JSON.parse(req.body.toString());
  if (p.event !== "feedback") return res.sendStatus(400);

  // 1. 持久化(base64 截图建议落到对象存储后存 URL,别直接塞 DB)
  const screenshotUrls = [];
  for (const dataUri of (p.screenshots || [])) {
    screenshotUrls.push(await uploadToS3(dataUri));
  }
  await db.insertTicket({
    order_id:     p.merchant_order_id,
    uid:          p.uid,
    topic:        p.topic,
    user_addr:    p.user_address,
    chain:        p.chain,
    note:         p.suggestion,
    screenshots:  screenshotUrls,
    created_at:   p.ts,
  });

  // 2. 通知客服 / 运营(Slack / 邮件 / 站内消息)
  await notifySupport(`用户 ${p.uid} 订单 ${p.merchant_order_id} 反馈:${p.topic}`);

  res.json({ ok: true });
});
处理建议
① 把 base64 截图上传到对象存储(OSS / S3 / R2)后只存 URL,别直接塞 DB——单次反馈 base64 可能 > 1 MB。
② 建工单 / 客服系统,24 小时内人工回复用户(可通过订单关联的邮箱 / uid 反查联系方式)。
③ 常见处理:确认链上有款 → 联系网关管理员提供 tx hash 人工入账;链上无款 → 引导用户重新支付或退款。

6支持的链(chain id)

回调 chain 字段的取值:

chain id协议USDT 合约 / mintUSDC 合约 / mint精度
trc20TRONTRC-20TR7N...j6tTEkx...dz86
erc20EthereumERC-200xdac1...1ec70xa0b8...eb486
solSolanaSPLEs9v...NYBEPjF...Dt1v6
bscBSCBEP-200x55d3...79550x8ac7...580d18
arbArbitrumArbitrum One0xfd08...cbb90xaf88...58316
polygonPolygonPoS0xc213...e8f0x3c49...33596
baseBaseCoinbase L20xfde4...9bb20x8335...29136
opOptimismOP Mainnet0x94b0...8e580x0b2c...ff856
BSC 精度注意:BSC 上 USDT 和 USDC 都是 18 位小数(Binance-Peg 版本),其它链都是 6 位。前端已处理,服务端如果从原始 raw 值解析,注意按 chain 区分精度。

7联调 / 演示模式

联调演示默认完全关闭——只有商户中心的「🎲 一键测试」按钮创建的订单才会启用模拟面板。 真实订单看不到任何演示按钮,上线无需任何改动

唯一开启方式:商户中心的「一键测试」

  1. 登录 /merchant.html
  2. (可选)先在面板「🔔 回调 URL」填上 webhook.site 的临时 URL,可以直接看到我们发来的 webhook JSON
  3. 🎲 生成测试订单并打开收银台
  4. 新窗口打开的收银台底部有 3 个模拟按钮:
  • 模拟足额支付 → 触发 event: "paid" 回调
  • 模拟欠费(少 0.01) → 触发 event: "partial" + 欠费弹框
  • 模拟多付(+0.5) → 触发 event: "overpaid"

测试订单的 merchant_order_idtest_ 开头,数据库 kind='test'。所有模拟数据隔离于真实订单。

🔒 安全设计说明
真实订单(kind='normal')的支付状态只接受后台链上扫描确认,绝不接受来自浏览器前端的"已支付"声明。 即使攻击者知道某真实订单的 ID 并能访问收银台页面,也无法伪造 webhook——所有模拟按钮针对真实订单都是空操作。

这意味着:
  • ✅ 真实订单绝无"白拿商品"风险
  • ✅ 真实订单的 webhook 只会在链上真转账后才触发(延迟 ≤ 1 分钟)
  • ✅ 你的 callback 接收端无需额外校验,只要 HMAC 签名对就可以信任

8上线前检查清单

商户接入侧只需确认以下几项,部署与链监听由网关负责:

  • 商户中心已保存:回调 URL + 提现地址(各链的接收地址,用于自动归集转账)
  • 妥善保存 api_keycallback_secret(两者仅在注册时出现一次)
  • 你的回调接口:HMAC-SHA256 验签通过才处理(用 callback_secret 校验 X-Signature
  • 你的回调接口:按 merchant_order_id / tx 幂等处理(同一事件可能重复到达)
  • 你的回调接口:15 秒内返回 2xx(否则我们判定失败并按 1m/5m/30m/2h/8h/24h/48h 重试,共 8 次)
  • 回调路径走 HTTPS 且公网可达(从我们的服务器能 DNS 解析并建立 TCP 连接)
  • 商户中心「🎲 一键测试」先跑一遍联调,确认 webhook 正确到达、验签通过、发货链路可达
  • 正式上线前用真实链做一笔小额支付(至少 TRON USDT 一笔),确认归集转账到你的提现地址成功

9常见问题

Q:用户付了钱但页面一直没反应?

我们后台每分钟扫链兜底。正常情况下用户付款后 30~90 秒内收银台页面会自动变「收款成功」。若仍未识别,用户可在收银台点「已支付但没反应」输入付款钱包地址主动触发一次服务端核查(1 小时内的转账都能命中);仍查不到可上传截图由你人工跟进。

Q:支付时选错了网络怎么办?

收银台的「支付时选错了网络」自助流程会让用户选实际转的链 + 填地址 + 上传截图。服务端到目标链扫一遍,只要用户转的是 USDT/USDC,我们都能识别并入账,回调中的 chain 会反映实际到账的链。

Q:回调你们重试几次?还不成功怎么办?

失败后按 1 分钟 / 5 分钟 / 30 分钟 / 2 小时 / 8 小时 / 24 小时 / 48 小时指数退避重试,共 8 次。全部失败后放弃并打标记。你可以随时用 GET /api/v1/orders/:id 查询最新状态做兜底核对。

Q:同一订单的 webhook 会重复吗?

会。网络抖动 / 我方重启 / 分期付款都可能造成多次触达。必须幂等:按 merchant_order_id(或 tx)去重。partial → paid 的跳转也可能让同一订单先后发出多个事件。

Q:用户多付了会怎样?

触发 event: "overpaid" 回调,amount_received 是实际到账金额。多付的部分同样会自动归集到你的提现地址,发货不发货由你决定(我们推荐:足额以上均正常发货)。

Q:订单已 expired 之后,又收到了 paid 回调,这是 bug 吗?

不是 bug,是特性。订单过期后 24 小时内,如果用户真的把钱转到了链上(可能超时才点确认、网络拥堵延迟、钱包审核慢等),我们会照常识别并触发 paid 回调——因为链上的钱我们一定要归集给你,不能让用户的钱石沉大海。
你要做的:收到 paid 就发货,哪怕订单之前是 expired。建议在 webhook 分发里加一个"若订单已取消/过期,先解锁再发货"的分支(见 §接收回调 Webhook · 晚到特性)。超过 24h 的付款我们不再识别——此时请走用户反馈接口由客服人工处理。

Q:用户付款后刷新页面 / 关掉页面,会丢单吗?

不会。订单真相完全在服务端 DB + 链上,页面只是显示层。用户重新打开 pay_url(或你通过 merchant_order_id 重新生成订单)都会回到同一个支付状态,且同一 uid 的收款地址保持不变

Q:能自定义支持更多链吗?

能。需联系网关管理员在 server/chains.js 里追加链配置(RPC 池 + 代币合约 + 精度)并重新部署。TRON/EVM 系列加链成本很低;Solana 以外的非 EVM 链需定制扫链逻辑。

Q:归集转账失败怎么办?

监控里会告警。常见原因:目标提现地址格式错 / 某链 gas 费(TRON Energy / EVM native 原生币)不足 / RPC 暂时异常。网关会自动重试并在管理员后台提示;极端情况可人工干预(目前归集失败不会影响你的 webhook 接收,支付已入账的订单依然会正常回调给你)。


文档版本 v2 · 托管式服务端 API 模式 · 有问题联系网关管理员