Skip to main content

回调说明

当虚拟信用卡发生交易时,系统会向您配置的回调地址发送 POST 请求,实时通知交易信息。
回调地址在用户信息中配置,可通过平台后台设置

回调请求

请求方式

POST {您配置的回调地址}

请求头

Content-Type: application/json

请求体参数

accountId
string
账户 ID
data
object
交易数据对象
timestamp
string
回调时间戳
sign
string
签名值

请求示例

{
  "accountId": "132456789",
  "data": {
    "id": "a7787ada1123-xxxx-uuuuu-sssss",
    "cardNum": "5572710152044****",
    "type": "Consumption",
    "status": "Pending",
    "amount": "-25.50",
    "merchantName": "Amazon",
    "transactionId": "TXN20231201123456",
    "recordTime": "2023-12-01T10:30:00.000+00:00",
    "remark": "在线购物"
  },
  "timestamp": "1701424200000",
  "sign": "ABC123DEF456..."
}

签名验证

重要:必须验证回调请求的签名,确保请求来自皮卡宝平台

验证步骤

1

提取签名

从请求体中提取 sign 字段
2

构建验证字符串

accountIddata 对象内的所有字段、timestamp 按照 ASCII 码排序并拼接注意:data 对象内的参数也要参与排序,空值字段也要参与签名
3

计算签名

使用相同的签名算法(MD5)计算签名值
4

对比验证

将计算的签名与请求中的签名对比,一致则验证通过

签名验证示例

function verifyWebhookSign(requestBody, secretKey) {
  const { sign, ...params } = requestBody;

  // 展开 data 对象
  const { data, accountId, timestamp } = params;
  const allParams = { accountId, timestamp, ...data };

  // 按 ASCII 排序并构建字符串
  const sortedKeys = Object.keys(allParams).sort();
  const stringA = sortedKeys
    .map(key => `${key}=${encodeURIComponent(allParams[key])}`)
    .join('&')
    .replace(/\+/g, '%20');

  // 拼接密钥并计算 MD5
  const stringSignTemp = `${stringA}&key=${secretKey}`;
  const calculatedSign = md5(stringSignTemp).toUpperCase();

  return calculatedSign === sign;
}
import hashlib
from urllib.parse import quote

def verify_webhook_sign(request_body, secret_key):
    sign = request_body.pop('sign')

    # 展开 data 对象
    data = request_body.pop('data')
    all_params = {
        'accountId': request_body['accountId'],
        'timestamp': request_body['timestamp'],
        **data
    }

    # 按 ASCII 排序
    sorted_keys = sorted(all_params.keys())
    string_a = '&'.join([
        f"{key}={quote(str(all_params[key]))}"
        for key in sorted_keys
    ]).replace('+', '%20')

    # 拼接密钥并计算 MD5
    string_sign_temp = f"{string_a}&key={secret_key}"
    calculated_sign = hashlib.md5(
        string_sign_temp.encode()
    ).hexdigest().upper()

    return calculated_sign == sign

响应要求

接收到回调后,您的服务器应返回:

成功响应

{
  "code": 0,
  "msg": "success"
}

失败响应

{
  "code": 1,
  "msg": "error message"
}
如果回调失败(网络错误、超时、返回非成功状态),系统会进行重试,最多重试 3 次

重试机制

  • 重试间隔:5秒、30秒、300秒
  • 重试次数:最多 3 次
  • 超时时间:10 秒
建议实现幂等性处理,避免重复回调导致的问题

最佳实践

签名验证

始终验证回调签名,防止伪造请求

异步处理

快速响应,将业务逻辑放在异步队列处理

幂等设计

使用交易 ID 去重,避免重复处理

日志记录

记录所有回调请求,便于问题排查

回调处理示例

// Express.js 示例
app.post('/webhook/vcc', async (req, res) => {
  try {
    // 1. 验证签名
    if (!verifyWebhookSign(req.body, SECRET_KEY)) {
      return res.status(403).json({ code: 1, msg: '签名验证失败' });
    }

    // 2. 检查是否已处理(幂等性)
    const { id } = req.body.data;
    if (await isProcessed(id)) {
      return res.json({ code: 0, msg: 'success' });
    }

    // 3. 快速响应
    res.json({ code: 0, msg: 'success' });

    // 4. 异步处理业务逻辑
    processTransactionAsync(req.body.data);

  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ code: 1, msg: '处理失败' });
  }
});

常见问题

在用户信息中的 notifyUrl 字段配置,可通过平台后台或 API 设置
所有类型的交易都会触发回调,包括消费、充值、退款、撤销等
可以在测试环境进行小额消费测试,验证回调是否正常接收和处理
系统会自动重试 3 次,如果都失败,需要通过交易查询接口主动查询