Saltar al contenido principal

¿Qué son los Webhooks?

Webhooks son callbacks HTTP que KillB envía a tu servidor cuando ocurren eventos. Habilitan notificaciones en tiempo real sobre ramps, usuarios, cuentas y transacciones sin polling constante.
Los webhooks son la forma recomendada de monitorear el estado de transacciones y mantener tu aplicación sincronizada con KillB.

Cómo Funcionan los Webhooks

Estructura del Evento Webhook

Todos los eventos webhook siguen esta estructura:
{
  id: string,              // UUID - ID único del evento
  event: string,           // Tipo de evento: RAMP | USER | TRANSACTION | ACCOUNT | CUSTODIAL_ACCOUNT
  action: string,          // Acción: CREATE | UPDATE | DELETE
  data: object,            // Datos específicos del evento
  createdAt: string,       // Timestamp ISO 8601
  updatedAt: string,       // Timestamp ISO 8601
  attempts: number         // Conteo de intentos de entrega (0 a 5)
}

Eventos de Webhook

KillB soporta los siguientes tipos de eventos:
Eventos de transacción rampTipo de Evento: RAMPAcciones:
  • CREATE - Nuevo ramp creado
  • UPDATE - Estado del ramp cambió
  • DELETE - Ramp cancelado (raro)
Valores de Estado Comunes:
  • QUOTE_CREATED - Cotización creada
  • PENDING_PAYMENT - Esperando pago
  • CASH_IN_PROCESSING - Pago siendo procesado
  • CASH_IN_COMPLETED - Pago confirmado
  • PROCESSING - Convirtiendo moneda
  • CASH_OUT_PROCESSING - Enviando fondos
  • COMPLETED - Ramp completado
  • FAILED - Ramp falló
  • CANCELED - Ramp cancelado

Configurando Webhooks

Crear Configuración de Webhook

POST /api/v2/webhooks
Solicitud:
{
  "url": "https://api.tuapp.com/webhooks/killb",
  "secret": "tu-clave-secreta-min-32-caracteres",
  "events": ["RAMP", "USER", "ACCOUNT"]
}
Respuesta:
{
  "id": "webhook-id",
  "customerId": "customer-id",
  "url": "https://api.tuapp.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.tuapp.com/webhooks/killb',
      secret: process.env.WEBHOOK_SECRET,
      events: ['RAMP', 'USER', 'ACCOUNT', 'TRANSACTION']
    })
  });
  
  return await response.json();
};

Seguridad de Webhook

Verificación de Firma

KillB firma todas las solicitudes webhook con HMAC SHA-256. Siempre verifica firmas para asegurar autenticidad. Encabezado: 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();
  
  // Calcular firma esperada
  const expectedSignature = crypto
    .createHmac('sha256', process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest('hex');
  
  // Verificar firma
  if (signature !== expectedSignature) {
    console.error('Firma de webhook inválida');
    return res.status(401).json({ error: 'Firma inválida' });
  }
  
  // Analizar y procesar evento
  const event = JSON.parse(payload);
  processWebhookEvent(event);
  
  res.status(200).json({ received: true });
});
¡Nunca omitas la verificación de firma! Webhooks no verificados pueden ser falsificados por atacantes.

Requisitos de Webhook

Tu endpoint de webhook debe:
  • Retornar 200 OK dentro de 5 segundos
  • Procesar eventos asincrónicamente
  • No esperar por operaciones lentas
  • Usar colas de jobs para procesamiento pesado
  • Ser idempotente (manejar eventos duplicados)
  • Usar ID del evento para detectar duplicados
  • Almacenar IDs de eventos procesados
  • Saltar eventos ya procesados
  • Mantener >99% de uptime
  • Usar solo HTTPS
  • Tener certificado SSL válido
  • Responder con códigos de estado apropiados
  • Siempre verificar x-signature-sha256
  • Usar comparación de tiempo constante
  • Rechazar firmas inválidas
  • Registrar fallas de verificación

Lógica de Reintento

KillB automáticamente reintenta entregas de webhook fallidas con backoff exponencial: Agenda de Reintento:
  • Intento 0: Entrega inmediata
  • Intento 1: Después de 1 minuto
  • Intento 2: Después de 5 minutos
  • Intento 3: Después de 15 minutos
  • Intento 4: Después de 1 hora
  • Intento 5: Después de 6 horas (intento final)
Criterios de Falla:
  • Código de respuesta no-2xx
  • Timeout de conexión (> 5 segundos)
  • Error de red
El campo attempts en el payload del webhook indica el intento de entrega actual (0 a 5).
Después de que el intento 5 falla, el webhook se marca como permanentemente fallido y requiere intervención manual. Verifica tus logs de webhook y corrige cualquier problema.

Probando Webhooks

Prueba Local con ngrok

# Instalar ngrok
npm install -g ngrok

# Iniciar tu servidor local
node server.js

# Exponer a internet
ngrok http 3000

# Usar URL ngrok en configuración de webhook
# https://abc123.ngrok.io/webhooks/killb

Prueba Manual

Activa eventos de prueba en sandbox:
// Crear ramp de prueba
const ramp = await createRamp(quoteId, userId, accountId);

// Simular finalización (activa webhooks)
await fetch('/api/v2/faker/cash-in', {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${token}` },
  body: JSON.stringify({ rampId: ramp.id })
});

// Tu webhook debe recibir evento ramp.cash_in_completed

Procesamiento de Eventos

Ejemplo de Handler

const processWebhookEvent = (webhookEvent) => {
  const { id, event, action, data, attempts } = webhookEvent;
  
  console.log(`Procesando ${event}.${action} (intento ${attempts})`);
  
  // Enrutar basado en tipo de evento y acción
  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;
      
    default:
      console.log(`Tipo de evento no manejado: ${event}`);
  }
};

const handleRampEvent = async (action, data) => {
  if (action === 'UPDATE' && data.status === 'COMPLETED') {
    // Actualizar base de datos
    await db.ramps.update(data.id, {
      status: data.status,
      updatedAt: data.updatedAt,
      transferProof: data.transferProof
    });
    
    // Notificar usuario
    await sendEmail(data.userId, 'Transacción Completada', {
      amount: data.toAmount,
      currency: data.toCurrency,
      txHash: data.transferProof
    });
  }
};

Idempotencia

Maneja entregas de webhook duplicadas usando el ID del evento:
const handleWebhook = async (webhookEvent) => {
  const { id, event, action, data } = webhookEvent;
  
  // Verificar si ya procesado (usando ID único del evento)
  const existing = await db.webhookEvents.findOne({ eventId: id });
  
  if (existing) {
    console.log(`Evento duplicado ${id}, saltando`);
    return { received: true, duplicate: true };
  }
  
  // Almacenar evento inmediatamente para prevenir procesamiento duplicado
  await db.webhookEvents.create({
    eventId: id,
    eventType: event,
    action: action,
    payload: data,
    receivedAt: new Date(),
    processed: false
  });
  
  try {
    // Procesar evento
    await processWebhookEvent(webhookEvent);
    
    // Marcar como procesado
    await db.webhookEvents.update(
      { eventId: id },
      { processed: true, processedAt: new Date() }
    );
    
    return { received: true };
  } catch (error) {
    // Registrar error pero aún marcar como recibido
    await db.webhookEvents.update(
      { eventId: id },
      { error: error.message, failedAt: new Date() }
    );
    
    throw error;
  }
};

Mejores Prácticas

app.post('/webhooks/killb', async (req, res) => {
  // Verificar firma
  verifySignature(req);
  
  // Reconocer inmediatamente
  res.status(200).json({ received: true });
  
  // Procesar asincrónicamente
  processAsync(req.body);
});
  • Retornar 200 OK inmediatamente
  • Procesar eventos en segundo plano
  • Usar colas de jobs (Bull, Celery, etc.)
  • No esperar por servicios externos
const processWebhook = async (event) => {
  try {
    await processEvent(event);
  } catch (error) {
    console.error('Procesamiento de webhook falló:', error);
    // Registrar en servicio de monitoreo
    await logError(error, event);
    // No lanzar - ya procesamos
    // KillB reintentará de todos modos
  }
};
  • Capturar todos los errores
  • Registrar fallas
  • No lanzar después de reconocer
  • Monitorear tasas de error
await db.webhookLogs.create({
  eventId: event.id,
  eventType: event.event,
  payload: event.data,
  receivedAt: new Date(),
  processed: true
});
  • Mantener pista de auditoría
  • Habilitar replay si es necesario
  • Depurar problemas fácilmente
  • Cumplir requisitos de compliance

Próximos Pasos