--- id: INT-014 type: Integration title: "Webhooks Outbound" provider: "BullMQ" status: Planificado integration_type: "events" created_at: 2026-01-10 updated_at: 2026-01-10 simco_version: "4.0.1" tags: - webhooks - events - bullmq - integration - outbound --- # INT-014: Webhooks Outbound ## Metadata | Campo | Valor | |-------|-------| | **Codigo** | INT-014 | | **Proveedor** | BullMQ (interno) | | **Tipo** | Eventos | | **Estado** | Planificado | | **Multi-tenant** | Si | | **Epic Relacionada** | MCH-029 | | **Owner** | Backend Team | --- ## 1. Descripcion Sistema de webhooks outbound que permite a tenants recibir notificaciones HTTP cuando ocurren eventos en el sistema. Incluye firma de payloads, reintentos con backoff y logs de entrega. **Casos de uso principales:** - Notificar sistemas externos de nuevos pedidos - Sincronizar inventario con ERP - Integraciones con Zapier/Make - Alertas personalizadas --- ## 2. Eventos Disponibles ### Eventos por Categoria | Categoria | Evento | Payload | |-----------|--------|---------| | **Orders** | order.created | Order completa | | | order.updated | Order con cambios | | | order.completed | Order finalizada | | | order.cancelled | Order cancelada | | **Products** | product.created | Producto nuevo | | | product.updated | Producto modificado | | | product.deleted | Producto eliminado | | | product.low_stock | Stock bajo minimo | | **Payments** | payment.received | Pago recibido | | | payment.failed | Pago fallido | | | payment.refunded | Pago reembolsado | | **Customers** | customer.created | Cliente nuevo | | | customer.updated | Cliente modificado | --- ## 3. Configuracion de Endpoints ### API de Configuracion ```typescript // POST /api/webhooks/endpoints { "url": "https://example.com/webhook", "events": ["order.created", "payment.received"], "secret": "whsec_xxxxxxxx", // Generado si no se provee "description": "Mi integracion", "is_active": true } // Response { "id": "wh_123abc", "url": "https://example.com/webhook", "events": ["order.created", "payment.received"], "secret": "whsec_xxxxxxxx", "is_active": true, "created_at": "2026-01-10T10:00:00Z" } ``` ### Endpoints CRUD | Metodo | Ruta | Descripcion | |--------|------|-------------| | GET | /api/webhooks/endpoints | Listar endpoints | | POST | /api/webhooks/endpoints | Crear endpoint | | GET | /api/webhooks/endpoints/:id | Obtener endpoint | | PATCH | /api/webhooks/endpoints/:id | Actualizar endpoint | | DELETE | /api/webhooks/endpoints/:id | Eliminar endpoint | | GET | /api/webhooks/deliveries | Listar entregas | | POST | /api/webhooks/endpoints/:id/test | Enviar test | --- ## 4. Payload de Webhook ### Estructura ```json { "id": "evt_123abc", "type": "order.created", "created_at": "2026-01-10T10:00:00Z", "data": { "id": "ord_456def", "total": 150.00, "items": [...], "customer": {...} }, "tenant_id": "550e8400-e29b-41d4-a716-446655440000" } ``` ### Headers | Header | Valor | Descripcion | |--------|-------|-------------| | `Content-Type` | application/json | Tipo de contenido | | `X-Webhook-Id` | evt_123abc | ID del evento | | `X-Webhook-Timestamp` | 1704880800 | Unix timestamp | | `X-Webhook-Signature` | sha256=xxx | Firma HMAC | | `User-Agent` | MiChangarrito/1.0 | Identificador | --- ## 5. Firma de Payloads ### Generacion de Firma ```typescript function signPayload( payload: string, secret: string, timestamp: number, ): string { const signedPayload = `${timestamp}.${payload}`; const signature = crypto .createHmac('sha256', secret) .update(signedPayload) .digest('hex'); return `sha256=${signature}`; } ``` ### Verificacion en Cliente ```typescript // Ejemplo para el receptor del webhook function verifySignature( payload: string, signature: string, secret: string, timestamp: number, tolerance: number = 300, // 5 minutos ): boolean { const currentTime = Math.floor(Date.now() / 1000); // Verificar que no sea muy viejo if (currentTime - timestamp > tolerance) { throw new Error('Timestamp too old'); } const expected = signPayload(payload, secret, timestamp); return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected), ); } ``` --- ## 6. Estrategia de Reintentos ### Exponential Backoff ``` Intento 1: Inmediato Intento 2: 1 segundo Intento 3: 2 segundos Intento 4: 4 segundos Intento 5: 8 segundos Intento 6: 16 segundos (maximo) ``` ### Configuracion BullMQ ```typescript await this.webhookQueue.add('deliver', { endpointId: endpoint.id, eventId: event.id, payload: event.data, }, { attempts: 6, backoff: { type: 'exponential', delay: 1000, }, removeOnComplete: true, removeOnFail: false, // Mantener para logs }); ``` ### Codigos de Respuesta | Codigo | Accion | Descripcion | |--------|--------|-------------| | 2xx | Exito | Entrega exitosa | | 3xx | Retry | Seguir redirecciones | | 4xx | Fallo | No reintentar (excepto 429) | | 429 | Retry | Rate limited, esperar | | 5xx | Retry | Error del servidor | | Timeout | Retry | Esperar siguiente intento | --- ## 7. Tabla de BD ### Schema webhooks ```sql CREATE SCHEMA IF NOT EXISTS webhooks; CREATE TABLE webhooks.endpoints ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, url VARCHAR(2000) NOT NULL, description VARCHAR(255), events TEXT[] NOT NULL, secret VARCHAR(255) NOT NULL, is_active BOOLEAN DEFAULT true, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); CREATE TABLE webhooks.deliveries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), endpoint_id UUID REFERENCES webhooks.endpoints(id), event_type VARCHAR(100) NOT NULL, event_id VARCHAR(100) NOT NULL, payload JSONB NOT NULL, status VARCHAR(20) NOT NULL, -- pending, success, failed attempts INTEGER DEFAULT 0, last_attempt_at TIMESTAMP WITH TIME ZONE, response_status INTEGER, response_body TEXT, error_message TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), completed_at TIMESTAMP WITH TIME ZONE ); CREATE INDEX idx_deliveries_endpoint ON webhooks.deliveries(endpoint_id); CREATE INDEX idx_deliveries_status ON webhooks.deliveries(status); ``` --- ## 8. Procesador de Webhooks ```typescript @Processor('webhooks') export class WebhookProcessor { constructor( private readonly httpService: HttpService, private readonly webhookService: WebhookService, ) {} @Process('deliver') async handleDelivery(job: Job) { const { endpointId, eventId, payload } = job.data; const endpoint = await this.webhookService.getEndpoint(endpointId); if (!endpoint.is_active) return; const timestamp = Math.floor(Date.now() / 1000); const payloadString = JSON.stringify(payload); const signature = this.signPayload(payloadString, endpoint.secret, timestamp); try { const response = await this.httpService.axiosRef.post( endpoint.url, payload, { headers: { 'Content-Type': 'application/json', 'X-Webhook-Id': eventId, 'X-Webhook-Timestamp': timestamp.toString(), 'X-Webhook-Signature': signature, 'User-Agent': 'MiChangarrito/1.0', }, timeout: 30000, // 30 segundos }, ); await this.webhookService.logDelivery(endpointId, eventId, { status: 'success', responseStatus: response.status, attempts: job.attemptsMade + 1, }); } catch (error) { const shouldRetry = this.shouldRetry(error); await this.webhookService.logDelivery(endpointId, eventId, { status: shouldRetry ? 'pending' : 'failed', responseStatus: error.response?.status, errorMessage: error.message, attempts: job.attemptsMade + 1, }); if (shouldRetry) { throw error; // BullMQ reintentara } } } private shouldRetry(error: any): boolean { if (!error.response) return true; // Timeout o network error const status = error.response.status; return status === 429 || status >= 500; } } ``` --- ## 9. UI de Administracion ### Funcionalidades - Lista de endpoints configurados - Crear/editar/eliminar endpoints - Ver historial de entregas - Reintentar entregas fallidas - Enviar evento de prueba - Rotar secret ### Ejemplo de Vista ``` +--------------------------------------------------+ | Webhooks [+ Nuevo] | +--------------------------------------------------+ | URL | Eventos | Estado | |------------------------|--------------|----------| | https://example.com/wh | order.* | Activo | | https://zapier.com/... | payment.* | Activo | | https://erp.local/api | product.* | Inactivo | +--------------------------------------------------+ Entregas Recientes: +--------------------------------------------------+ | Evento | Destino | Estado | Fecha | |-----------------|--------------|--------|--------| | order.created | example.com | OK | 10:00 | | payment.failed | zapier.com | OK | 09:55 | | product.updated | erp.local | Failed | 09:50 | +--------------------------------------------------+ ``` --- ## 10. Testing ### Enviar Evento de Prueba ```typescript // POST /api/webhooks/endpoints/:id/test { "event_type": "test.webhook" } // Payload enviado { "id": "evt_test_123", "type": "test.webhook", "created_at": "2026-01-10T10:00:00Z", "data": { "message": "This is a test webhook" } } ``` ### Herramientas de Debug - [webhook.site](https://webhook.site) - Receptor de prueba - [ngrok](https://ngrok.com) - Tunel para localhost --- ## 11. Monitoreo ### Metricas | Metrica | Descripcion | Alerta | |---------|-------------|--------| | webhook_deliveries_total | Total entregas | - | | webhook_deliveries_success | Entregas exitosas | - | | webhook_deliveries_failed | Entregas fallidas | > 10% | | webhook_delivery_latency | Latencia de entrega | > 5s | | webhook_queue_length | Jobs pendientes | > 100 | --- ## 12. Referencias - [Webhooks Best Practices](https://webhooks.dev/best-practices) - [BullMQ Documentation](https://docs.bullmq.io/) - [Stripe Webhooks](https://stripe.com/docs/webhooks) (referencia) - [ADR-0007: Webhook Retry Strategy](../97-adr/ADR-0007-webhook-retry-strategy.md) --- **Ultima actualizacion:** 2026-01-10 **Autor:** Backend Team