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
}充值回调字段说明
eventstring required | 固定值 "recharge"。 例: recharge |
idinteger required | 充值记录 id,可用于调 GET /api/v1/wallet/recharge/get?id=9001 查详情。 例: 9001 |
applicationIdinteger required | 所属应用 id。 例: 1024 |
chainIdinteger required | 链内部 id(非 EVM chainId)。 例: 11 |
chainTokenIdinteger required | 代币 id,与 /support/tokens 返回的 chainTokenId 对应。 例: 50 |
addressstring required | 收款地址(用户充值地址)。 例: 0xab12... |
amountstring required | 到账金额,字符串格式,已考虑 decimals。 例: "100.00" |
txHashstring required | 链上交易哈希。 例: 0xabc... |
blockHeightinteger required | 所在区块高度。 例: 19384712 |
txStatusstring required | 充值生命周期状态: PENDING / CONFIRMED / SETTLED / FAILED。 例: SETTLED |
confirmationsinteger required | 当前确认数; SETTLED 事件固定为 -1。 例: 20 |
minConfirmationsinteger required | 最小确认数; SETTLED 事件固定为 -1。 例: 12 |
业务用户入账只看 RECHARGE 生命周期
RECHARGE.CONFIRMED 表示链上充值已确认。 直通模式下,资金还需要归集到商户归集地址,收到 RECHARGE.SETTLED 后才建议给业务用户最终入账。 COLLECT 回调只用于资金对账、归集监控和异常排查。 payload 不含 chainCode / tokenSymbol
当前充值回调 payload 只包含chainId / chainTokenId 等内部 id。 如需 chainCode、tokenSymbol 等可读字段,请用 id 调 GET /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"
}提现回调字段说明
eventstring required | 固定值 "withdraw"。 例: withdraw |
idinteger required | 提现记录 id。 例: 12345 |
applicationIdinteger required | 所属应用 id。 例: 1024 |
appOrderNostring required | 商户侧提现业务单号,与发起提现时传入的一致。 例: ORDER_2026050501 |
chainIdinteger required | 链内部 id。 例: 11 |
chainTokenIdinteger required | 代币 id。 例: 50 |
toAddressstring required | 提现目标地址。 例: 0xRecipient... |
amountstring required | 提现金额,字符串格式。 例: "10.5" |
txHashstring required | 链上交易哈希。 例: 0xdef... |
statusstring 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"
}归集回调字段说明
eventstring required | 固定值 "collect"。 例: collect |
idinteger required | 归集明细 id,与 itemId 相同。 例: 21 |
requestIdinteger required | 归集批次 id。 例: 21 |
itemIdinteger required | 归集明细 id。 例: 21 |
applicationIdinteger required | 所属应用 id。 例: 1024 |
chainIdinteger required | 链内部 id。 例: 11 |
chainTokenIdinteger required | 代币 id。 例: 50 |
stagestring? optional | 归集阶段,如 HOT_GATHER / COLD_TRANSFER。 例: HOT_GATHER |
fromAddressIdinteger required | 来源地址 id。 例: 30001 |
fromAddressstring required | 来源地址,通常是 USER 地址。 例: 0xUser... |
toAddressstring required | 目标归集地址。 例: 0xCollect... |
amountstring required | 归集金额,字符串格式。 例: "2.000000000000000000" |
txHashstring? optional | 归集链上交易哈希;未广播或同地址直接确认时可能为 null。 例: 0xcollectTx... |
statusstring required | 归集结果: SUCCESS / FAILED。 例: SUCCESS |
failureReasonCodestring? optional | 结构化失败原因码;成功时为 null。 例: ON_CHAIN_REVERTED |
failureReasonMessagestring? optional | 原始失败原因;成功时为 null。 例: tx reverted on chain |
retryTimesinteger required | 归集明细已重试次数。 例: 0 |
gasFeestring? optional | 归集交易实际消耗 gas,按原生币显示;未确认时为 null。 例: "0.0031" |
gasFeeCurrencystring? optional | gas 费用币种。 例: MATIC |
gasFundingobject? optional | 如本次归集触发过 FEE → USER 补 gas,这里返回补 gas 快照。 例: {"status":"SUCCESS","txHash":"0xgas..."} |
relatedRechargeIdsinteger[] required | 本归集明细关联的充值记录 id 列表。 例: [9001] |
createdTimestring required | 归集明细创建时间,ISO 本地时间字符串。 例: 2026-07-04T07:46:01 |
finishedTimestring 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"
}链上事件回调字段说明
eventstring required | 固定值 "chain_event"。 例: chain_event |
idinteger required | 链上事件记录 id。 例: 7777 |
subscriptionIdinteger required | 命中的订阅配置 id。 例: 33 |
applicationIdinteger required | 所属应用 id。 例: 1024 |
chainIdinteger required | 链内部 id。 例: 11 |
chainTokenIdinteger required | 代币 id。 例: 50 |
contractAddressstring required | 合约地址。 例: 0xUSDT... |
fromAddressstring required | 转出地址。 例: 0xSender... |
toAddressstring required | 转入地址(被监听地址)。 例: 0xMonitored... |
amountstring required | 转账金额,字符串格式。 例: "1000.00" |
txHashstring required | 链上交易哈希。 例: 0xfedcba... |
blockHeightinteger required | 所在区块高度。 例: 19384800 |
directionMatchedstring required | 方向匹配: IN 表示资金流入被监听地址,OUT 表示流出。 例: IN |
addressNaturestring 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 });
});下一步
- 查询回调任务 — 查看回调状态与失败记录
- 查询 attempt 历史 — 逐次推送记录(请求/响应详情)
- 重发回调 — 手工触发重试
- 查询充值详情 — 用充值回调中的 id 查可读字段