跳转到主要内容

什么是 Webhooks?

Webhooks 是 KillB 在事件发生时发送到您服务器的 HTTP 回调。它们能够实时通知 ramps、用户、账户和交易,而无需持续轮询。
Webhooks 是监控交易状态并保持应用程序与 KillB 同步的推荐方式。

Webhooks 如何工作

Webhook 事件结构

所有 webhook 事件都遵循此结构:
{
  id: string,              // UUID - 唯一事件 ID
  event: string,           // 事件类型:RAMP | USER | TRANSACTION | ACCOUNT | CUSTODIAL_ACCOUNT
  action: string,          // 操作:CREATE | UPDATE | DELETE
  data: object,            // 事件特定数据
  createdAt: string,       // ISO 8601 时间戳
  updatedAt: string,       // ISO 8601 时间戳
  attempts: number         // 交付尝试次数(0 到 5)
}

Webhook 事件

KillB 支持以下事件类型:
Ramp 交易事件事件类型: RAMP操作:
  • CREATE - 创建新 ramp
  • UPDATE - Ramp 状态已更改
  • DELETE - Ramp 已取消(罕见)
常见状态值:
  • QUOTE_CREATED - 报价已创建
  • PENDING_PAYMENT - 等待支付
  • CASH_IN_PROCESSING - 正在处理支付
  • CASH_IN_COMPLETED - 支付已确认
  • PROCESSING - 正在转换货币
  • CASH_OUT_PROCESSING - 正在发送资金
  • COMPLETED - Ramp 已完成
  • FAILED - Ramp 失败
  • CANCELED - Ramp 已取消

设置 Webhooks

创建 Webhook 配置

POST /api/v2/webhooks
请求:
{
  "url": "https://api.yourapp.com/webhooks/killb",
  "secret": "your-secret-key-min-32-chars",
  "events": ["RAMP", "USER", "ACCOUNT"]
}
响应:
{
  "id": "webhook-id",
  "customerId": "customer-id",
  "url": "https://api.yourapp.com/webhooks/killb",
  "events": ["RAMP", "USER", "ACCOUNT"],
  "active": true,
  "createdAt": "2024-01-15T10:30:00.000Z"
}
const setupWebhook = async () => {
  const response = await fetch('https://teste-94u93qnn.uc.gateway.dev/api/v2/webhooks', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      url: 'https://api.yourapp.com/webhooks/killb',
      secret: process.env.WEBHOOK_SECRET,
      events: ['RAMP', 'USER', 'ACCOUNT', 'TRANSACTION']
    })
  });
  
  return await response.json();
};

Webhook 安全

签名验证

KillB 使用 HMAC SHA-256 对所有 webhook 请求进行签名。始终验证签名以确保真实性。 标头: x-signature-sha256
const crypto = require('crypto');
const express = require('express');

app.post('/webhooks/killb', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-signature-sha256'];
  const payload = req.body.toString();
  
  // 计算预期签名
  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  
  // 验证签名
  if (signature !== expectedSignature) {
    console.error('Invalid webhook signature');
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  // 解析并处理事件
  const event = JSON.parse(payload);
  processWebhookEvent(event);
  
  res.status(200).json({ received: true });
});
永远不要跳过签名验证! 未验证的 webhooks 可能被攻击者伪造。

Webhook 要求

您的 webhook 端点必须:
  • 在 5 秒内返回 200 OK
  • 异步处理事件
  • 不要等待慢速操作
  • 对繁重处理使用作业队列
  • 幂等(处理重复事件)
  • 使用事件 ID 检测重复
  • 存储已处理的事件 ID
  • 跳过已处理的事件
  • 保持 >99% 正常运行时间
  • 仅使用 HTTPS
  • 拥有有效的 SSL 证书
  • 使用适当的状态代码响应
  • 始终检查 x-signature-sha256
  • 使用恒定时间比较
  • 拒绝无效签名
  • 记录验证失败

重试逻辑

KillB 使用指数退避自动重试失败的 webhook 交付: 重试计划:
  • 尝试 0:立即交付
  • 尝试 1:1 分钟后
  • 尝试 2:5 分钟后
  • 尝试 3:15 分钟后
  • 尝试 4:1 小时后
  • 尝试 5:6 小时后(最终尝试)
失败标准:
  • 非 2xx 响应代码
  • 连接超时(> 5 秒)
  • 网络错误
webhook 有效负载中的 attempts 字段指示当前交付尝试(0 到 5)。
// 检查重试次数
if (webhookEvent.attempts > 3) {
  console.warn(`High retry count: ${webhookEvent.attempts}`);
  // 提醒监控系统
}
尝试 5 次失败后,webhook 被标记为永久失败,需要手动干预。检查您的 webhook 日志并修复任何问题。

测试 Webhooks

使用 ngrok 进行本地测试

# 安装 ngrok
npm install -g ngrok

# 启动本地服务器
node server.js

# 暴露到互联网
ngrok http 3000

# 在 webhook 配置中使用 ngrok URL
# https://abc123.ngrok.io/webhooks/killb

手动测试

在沙盒中触发测试事件:
// 创建测试 ramp
const ramp = await createRamp(quoteId, userId, accountId);

// 模拟完成(触发 webhooks)
await fetch('/api/v2/faker/cash-in', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}` },
  body: JSON.stringify({ rampId: ramp.id })
});

// 您的 webhook 应该收到 ramp.cash_in_completed 事件

管理 Webhooks

获取 Webhook 配置

GET /api/v2/webhooks
返回您当前的 webhook 配置。

更新 Webhook

PATCH /api/v2/webhooks
更新:
  • URL
  • Secret
  • 要订阅的事件
  • 活动状态
{
  "url": "https://api.yourapp.com/webhooks/killb-v2",
  "active": true,
  "events": ["RAMP", "USER"]
}

删除 Webhook

DELETE /api/v2/webhooks
删除 webhook 配置。将不再发送事件。

事件处理

示例处理程序

const processWebhookEvent = (webhookEvent) => {
  const { id, event, action, data, attempts } = webhookEvent;
  
  console.log(`Processing ${event}.${action} (attempt ${attempts})`);
  
  // 根据事件类型和操作路由
  switch(event) {
    case 'RAMP':
      handleRampEvent(action, data);
      break;
      
    case 'USER':
      handleUserEvent(action, data);
      break;
      
    case 'ACCOUNT':
      handleAccountEvent(action, data);
      break;
      
    case 'TRANSACTION':
      handleTransactionEvent(action, data);
      break;
      
    case 'CUSTODIAL_ACCOUNT':
      handleCustodialEvent(action, data);
      break;
      
    default:
      console.log(`Unhandled event type: ${event}`);
  }
};

const handleRampEvent = async (action, data) => {
  if (action === 'UPDATE' && data.status === 'COMPLETED') {
    // 更新数据库
    await db.ramps.update(data.id, {
      status: data.status,
      updatedAt: data.updatedAt,
      transferProof: data.transferProof
    });
    
    // 通知用户
    await sendEmail(data.userId, 'Transaction Completed', {
      amount: data.toAmount,
      currency: data.toCurrency,
      txHash: data.transferProof
    });
    
    // 更新内部系统
    await updateAccounting(data);
  }
};

const handleUserEvent = async (action, data) => {
  if (action === 'UPDATE') {
    // 检查 KYC 级别是否更改
    await db.users.update(data.id, {
      status: data.status,
      accessLevel: data.accessLevel,
      updatedAt: data.updatedAt
    });
    
    // 如果 KYC 已批准,启用功能
    if (data.status === 'ACTIVE' && data.accessLevel !== 'L0') {
      await enableUserFeatures(data.id, data.accessLevel);
    }
  }
};

const handleAccountEvent = async (action, data) => {
  if (action === 'UPDATE' && data.status === 'ACTIVE') {
    // 账户已验证
    await db.accounts.update(data.id, {
      status: data.status,
      verified: true
    });
    
    // 通知用户他们的账户已准备就绪
    await sendNotification(data.userId, 'Account Verified');
  }
};

幂等性

使用事件 ID 处理重复的 webhook 交付:
const handleWebhook = async (webhookEvent) => {
  const { id, event, action, data, attempts } = webhookEvent;
  
  // 检查是否已处理(使用唯一事件 ID)
  const existing = await db.webhookEvents.findOne({ eventId: id });
  
  if (existing) {
    console.log(`Duplicate event ${id}, skipping`);
    return { received: true, duplicate: true };
  }
  
  // 立即存储事件以防止重复处理
  await db.webhookEvents.create({
    eventId: id,
    eventType: event,
    action: action,
    payload: data,
    attempts: attempts,
    receivedAt: new Date(),
    processed: false
  });
  
  try {
    // 处理事件
    await processWebhookEvent(webhookEvent);
    
    // 标记为已处理
    await db.webhookEvents.update(
      { eventId: id },
      { processed: true, processedAt: new Date() }
    );
    
    return { received: true };
  } catch (error) {
    // 记录错误但仍标记为已接收
    await db.webhookEvents.update(
      { eventId: id },
      { error: error.message, failedAt: new Date() }
    );
    
    throw error;
  }
};

最佳实践

app.post('/webhooks/killb', async (req, res) => {
  // 验证签名
  verifySignature(req);
  
  // 立即确认
  res.status(200).json({ received: true });
  
  // 异步处理
  processAsync(req.body);
});
  • 立即返回 200 OK
  • 在后台处理事件
  • 使用作业队列(Bull、Celery 等)
  • 不要等待外部服务
const processWebhook = async (event) => {
  try {
    await processEvent(event);
  } catch (error) {
    console.error('Webhook processing failed:', error);
    // 记录到监控服务
    await logError(error, event);
    // 不要抛出 - 我们已经处理了
    // KillB 无论如何都会重试
  }
};
  • 捕获所有错误
  • 记录失败
  • 确认后不要抛出
  • 监控错误率
await db.webhookLogs.create({
  eventId: event.id,
  eventType: event.event,
  payload: event.data,
  receivedAt: new Date(),
  processed: true
});
  • 保留审计跟踪
  • 如果需要,启用重放
  • 轻松调试问题
  • 满足合规要求
  • 跟踪交付成功率
  • 多次失败时提醒
  • 监控处理时间
  • 设置正常运行时间监控
  • 检查签名验证失败

Webhook 安全清单

1

仅使用 HTTPS

永远不要对 webhook 端点使用 HTTP
✅ https://api.yourapp.com/webhooks
❌ http://api.yourapp.com/webhooks
2

验证每个请求

在每个 webhook 上检查 x-signature-sha256
3

使用强密钥

最少 32 个字符,随机,安全存储
const secret = crypto.randomBytes(32).toString('hex');
4

实现速率限制

防止 webhook 泛滥
5

记录可疑活动

跟踪无效签名和异常模式

故障排除

检查:
  • Webhook URL 可公开访问
  • SSL 证书有效
  • 防火墙允许 KillB IP
  • 端点快速返回 200 OK
  • 没有反向代理问题
测试:
curl -X POST https://your-webhook-url \
  -H "Content-Type: application/json" \
  -d '{"test": "data"}'
常见问题:
  • 使用错误的密钥
  • 在验证之前解析正文
  • 字符集/编码问题
  • 空白修改
解决方案:
// 使用原始正文
app.use('/webhooks', express.raw({type: 'application/json'}));

// 在解析之前验证
verifySignature(req.body.toString(), signature);

// 然后解析
const event = JSON.parse(req.body);
这是正常的! Webhooks 可能会多次交付。使用以下方式处理:
  • 事件 ID 跟踪
  • 数据库唯一约束
  • 幂等性密钥
  • “已处理”标志

高级配置

过滤事件

仅订阅您需要的事件:
{
  "url": "https://api.yourapp.com/webhooks/killb",
  "secret": "your-secret",
  "events": ["RAMP"]
}

多个 Webhooks

为不同的事件类型创建不同的端点:
// Ramp 事件 → 支付处理器
await createWebhook({
  url: 'https://payments.yourapp.com/webhooks',
  events: ['RAMP']
});

// 用户事件 → CRM
await createWebhook({
  url: 'https://crm.yourapp.com/webhooks',
  events: ['USER']
});
每个客户只能有一个活动的 webhook 配置。使用单个端点并在内部路由。

Webhook 监控

// 跟踪 webhook 健康
const webhookMetrics = {
  received: 0,
  processed: 0,
  failed: 0,
  averageProcessingTime: 0
};

app.post('/webhooks/killb', async (req, res) => {
  const startTime = Date.now();
  webhookMetrics.received++;
  
  try {
    await processWebhook(req.body);
    webhookMetrics.processed++;
  } catch (error) {
    webhookMetrics.failed++;
    throw error;
  } finally {
    const duration = Date.now() - startTime;
    webhookMetrics.averageProcessingTime = 
      (webhookMetrics.averageProcessingTime + duration) / 2;
  }
  
  res.status(200).json({ received: true });
});

下一步