Skip to Content
Temas AvanzadosWebhooks en Profundidad

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

gaugeEficiente

Reduce llamadas API innecesarias

arrow-upEscalable

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

const express = require('express'); const crypto = require('crypto'); const app = express(); // IMPORTANTE: Usar express.raw para verificación de firma app.post('/webhooks/koywe', express.raw({ type: 'application/json' }), async (req, res) => { try { // 1. Verificar firma const signature = req.headers['koywe-signature']; const secret = process.env.KOYWE_WEBHOOK_SECRET; if (!verifyWebhookSignature(req.body, signature, secret)) { console.error('Firma de webhook inválida'); return res.status(401).send('Firma inválida'); } // 2. Parsear evento const event = JSON.parse(req.body.toString()); // 3. Procesar evento await processWebhookEvent(event); // 4. Responder rápido res.status(200).send('OK'); } catch (error) { console.error('Error procesando webhook:', error); res.status(500).send('Error interno'); } } ); function verifyWebhookSignature(payload, signature, secret) { const expectedSignature = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return signature === expectedSignature; } async function processWebhookEvent(event) { console.log('Evento recibido:', event.type); switch (event.type) { case 'order.completed': await handleOrderCompleted(event.data); break; case 'order.failed': await handleOrderFailed(event.data); break; // ... otros tipos de evento } }
from flask import Flask, request import hmac import hashlib import json app = Flask(__name__) @app.route('/webhooks/koywe', methods=['POST']) def webhook(): try: # 1. Verificar firma signature = request.headers.get('koywe-signature') secret = os.getenv('KOYWE_WEBHOOK_SECRET') if not verify_webhook_signature(request.data, signature, secret): return 'Firma inválida', 401 # 2. Parsear evento event = json.loads(request.data) # 3. Procesar evento process_webhook_event(event) # 4. Responder rápido return 'OK', 200 except Exception as e: print(f"Error procesando webhook: {e}") return 'Error interno', 500 def verify_webhook_signature(payload, signature, secret): expected_signature = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest() return signature == expected_signature

Tipos de Eventos

Koywe emite eventos en varias familias de recursos. Todos los eventos comparten la misma envoltura superior (id, type, version, occurred_at, source, environment, organization_id, merchant_id, data) que se muestra en la siguiente sección — pero el contenido de data varía según la familia del evento. Los campos como orderId y amountIn del ejemplo siguiente son específicos de la familia order.*; no aplican a merchant.*, policy.*, invitation.*, bank_income.* ni webhook.ping, que llevan su propia estructura de data específica del recurso. La taxonomía es aditiva — pueden introducirse nuevos eventos con el tiempo, y cualquier cambio incompatible se señala en el campo version de la envoltura.

Ciclo de Vida de la Orden

Tipo de EventoCuándo se Dispara
order.createdLa orden fue creada
order.approvedLa aprobación de política se resolvió; la orden pasa a PROCESSING
order.processingEl pago o transferencia está siendo procesado
order.paidPago confirmado (por el proveedor de pagos o confirmación en cadena)
order.completedFondos liquidados — acreditados en PAYIN/ONRAMP, debitados en PAYOUT/OFFRAMP
order.failedLa orden falló en alguna etapa (pago, ledger, transferencia)
order.expiredLa orden pasó su fecha de vencimiento sin ser pagada
order.canceledLa orden fue cancelada por el usuario o el sistema
order.refundedLa orden fue reembolsada tras haber sido completada
order.updatedFallback genérico para cambios de estado no cubiertos arriba

Ortografía: La forma canónica es order.canceled (una sola l). Borradores internos previos usaron order.cancelled — si tienes handlers con esa ortografía, cámbialos a order.canceled.

Merchant y KYB

Tipo de EventoCuándo se Dispara
merchant.createdSe creó un merchant dentro de una organización
merchant.kyb.in_progressSe inició la revisión de KYB del merchant
merchant.kyb.approvedKYB aprobado; el merchant puede operar en producción
merchant.kyb.rejectedKYB rechazado; se requiere remediación

Invitaciones

Se emiten tanto para invitaciones a nivel de usuario como a nivel de organización (se distinguen por el resourceType del payload: invitation vs organization_invitation).

Tipo de EventoCuándo se Dispara
invitation.createdSe emitió una invitación
invitation.acceptedEl destinatario aceptó la invitación
invitation.expiredLa invitación expiró sin ser aceptada
invitation.failedLa entrega o procesamiento de la invitación falló
invitation.assignedEl destino de la invitación fue resuelto/asignado a un usuario

Aprobaciones de Políticas

Se emiten cuando una operación protegida atraviesa el flujo de aprobación de políticas.

Tipo de EventoCuándo se Dispara
policy.approval.requestedSe disparó una política; se notificó a uno o más aprobadores
policy.approval.receivedUn aprobador individual respondió (aprobar/rechazar)
policy.approval.approvedSe alcanzó el umbral; la solicitud queda aprobada
policy.approval.rejectedLa política rechazó la solicitud

Ejecución de Políticas

Un evento dinámico aparte se dispara cuando una política finalmente se ejecuta sobre su recurso objetivo. El tipo de evento incrusta el tipo de recurso:

policy.{resourceType}.executed

Tipos de recurso actualmente emitidos:

Tipo de EventoRecurso
policy.order.executedOrden
policy.account.executedCuenta bancaria
policy.deal.executedDeal
policy.user_invite.executedInvitación de usuario
policy.passkey_enrollment.executedEnrolamiento de passkey
policy.wallet_access_policy.executedPolítica de acceso a wallet
policy.policy.executedDefinición de política
policy.policy_rule.executedRegla de política

Si quieres manejar todas las ejecuciones de política de forma genérica, haz match contra el patrón policy.*.executed en lugar de enumerar cada tipo de recurso — se suman nuevos a medida que más recursos quedan protegidos por política.

Ingresos Bancarios

Tipo de EventoCuándo se Dispara
bank_income.receivedSe acreditó un depósito en una cuenta bancaria virtual — se dispara tanto para transferencias reales como para simulaciones en sandbox

Sistema

Tipo de EventoCuándo se Dispara
webhook.pingEvento de prueba emitido por el endpoint ping del webhook; úsalo para verificar conectividad y validación de firma

Estructura de Payload

{ "id": "evt_abc123", "type": "order.completed", "version": "v1", "occurred_at": "2025-11-13T15:30:00Z", "source": "koywe.api", "environment": "production", "organization_id": "org_xyz", "merchant_id": "mrc_abc", "data": { "orderId": "ord_123456", "type": "PAYIN", "status": "COMPLETED", "amountIn": 50000, "amountOut": 50000, "originCurrencySymbol": "COP", "destinationCurrencySymbol": "COP", "externalId": "order-12345", "createdAt": "2025-11-13T15:00:00Z", "completedAt": "2025-11-13T15:30:00Z" } }

Política de Reintentos

Koywe envía cada webhook una sola vez, y reintenta únicamente si la falla parece transitoria. Las reglas son:

  • Timeout de la solicitud: 30 segundos. Respuestas más lentas se tratan como falla de red.
  • Fallas reintentables — la entrega se reintenta con backoff:
    • Respuestas 5xx desde tu endpoint
    • 429 Too Many Requests
    • Errores de red y timeouts (sin respuesta HTTP)
  • Fallas no reintentables — la entrega queda marcada como FAILED tras un único intento, sin reintentos:
    • Cualquier otro 4xx (p. ej. 400, 401, 403, 404, 422)

Las fallas reintentables se reencolan con backoff. Cada intento incrementa retryCount y se agrega a attempts[] en la entrega; cuando se agota el presupuesto de reintentos, deliveryStatus pasa a FAILED — esto aplica también a 429, por lo que devolver 429 señala throttling pero no exime a la entrega de quedar marcada como FAILED si se agotan los reintentos. Puedes inspeccionar el estado con GET /api/v1/organizations/:organizationId/webhook-events/:eventId/deliveries, y reenviar manualmente un evento fallido con el endpoint de replay.

Los reintentos solo aplican antes del ACK. La lógica de reintentos automáticos de Koywe (5xx / 429 / timeout de red) solo se activa cuando recibe una respuesta de error — o ninguna respuesta — desde tu endpoint. Una vez que devuelves 200 OK, la entrega se considera exitosa y no se reintenta, aunque tu procesamiento en segundo plano falle después. Si usas el patrón de ACK inmediato (ver Rendimiento más abajo), debes persistir de forma durable el evento (por ej. encolar en una cola de trabajos confiable o hacer commit a una base de datos) antes de devolver 200 OK; de lo contrario, una caída entre el ACK y el procesamiento hará que pierdas el evento y Koywe no lo reenviará automáticamente — tendrás que invocar el endpoint de replay manualmente.

4xx es terminal. Si tu endpoint devuelve 401 porque todavía no aplicaste una rotación de secreto, o 404 por un typo en la ruta, Koywe no reintenta — asume que la solicitud está fundamentalmente mal. Arregla el endpoint y usa POST /api/v1/organizations/:organizationId/webhook-events/:eventId/replay para reenviar. Devuelve 5xx si quieres que Koywe reintente automáticamente (por ej. ante caídas transitorias aguas abajo). Devuelve 429 para señalar throttling — los reintentos siguen aplicando con backoff, y la entrega puede igualmente quedar marcada como FAILED una vez agotado el presupuesto de reintentos.

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

Implementar Idempotencia

const redis = require('redis'); const client = redis.createClient(); async function processWebhookEvent(event) { const eventId = event.id; // Verificar si ya procesamos este evento const alreadyProcessed = await client.get(`webhook:${eventId}`); if (alreadyProcessed) { console.log('Evento ya procesado, saltando'); return; } // Procesar evento await handleEvent(event); // Marcar como procesado (expirar después de 7 días) await client.setEx(`webhook:${eventId}`, 604800, 'processed'); }
async function processWebhookEvent(event) { const eventId = event.id; // Insertar con constraint único try { await db.query( 'INSERT INTO processed_webhooks (event_id, processed_at) VALUES ($1, NOW())', [eventId] ); } catch (error) { if (error.code === '23505') { // Violación de constraint único console.log('Evento ya procesado'); return; } throw error; } // Procesar evento await handleEvent(event); }

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:

const Bull = require('bull'); const webhookQueue = new Bull('webhooks'); // Endpoint de webhook - responde rápido app.post('/webhooks/koywe', express.raw({type: 'application/json'}), async (req, res) => { // Convertir body raw a string una vez const rawBody = req.body.toString(); if (!verifySignature(rawBody, req.headers['koywe-signature'])) { return res.status(401).send('Inválido'); } // Parsear JSON desde string const event = JSON.parse(rawBody); // Agregar a cola y responder inmediatamente await webhookQueue.add(event); res.status(200).send('OK'); }); // Procesador de trabajos - maneja eventos en background webhookQueue.process(async (job) => { const event = job.data; await processWebhookEvent(event); });

Monitoreo

async function logWebhookEvent(event, status, error = null) { await db.query(` INSERT INTO webhook_log ( event_id, event_type, status, error, received_at ) VALUES ($1, $2, $3, $4, NOW()) `, [event.id, event.type, status, error]); } app.post('/webhooks/koywe', async (req, res) => { const event = JSON.parse(req.body); try { await processWebhookEvent(event); await logWebhookEvent(event, 'success'); res.status(200).send('OK'); } catch (error) { await logWebhookEvent(event, 'error', error.message); res.status(500).send('Error'); } });

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

const processedEvents = new Set(); async function processWebhookEvent(event) { // Método en memoria simple (usar DB/Redis en producción) if (processedEvents.has(event.id)) { console.log('Evento duplicado, saltando'); return; } processedEvents.add(event.id); await handleEvent(event); }

Próximos Pasos

Last updated on