Skip to content

回调

平台在充值到账提现状态变更时回调你的 callback_url。回调体是 OAEP 密文,并带 ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN 三个请求头——与下行响应使用同一套验签 + 解密逻辑,你可以直接复用 SDK。

回调是对接中最容易出错、也最影响资金记账的一环,请完整阅读本页并按 上线自检 与文末联调方式验证后再上线。

投递方式(实现细节)

重要实现细节:回调请求长什么样

  • HTTP body 是整段 Base64 密文,不是 { code, data, message } 的 JSON 包裹。请直接读取原始 body(PHP php://input、Express express.text() 等),不要用 JSON 解析器去解它。
  • 请求头 Content-Type: application/json(但 body 实际是密文文本,勿按 JSON 处理)。
  • 单次投递超时 3 秒,超时即判定本次失败,无同步重试(失败后进入退避重试,见下文)。请把耗时的业务处理放到异步队列,先尽快应答。
  • 回调 URL 优先级:单据级 callbackUrlcreateAddress / createWithdrawOrder 请求时传入)优先;未传则用商户级默认 callback_url(商户后台配置,可通过 merchantInfo 查看);两者均为空则不推送任何回调
  • 回调由平台定时任务触发,属异步推送,到账 / 状态变更后会有短暂延迟,并非长连接实时推送。

处理回调的 5 步

  1. 读取请求头 ANONY-TIMESTAMP / ANONY-NONCE / ANONY-SIGN,请求体为 Base64 密文。
  2. 校验时间窗(±5 分钟)+ nonce 去重(拒绝重放的回调)。
  3. canonical(ts, nonce, body) + 平台公钥验签(canonical 串规范见 协议规范)。
  4. 商户私钥 OAEP 解密得到业务 JSON。
  5. 处理成功后按与平台约定返回(如纯文本 success;确切应答语义见下文「应答语义」)。

回调与响应使用同一套 canonical 验签 + OAEP 解密逻辑,SDK 的 verifyResponse / verify+decrypt 可直接复用,见 SDK 使用

回调必须传入 nonce 去重函数(isFreshNonce)

verifyResponseisFreshNonce(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 + 任意文本)让平台按退避重试即可。
回调类型你返回平台行为
depositSUCCESS标记推送成功
deposit其他 / 超时按退避重试
withdrawSUCCESS推进订单状态
withdrawFAIL拒绝该笔提现并退款(WithdrawRefund)
withdraw其他 / 超时按退避重试
withdrawalPendingConfirmSUCCESS放行提现,继续走转账流程
withdrawalPendingConfirmFAIL拒绝并退款

失败重试与手动补推

重要实现细节:重试退避

单次投递失败后,平台按以下间隔退避重试:

间隔序列
2 秒 → 30 秒 → 2 分钟 → 5 分钟 → 15 分钟 → 60 分钟

累计 5 次失败后停止推送。停止后不会自动恢复,可在商户后台对该笔记录手动补推。请为回调接口配置监控告警,避免因自身服务不可用而漏单。

由于存在重试与补推,同一笔回调可能送达多次,你的处理逻辑必须幂等(见下文伪代码)。

充值到账回调

解密后的业务字段:

字段类型说明
businessTypestring固定 deposit
txidstring链上交易哈希
addressstring收款地址
addressTypestring地址类型(当前开放:trx/solana/eth_bnb/ton;btc 为预留值)
fromAddressstring付款方地址
amountstring到账金额
currencystring币种符号(枚举见 接口公共说明
feestring手续费
apiOrderstring平台订单号
userOrderstring商户订单号(创建地址时传入)

TRON 链 USDC 充值已关闭

TRON 链 USDC(TRC-20 USDC)充值已关闭:即使有 USDC 转入你的 TRON 收款地址,平台也不入账、不触发充值回调。 请勿引导你的用户向 TRON 收款地址转入 USDC,否则资金无法自动到账。支持的币种以 接口公共说明 的枚举与 getCurrencies 返回为准。

重要实现细节:触发条件

充值回调仅在**入账成功且金额 ≥ 该币种最低充值额(deposit_min)**时触发。各币种 deposit_min 可通过公共接口 getCurrencies 查询。

提现状态回调

解密后的业务字段:

字段类型说明
businessTypestring固定 withdraw
toAddressstring提现目标地址
amountstring提现金额
currencystring币种符号
feestring手续费
apiOrderstring平台订单号
userOrderstring商户订单号
withdrawTypestring提现类型(normal/financialConfirmation)
messagestring备注
txidstring链上交易哈希(未完成时可能为空)
orderStatusstring订单状态(如 success/fail/pending)

重要实现细节:提现待确认回调(withdrawalPendingConfirm)

除上表的最终态回调外,提现流程中还可能收到 businessType: withdrawalPendingConfirm待确认回调——此时订单尚未上链,报文中没有 txidorderStatus 字段。你的应答直接决定订单走向:返回 SUCCESS 放行该笔提现,返回 FAIL 拒绝并退款。请务必按 businessType 分发处理,不要假设所有回调都带 txid/orderStatus

商户侧处理示例(伪代码)

完整处理流程:验时间窗 → nonce 去重 → 验签 → 解密 → 按 businessType 分发 → 幂等处理 → 返回 SUCCESS

以下为骨架示例(各语言 SDK 的实际方法签名以 SDK 使用 为准):

php
<?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';                   // 纯文本大写
python
# 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"               # 纯文本大写
js
// 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 同步。