回调
平台在充值到账、提现状态变更时回调你的 callback_url。回调体是 OAEP 密文,并带 ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN 三个请求头——与下行响应使用同一套验签 + 解密逻辑,你可以直接复用 SDK。
回调是对接中最容易出错、也最影响资金记账的一环,请完整阅读本页并按 上线自检 与文末联调方式验证后再上线。
投递方式(实现细节)
重要实现细节:回调请求长什么样
- HTTP body 是整段 Base64 密文,不是
{ code, data, message }的 JSON 包裹。请直接读取原始 body(PHPphp://input、Expressexpress.text()等),不要用 JSON 解析器去解它。 - 请求头
Content-Type: application/json(但 body 实际是密文文本,勿按 JSON 处理)。 - 单次投递超时 3 秒,超时即判定本次失败,无同步重试(失败后进入退避重试,见下文)。请把耗时的业务处理放到异步队列,先尽快应答。
- 回调 URL 优先级:单据级
callbackUrl(createAddress / createWithdrawOrder 请求时传入)优先;未传则用商户级默认callback_url(商户后台配置,可通过 merchantInfo 查看);两者均为空则不推送任何回调。 - 回调由平台定时任务触发,属异步推送,到账 / 状态变更后会有短暂延迟,并非长连接实时推送。
处理回调的 5 步
- 读取请求头
ANONY-TIMESTAMP/ANONY-NONCE/ANONY-SIGN,请求体为 Base64 密文。 - 校验时间窗(±5 分钟)+ nonce 去重(拒绝重放的回调)。
canonical(ts, nonce, body)+ 平台公钥验签(canonical 串规范见 协议规范)。- 商户私钥 OAEP 解密得到业务 JSON。
- 处理成功后按与平台约定返回(如纯文本
success;确切应答语义见下文「应答语义」)。
回调与响应使用同一套 canonical 验签 + OAEP 解密逻辑,SDK 的
verifyResponse/verify+decrypt可直接复用,见 SDK 使用。
回调必须传入 nonce 去重函数(isFreshNonce)
verifyResponse 的 isFreshNonce(PHP / Python / Node)参数省略时会跳过 nonce 去重——处理同步响应时可以省略,但处理回调时必须传入一个基于原子存储(如 Redis SET key 1 NX EX 900)的判重函数,否则回调可被重放。Go / Java 请自行在 verify 前完成时间窗校验 + nonce 去重。nonce 去重键的过期时间建议 ≥ 15 分钟。
应答语义
重要实现细节:平台如何解读你的应答
- 存款回调:必须返回纯文本大写
SUCCESS才算推送成功,其他任何应答(含超时)都会触发重试。 - 提现回调(
businessType: withdraw):SUCCESS= 确认收到、平台推进订单状态;FAIL= 拒绝该笔提现,平台执行退款(WithdrawRefund);其他应答 = 判定失败,进入重试。 - 提现待确认回调(
businessType: withdrawalPendingConfirm):SUCCESS= 放行该笔提现;FAIL= 拒绝并退款。 - 因此:当你验签失败、解密失败或系统异常时,绝对不要返回
FAIL(提现类回调会被当作"商户主动拒绝"而直接退款)——返回其他内容(如 HTTP 400 + 任意文本)让平台按退避重试即可。
| 回调类型 | 你返回 | 平台行为 |
|---|---|---|
deposit | SUCCESS | 标记推送成功 |
deposit | 其他 / 超时 | 按退避重试 |
withdraw | SUCCESS | 推进订单状态 |
withdraw | FAIL | 拒绝该笔提现并退款(WithdrawRefund) |
withdraw | 其他 / 超时 | 按退避重试 |
withdrawalPendingConfirm | SUCCESS | 放行提现,继续走转账流程 |
withdrawalPendingConfirm | FAIL | 拒绝并退款 |
失败重试与手动补推
重要实现细节:重试退避
单次投递失败后,平台按以下间隔退避重试:
| 间隔序列 |
|---|
| 2 秒 → 30 秒 → 2 分钟 → 5 分钟 → 15 分钟 → 60 分钟 |
累计 5 次失败后停止推送。停止后不会自动恢复,可在商户后台对该笔记录手动补推。请为回调接口配置监控告警,避免因自身服务不可用而漏单。
由于存在重试与补推,同一笔回调可能送达多次,你的处理逻辑必须幂等(见下文伪代码)。
充值到账回调
解密后的业务字段:
| 字段 | 类型 | 说明 |
|---|---|---|
businessType | string | 固定 deposit |
txid | string | 链上交易哈希 |
address | string | 收款地址 |
addressType | string | 地址类型(当前开放:trx/solana/eth_bnb/ton;btc 为预留值) |
fromAddress | string | 付款方地址 |
amount | string | 到账金额 |
currency | string | 币种符号(枚举见 接口公共说明) |
fee | string | 手续费 |
apiOrder | string | 平台订单号 |
userOrder | string | 商户订单号(创建地址时传入) |
TRON 链 USDC 充值已关闭
TRON 链 USDC(TRC-20 USDC)充值已关闭:即使有 USDC 转入你的 TRON 收款地址,平台也不入账、不触发充值回调。 请勿引导你的用户向 TRON 收款地址转入 USDC,否则资金无法自动到账。支持的币种以 接口公共说明 的枚举与 getCurrencies 返回为准。
重要实现细节:触发条件
充值回调仅在**入账成功且金额 ≥ 该币种最低充值额(deposit_min)**时触发。各币种 deposit_min 可通过公共接口 getCurrencies 查询。
提现状态回调
解密后的业务字段:
| 字段 | 类型 | 说明 |
|---|---|---|
businessType | string | 固定 withdraw |
toAddress | string | 提现目标地址 |
amount | string | 提现金额 |
currency | string | 币种符号 |
fee | string | 手续费 |
apiOrder | string | 平台订单号 |
userOrder | string | 商户订单号 |
withdrawType | string | 提现类型(normal/financialConfirmation) |
message | string | 备注 |
txid | string | 链上交易哈希(未完成时可能为空) |
orderStatus | string | 订单状态(如 success/fail/pending) |
重要实现细节:提现待确认回调(withdrawalPendingConfirm)
除上表的最终态回调外,提现流程中还可能收到 businessType: withdrawalPendingConfirm 的待确认回调——此时订单尚未上链,报文中没有 txid 和 orderStatus 字段。你的应答直接决定订单走向:返回 SUCCESS 放行该笔提现,返回 FAIL 拒绝并退款。请务必按 businessType 分发处理,不要假设所有回调都带 txid/orderStatus。
商户侧处理示例(伪代码)
完整处理流程:验时间窗 → nonce 去重 → 验签 → 解密 → 按 businessType 分发 → 幂等处理 → 返回 SUCCESS。
以下为骨架示例(各语言 SDK 的实际方法签名以 SDK 使用 为准):
<?php
require 'AnonyV2Client.php';
$ts = $_SERVER['HTTP_ANONY_TIMESTAMP'] ?? '';
$nonce = $_SERVER['HTTP_ANONY_NONCE'] ?? '';
$sign = $_SERVER['HTTP_ANONY_SIGN'] ?? '';
$body = file_get_contents('php://input'); // 整段 Base64 密文,不是 JSON
$client = new AnonyV2Client($appId, $serverPublicKeyB64, $merchantPrivateKeyB64);
try {
// 时间窗校验 → nonce 去重(isFreshNonce 必传)→ 验签 → 解密
$data = $client->verifyResponse($body, $ts, $nonce, $sign, function ($n) use ($redis) {
// 原子去重:首次写入成功 = 新 nonce;重复 nonce 返回 false → 回调被拒
return (bool) $redis->set('anony:cb:nonce:' . $n, 1, ['nx', 'ex' => 900]);
});
} catch (Throwable $e) {
http_response_code(400); // 不要返回 FAIL:让平台按退避重试
exit('verify failed');
}
switch ($data['businessType'] ?? '') {
case 'deposit':
handleDeposit($data); // 以 txid / apiOrder 做幂等,重复回调直接回 SUCCESS
break;
case 'withdraw':
handleWithdraw($data); // 以 apiOrder + orderStatus 做幂等
break;
case 'withdrawalPendingConfirm':
if (!approveWithdraw($data)) {
exit('FAIL'); // 拒绝该笔提现并退款
}
break;
}
echo 'SUCCESS'; // 纯文本大写# Flask 示例骨架
from anony_v2 import AnonyV2Client
client = AnonyV2Client(APP_ID, SERVER_PUB_B64, MERCHANT_PRIV_B64)
@app.post("/anony/callback")
def anony_callback():
ts = request.headers.get("ANONY-TIMESTAMP", "")
nonce = request.headers.get("ANONY-NONCE", "")
sign = request.headers.get("ANONY-SIGN", "")
body = request.get_data(as_text=True) # 整段 Base64 密文,不是 JSON
def is_fresh_nonce(n: str) -> bool:
# 原子去重:SET NX EX 900,首次写入成功 = 新 nonce
return bool(redis.set(f"anony:cb:nonce:{n}", 1, nx=True, ex=900))
try:
# 时间窗校验 → nonce 去重(is_fresh_nonce 必传)→ 验签 → 解密
data = client.verify_response(body, ts, nonce, sign, is_fresh_nonce)
except Exception:
return "verify failed", 400 # 不要返回 FAIL:让平台重试
bt = data.get("businessType")
if bt == "deposit":
handle_deposit(data) # 以 txid / apiOrder 做幂等
elif bt == "withdraw":
handle_withdraw(data) # 以 apiOrder + orderStatus 做幂等
elif bt == "withdrawalPendingConfirm":
if not approve_withdraw(data):
return "FAIL" # 拒绝该笔提现并退款
return "SUCCESS" # 纯文本大写// Express 示例骨架
const express = require('express');
const { AnonyV2Client } = require('./anony_v2');
const c = new AnonyV2Client(appId, serverPubB64, merchantPrivB64);
const app = express();
// 关键:body 是整段密文文本,不能用 express.json()
app.post('/anony/callback', express.text({ type: '*/*' }), async (req, res) => {
const ts = req.get('ANONY-TIMESTAMP') || '';
const nonce = req.get('ANONY-NONCE') || '';
const sign = req.get('ANONY-SIGN') || '';
const body = req.body; // 整段 Base64 密文
// 原子去重(SET NX EX 900),结果作为 isFreshNonce 传入
const fresh = await redis.set(`anony:cb:nonce:${nonce}`, '1', { NX: true, EX: 900 });
let data;
try {
// 时间窗校验 → nonce 去重(isFreshNonce 必传)→ 验签 → 解密
data = c.verifyResponse(body, ts, nonce, sign, () => fresh === 'OK');
} catch (e) {
return res.status(400).send('verify failed'); // 不要返回 FAIL:让平台重试
}
switch (data.businessType) {
case 'deposit':
await handleDeposit(data); // 以 txid / apiOrder 做幂等
break;
case 'withdraw':
await handleWithdraw(data); // 以 apiOrder + orderStatus 做幂等
break;
case 'withdrawalPendingConfirm':
if (!(await approveWithdraw(data))) {
return res.send('FAIL'); // 拒绝该笔提现并退款
}
break;
}
res.send('SUCCESS'); // 纯文本大写
});Go / Java SDK 没有封装
isFreshNonce,请按同样顺序自行实现:先校验时间窗与 nonce 去重,再调用Verify/verify验签、Decrypt/decrypt解密,最后按businessType分发。
联调
- 商户后台提供"测试回调"按钮:可对你配置的回调地址发起一次真实格式(密文 + 签名头)的推送,用于验证你的接收端验签、解密、应答是否正确。建议上线前先跑通测试回调,再配合 上线自检 selfcheck 验证上行链路。
- 验签失败时请按 错误与排查 与 常见问题排查 的顺序检查:OAEP 是否 SHA-1、canonical 串是否
ts\nnonce\nbody、密钥角色是否用反(回调验签用平台公钥、解密用商户私钥)、服务器时间是否 NTP 同步。