Webhooks - Guía Profunda

Implementación avanzada de webhooks para producción

Webhooks - Guía Profunda

Implementación avanzada de webhooks para entornos de producción.

Resumen de Webhooks

Los webhooks son notificaciones HTTP POST que Koywe envía a tu servidor cuando ocurren eventos (orden completada, pago recibido, etc.).

¿Por qué usar Webhooks?

Tiempo Real

Recibe notificaciones instantáneas de eventos

Confiable

No depende de polling o verificaciones manuales

Eficiente

Reduce llamadas API innecesarias

Escalable

Maneja miles de eventos sin sobrecarga


Configurar Webhooks

Endpoint de Webhook

Tu endpoint debe:

  • Ser públicamente accesible (HTTPS requerido en producción)
  • Responder rápidamente (< 5 segundos)
  • Devolver código de estado 200-299 para éxito
  • Manejar reintentos idempotentemente

Implementación Básica

1const express = require('express');
2const crypto = require('crypto');
3
4const app = express();
5
6// IMPORTANTE: Usar express.raw para verificación de firma
7app.post('/webhooks/koywe',
8 express.raw({ type: 'application/json' }),
9 async (req, res) => {
10 try {
11 // 1. Verificar firma
12 const signature = req.headers['koywe-signature'];
13 const secret = process.env.KOYWE_WEBHOOK_SECRET;
14
15 if (!verifyWebhookSignature(req.body, signature, secret)) {
16 console.error('Firma de webhook inválida');
17 return res.status(401).send('Firma inválida');
18 }
19
20 // 2. Parsear evento
21 const event = JSON.parse(req.body.toString());
22
23 // 3. Procesar evento
24 await processWebhookEvent(event);
25
26 // 4. Responder rápido
27 res.status(200).send('OK');
28
29 } catch (error) {
30 console.error('Error procesando webhook:', error);
31 res.status(500).send('Error interno');
32 }
33 }
34);
35
36function verifyWebhookSignature(payload, signature, secret) {
37 const expectedSignature = crypto
38 .createHmac('sha256', secret)
39 .update(payload)
40 .digest('hex');
41
42 return signature === expectedSignature;
43}
44
45async function processWebhookEvent(event) {
46 console.log('Evento recibido:', event.type);
47
48 switch (event.type) {
49 case 'order.completed':
50 await handleOrderCompleted(event.data);
51 break;
52 case 'order.failed':
53 await handleOrderFailed(event.data);
54 break;
55 // ... otros tipos de evento
56 }
57}

Tipos de Eventos

Eventos de Orden

TipoCuándo se DisparaDatos Incluidos
order.createdOrden creadaID de orden, tipo, monto
order.pendingEsperando pagoID de orden, URL de pago
order.processingPago siendo procesadoID de orden
order.paidPago confirmadoID de orden, monto
order.completedFondos liquidadosID de orden, monto out
order.failedOrden fallóID de orden, razón de error
order.expiredOrden expiróID de orden
order.cancelledOrden canceladaID de orden

Estructura de Payload

1{
2 "id": "evt_abc123",
3 "type": "order.completed",
4 "version": "v1",
5 "occurred_at": "2025-11-13T15:30:00Z",
6 "source": "koywe.api",
7 "environment": "production",
8 "organization_id": "org_xyz",
9 "merchant_id": "mrc_abc",
10 "data": {
11 "orderId": "ord_123456",
12 "type": "PAYIN",
13 "status": "COMPLETED",
14 "amountIn": 50000,
15 "amountOut": 50000,
16 "originCurrencySymbol": "COP",
17 "destinationCurrencySymbol": "COP",
18 "externalId": "order-12345",
19 "createdAt": "2025-11-13T15:00:00Z",
20 "completedAt": "2025-11-13T15:30:00Z"
21 }
22}

Lógica de Reintentos

Koywe reintenta webhooks fallidos con backoff exponencial:

IntentoTiempo DespuésTotal Acumulado
1Inmediato0 minutos
25 minutos5 minutos
315 minutos20 minutos
41 hora1 hora 20 minutos
56 horas7 horas 20 minutos

Idempotencia Crítica: Tu endpoint debe manejar el mismo evento múltiples veces de forma segura.

Implementar Idempotencia

1const redis = require('redis');
2const client = redis.createClient();
3
4async function processWebhookEvent(event) {
5 const eventId = event.id;
6
7 // Verificar si ya procesamos este evento
8 const alreadyProcessed = await client.get(`webhook:${eventId}`);
9
10 if (alreadyProcessed) {
11 console.log('Evento ya procesado, saltando');
12 return;
13 }
14
15 // Procesar evento
16 await handleEvent(event);
17
18 // Marcar como procesado (expirar después de 7 días)
19 await client.setEx(`webhook:${eventId}`, 604800, 'processed');
20}

Mejores Prácticas

Seguridad

Siempre haz esto:

  • ✅ Verificar firma de webhook
  • ✅ Usar HTTPS en producción
  • ✅ Validar estructura de payload
  • ✅ Implementar límite de tasa
  • ✅ Registrar todos los webhooks recibidos

Rendimiento

Procesamiento Asíncrono: Responde rápido (200 OK) y procesa eventos en background:

Cola de Trabajos
1const Bull = require('bull');
2const webhookQueue = new Bull('webhooks');
3
4// Endpoint de webhook - responde rápido
5app.post('/webhooks/koywe', express.raw({type: 'application/json'}), async (req, res) => {
6 // Convertir body raw a string una vez
7 const rawBody = req.body.toString();
8
9 if (!verifySignature(rawBody, req.headers['koywe-signature'])) {
10 return res.status(401).send('Inválido');
11 }
12
13 // Parsear JSON desde string
14 const event = JSON.parse(rawBody);
15
16 // Agregar a cola y responder inmediatamente
17 await webhookQueue.add(event);
18 res.status(200).send('OK');
19});
20
21// Procesador de trabajos - maneja eventos en background
22webhookQueue.process(async (job) => {
23 const event = job.data;
24 await processWebhookEvent(event);
25});

Monitoreo

Registrar Todos los Eventos
1async function logWebhookEvent(event, status, error = null) {
2 await db.query(`
3 INSERT INTO webhook_log (
4 event_id, event_type, status, error, received_at
5 ) VALUES ($1, $2, $3, $4, NOW())
6 `, [event.id, event.type, status, error]);
7}
8
9app.post('/webhooks/koywe', async (req, res) => {
10 const event = JSON.parse(req.body);
11
12 try {
13 await processWebhookEvent(event);
14 await logWebhookEvent(event, 'success');
15 res.status(200).send('OK');
16 } catch (error) {
17 await logWebhookEvent(event, 'error', error.message);
18 res.status(500).send('Error');
19 }
20});

Solución de Problemas

Webhooks No Recibidos

Verificar:

  1. Endpoint es públicamente accesible
  2. Firewall permite tráfico entrante
  3. SSL/TLS válido (en producción)
  4. Endpoint responde < 5 segundos
  5. Devuelve código 200-299

Webhooks Duplicados

Manejar Duplicados
1const processedEvents = new Set();
2
3async function processWebhookEvent(event) {
4 // Método en memoria simple (usar DB/Redis en producción)
5 if (processedEvents.has(event.id)) {
6 console.log('Evento duplicado, saltando');
7 return;
8 }
9
10 processedEvents.add(event.id);
11 await handleEvent(event);
12}

Próximos Pasos