WEBHOOKS / PAYLOAD

Payload 与验签

派付通过 HTTPS POST 把链上事件主动推送到商户配置的回调地址。所有回调使用与商户调派付 API 完全一致的 HMAC-SHA256 签名算法——只是角色互换,这次派付当客户端、商户当服务端。

回调地址在管理后台配置

回调 endpoint 地址在派付管理后台按 bizType(RECHARGE / WITHDRAW / COLLECT / CHAIN_EVENT 等)分别配置,不通过 API 设置。

ACK 约定

派付判断本次回调是否成功的规则:

  • 商户 endpoint 返回 HTTP 200 且响应 body 包含 {"code":0} 或字符串 "success" / "ok" → 视为 ACK 成功,不再重试。
  • 其它情况(非 200、超时、body 不含上述标识)→ 标记 FAILED,进入指数退避重试。

重试策略

回调失败后按以下公式计算下次重试时间:

nextRetryTime = now + 60 × 2^retryTimes 秒,最多重试 10 次

10 次全部失败后任务状态变为 FAILED,可通过 重发回调接口 手工触发重发,或在管理后台操作。

验签算法

派付推送给商户的回调请求带有 4 个 HMAC header: X-Api-Key / X-Timestamp(UNIX 毫秒 13 位)/ X-Nonce / X-Signature。 签名串构造与商户调派付 API 完全一致:

stringToSign = "POST" + "\n"
             + 商户回调 path + "\n"
             + X-Timestamp + "\n"
             + X-Nonce + "\n"
             + SHA256_hex(rawBody)

X-Signature = HMAC-SHA256(apiSecret, stringToSign) 小写 hex

必须用 rawBody 做 HMAC,不能 parse 后 stringify

验签对象是 HTTP 请求的原始字节。经过 JSON parse → stringify 会丢失空白字符,导致签名永远不匹配。 必须从 InputStream / raw buffer 直接读字节做 HMAC 比对。

Payload 示例

充值回调(event: recharge)

// 充值回调 payload
{
  "event": "recharge",
  "id": 9001,
  "applicationId": 1024,
  "chainId": 11,
  "chainTokenId": 50,
  "address": "0xab12...",
  "amount": "100.00",
  "txHash": "0xabc...",
  "blockHeight": 19384712,
  "txStatus": "SETTLED",
  "confirmations": -1,
  "minConfirmations": -1
}

充值回调字段说明

event
string required
固定值 "recharge"
例: recharge
id
integer required
充值记录 id,可用于调 GET /api/v1/wallet/recharge/get?id=9001 查详情。
例: 9001
applicationId
integer required
所属应用 id。
例: 1024
chainId
integer required
链内部 id(非 EVM chainId)。
例: 11
chainTokenId
integer required
代币 id,与 /support/tokens 返回的 chainTokenId 对应。
例: 50
address
string required
收款地址(用户充值地址)。
例: 0xab12...
amount
string required
到账金额,字符串格式,已考虑 decimals。
例: "100.00"
txHash
string required
链上交易哈希。
例: 0xabc...
blockHeight
integer required
所在区块高度。
例: 19384712
txStatus
string required
充值生命周期状态:PENDING / CONFIRMED / SETTLED / FAILED
例: SETTLED
confirmations
integer required
当前确认数;SETTLED 事件固定为 -1
例: 20
minConfirmations
integer required
最小确认数;SETTLED 事件固定为 -1
例: 12

业务用户入账只看 RECHARGE 生命周期

RECHARGE.CONFIRMED 表示链上充值已确认。 直通模式下,资金还需要归集到商户归集地址,收到 RECHARGE.SETTLED 后才建议给业务用户最终入账。 COLLECT 回调只用于资金对账、归集监控和异常排查。

payload 不含 chainCode / tokenSymbol

当前充值回调 payload 只包含 chainId / chainTokenId 等内部 id。 如需 chainCode、tokenSymbol 等可读字段,请用 idGET /api/v1/wallet/recharge/get?id=9001 查详情。

提现回调(event: withdraw)

// 提现回调 payload(BROADCASTED→CONFIRMING→SUCCESS/FAILED 时可能多次推送)
{
  "event": "withdraw",
  "id": 12345,
  "applicationId": 1024,
  "appOrderNo": "ORDER_2026050501",
  "chainId": 11,
  "chainTokenId": 50,
  "toAddress": "0xRecipient...",
  "amount": "10.5",
  "txHash": "0xdef...",
  "status": "SUCCESS"
}

提现回调字段说明

event
string required
固定值 "withdraw"
例: withdraw
id
integer required
提现记录 id。
例: 12345
applicationId
integer required
所属应用 id。
例: 1024
appOrderNo
string required
商户侧提现业务单号,与发起提现时传入的一致。
例: ORDER_2026050501
chainId
integer required
链内部 id。
例: 11
chainTokenId
integer required
代币 id。
例: 50
toAddress
string required
提现目标地址。
例: 0xRecipient...
amount
string required
提现金额,字符串格式。
例: "10.5"
txHash
string required
链上交易哈希。
例: 0xdef...
status
string required
提现状态,如 BROADCASTED / CONFIRMING / SUCCESS / FAILED。
例: SUCCESS

提现回调可能多次推送,按 (appOrderNo, status) 幂等

提现在 BROADCASTED → CONFIRMING → SUCCESS / FAILED 状态转换时均会触发回调推送。 商户需按 (appOrderNo, status) 组合做幂等去重,不要因重复推送重复操作账户。

归集回调(event: collect)

// 归集回调 payload(信息性事件,用于对账和运维,不替代 RECHARGE.SETTLED)
{
  "event": "collect",
  "id": 21,
  "requestId": 21,
  "itemId": 21,
  "applicationId": 1024,
  "chainId": 11,
  "chainTokenId": 50,
  "stage": "HOT_GATHER",
  "fromAddressId": 30001,
  "fromAddress": "0xUser...",
  "toAddress": "0xCollect...",
  "amount": "2.000000000000000000",
  "txHash": "0xcollectTx...",
  "status": "SUCCESS",
  "failureReasonCode": null,
  "failureReasonMessage": null,
  "retryTimes": 0,
  "gasFee": "0.0031",
  "gasFeeCurrency": "MATIC",
  "gasFunding": {
    "id": 6,
    "status": "SUCCESS",
    "txHash": "0xgasTx...",
    "feeAddress": "0xFee...",
    "userAddress": "0xUser...",
    "amount": "0.2",
    "failureReasonMessage": null
  },
  "relatedRechargeIds": [9001],
  "createdTime": "2026-07-04T07:46:01",
  "finishedTime": "2026-07-04T07:48:10"
}

归集回调字段说明

event
string required
固定值 "collect"
例: collect
id
integer required
归集明细 id,与 itemId 相同。
例: 21
requestId
integer required
归集批次 id。
例: 21
itemId
integer required
归集明细 id。
例: 21
applicationId
integer required
所属应用 id。
例: 1024
chainId
integer required
链内部 id。
例: 11
chainTokenId
integer required
代币 id。
例: 50
stage
string? optional
归集阶段,如 HOT_GATHER / COLD_TRANSFER
例: HOT_GATHER
fromAddressId
integer required
来源地址 id。
例: 30001
fromAddress
string required
来源地址,通常是 USER 地址。
例: 0xUser...
toAddress
string required
目标归集地址。
例: 0xCollect...
amount
string required
归集金额,字符串格式。
例: "2.000000000000000000"
txHash
string? optional
归集链上交易哈希;未广播或同地址直接确认时可能为 null。
例: 0xcollectTx...
status
string required
归集结果:SUCCESS / FAILED
例: SUCCESS
failureReasonCode
string? optional
结构化失败原因码;成功时为 null。
例: ON_CHAIN_REVERTED
failureReasonMessage
string? optional
原始失败原因;成功时为 null。
例: tx reverted on chain
retryTimes
integer required
归集明细已重试次数。
例: 0
gasFee
string? optional
归集交易实际消耗 gas,按原生币显示;未确认时为 null。
例: "0.0031"
gasFeeCurrency
string? optional
gas 费用币种。
例: MATIC
gasFunding
object? optional
如本次归集触发过 FEE → USER 补 gas,这里返回补 gas 快照。
例: {"status":"SUCCESS","txHash":"0xgas..."}
relatedRechargeIds
integer[] required
本归集明细关联的充值记录 id 列表。
例: [9001]
createdTime
string required
归集明细创建时间,ISO 本地时间字符串。
例: 2026-07-04T07:46:01
finishedTime
string required
归集明细终态时间,ISO 本地时间字符串。
例: 2026-07-04T07:48:10

COLLECT 是对账事件,不是用户入账凭证

一笔归集可能合并多笔充值,也可能经历 gas 补给、重试、部分成功等过程。 客户平台给业务用户入账应以 RECHARGE.txStatus = SETTLED 为准。

链上事件回调(event: chain_event)

// 链上事件回调 payload(仅当应用配置了 chain_event 订阅时推送)
{
  "event": "chain_event",
  "id": 7777,
  "subscriptionId": 33,
  "applicationId": 1024,
  "chainId": 11,
  "chainTokenId": 50,
  "contractAddress": "0xUSDT...",
  "fromAddress": "0xSender...",
  "toAddress": "0xMonitored...",
  "amount": "1000.00",
  "txHash": "0xfedcba...",
  "blockHeight": 19384800,
  "directionMatched": "IN",
  "addressNature": "EOA"
}

链上事件回调字段说明

event
string required
固定值 "chain_event"
例: chain_event
id
integer required
链上事件记录 id。
例: 7777
subscriptionId
integer required
命中的订阅配置 id。
例: 33
applicationId
integer required
所属应用 id。
例: 1024
chainId
integer required
链内部 id。
例: 11
chainTokenId
integer required
代币 id。
例: 50
contractAddress
string required
合约地址。
例: 0xUSDT...
fromAddress
string required
转出地址。
例: 0xSender...
toAddress
string required
转入地址(被监听地址)。
例: 0xMonitored...
amount
string required
转账金额,字符串格式。
例: "1000.00"
txHash
string required
链上交易哈希。
例: 0xfedcba...
blockHeight
integer required
所在区块高度。
例: 19384800
directionMatched
string required
方向匹配:IN 表示资金流入被监听地址,OUT 表示流出。
例: IN
addressNature
string required
地址性质:EOA(普通外部账户)或 CONTRACT(合约地址)。
例: EOA

验签示例代码

import crypto from 'node:crypto';
import express from 'express';

const app = express();

// 必须用 raw body —— 不能经过 express.json() 解析
app.use('/callback', express.raw({ type: 'application/json', limit: '1mb' }));

app.post('/callback', (req, res) => {
  const ts    = req.get('X-Timestamp');   // UNIX 毫秒 13 位
  const nonce = req.get('X-Nonce');
  const sig   = req.get('X-Signature');
  const rawBody = req.body.toString('utf8');

  // 1. 防重放:±5 分钟
  if (Math.abs(Date.now() - Number(ts)) > 5 * 60 * 1000) {
    return res.status(200).json({ code: 1, msg: 'timestamp expired' });
  }

  // 2. 重建 stringToSign(与商户调派付 API 完全一致的公式)
  //    stringToSign = METHOD + "\n" + PATH + "\n" + ts + "\n" + nonce + "\n" + SHA256(body)
  const callbackPath = '/callback';   // 你的回调 endpoint 路径
  const bodyHash = crypto.createHash('sha256').update(rawBody, 'utf8').digest('hex');
  const stringToSign = ['POST', callbackPath, ts, nonce, bodyHash].join('\n');

  // 3. constant-time 比对
  const expected = crypto
    .createHmac('sha256', process.env.PQPA_SECRET)
    .update(stringToSign, 'utf8')
    .digest('hex');
  const ok = crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(sig,      'hex'),
  );
  if (!ok) return res.status(200).json({ code: 1, msg: 'signature mismatch' });

  // 4. 业务幂等:RECHARGE 按 (event, id, txStatus),其它事件按自身终态字段去重
  const payload = JSON.parse(rawBody);
  // ...处理业务逻辑...

  // ACK:返回 HTTP 200 且 body 含 {"code":0}
  res.status(200).json({ code: 0 });
});

下一步