# SAAS-010: Webhooks ## Metadata - **Codigo:** SAAS-010 - **Modulo:** Webhooks - **Prioridad:** P2 - **Estado:** Pendiente - **Fase:** 5 - Integraciones ## Descripcion Sistema de webhooks outbound: configuracion de endpoints por tenant, eventos suscribibles, firma de payloads, reintentos automaticos, y logs de entregas. ## Objetivos 1. Configuracion de webhooks por tenant 2. Eventos suscribibles 3. Firma HMAC de payloads 4. Reintentos con backoff 5. Dashboard de entregas ## Alcance ### Incluido - CRUD de webhooks por tenant - Eventos de sistema suscribibles - Firma HMAC-SHA256 - Reintentos exponenciales (max 5) - Logs de entregas - Test endpoint ### Excluido - Transformacion de payloads - Webhooks inbound (recibir) - Fanout a multiples endpoints por evento ## Modelo de Datos ### Tablas (schema: webhooks) **webhooks** - id, tenant_id, name - url, secret (encrypted) - events (JSONB array) - headers (JSONB) - is_active, created_at **webhook_deliveries** - id, webhook_id, event_type - payload (JSONB) - response_status, response_body - attempt, next_retry_at - delivered_at, created_at ## Eventos Disponibles | Evento | Descripcion | Payload | |--------|-------------|---------| | user.created | Usuario creado | User object | | user.updated | Usuario actualizado | User + changes | | user.deleted | Usuario eliminado | { userId } | | subscription.created | Nueva suscripcion | Subscription | | subscription.updated | Suscripcion cambiada | Subscription | | subscription.cancelled | Suscripcion cancelada | Subscription | | invoice.paid | Factura pagada | Invoice | | invoice.failed | Pago fallido | Invoice | ## Endpoints API | Metodo | Endpoint | Descripcion | |--------|----------|-------------| | GET | /webhooks | Listar webhooks | | GET | /webhooks/:id | Obtener webhook | | POST | /webhooks | Crear webhook | | PUT | /webhooks/:id | Actualizar webhook | | DELETE | /webhooks/:id | Eliminar webhook | | POST | /webhooks/:id/test | Enviar test | | GET | /webhooks/:id/deliveries | Historial entregas | | POST | /webhooks/:id/deliveries/:did/retry | Reintentar | | GET | /webhooks/events | Eventos disponibles | ## Firma de Payloads ### Generacion ```typescript function signPayload(payload: object, secret: string): string { const timestamp = Date.now(); const body = JSON.stringify(payload); const signature = crypto .createHmac('sha256', secret) .update(`${timestamp}.${body}`) .digest('hex'); return `t=${timestamp},v1=${signature}`; } ``` ### Headers Enviados ``` X-Webhook-Signature: t=1704067200000,v1=abc123... X-Webhook-Id: wh_123456 X-Webhook-Event: user.created X-Webhook-Timestamp: 1704067200000 ``` ### Verificacion (lado receptor) ```typescript function verifySignature(payload: string, signature: string, secret: string): boolean { const [timestamp, hash] = parseSignature(signature); // Verificar timestamp (5 min tolerance) if (Date.now() - timestamp > 300000) return false; const expected = crypto .createHmac('sha256', secret) .update(`${timestamp}.${payload}`) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(hash), Buffer.from(expected) ); } ``` ## Logica de Reintentos ``` Intento 1: Inmediato Intento 2: +1 minuto Intento 3: +5 minutos Intento 4: +30 minutos Intento 5: +2 horas Despues: Marcar como fallido ``` ### Status de Entrega | Status | Descripcion | |--------|-------------| | pending | En cola | | delivered | Entregado (2xx) | | failed | Fallo permanente | | retrying | En reintento | ## Implementacion ### Dispatcher ```typescript @Injectable() export class WebhookDispatcher { async dispatch(tenantId: string, event: string, data: object): Promise { const webhooks = await this.getActiveWebhooks(tenantId, event); for (const webhook of webhooks) { await this.webhookQueue.add('deliver', { webhookId: webhook.id, event, payload: data, attempt: 1 }); } } } ``` ### Worker ```typescript @Processor('webhooks') export class WebhookWorker { @Process('deliver') async deliver(job: Job): Promise { const { webhookId, event, payload, attempt } = job.data; const webhook = await this.getWebhook(webhookId); const signature = this.signPayload(payload, webhook.secret); try { const response = await axios.post(webhook.url, payload, { headers: { 'X-Webhook-Signature': signature, 'X-Webhook-Event': event, ...webhook.headers }, timeout: 30000 }); await this.logDelivery(webhookId, event, payload, response, 'delivered'); } catch (error) { await this.handleFailure(job, error); } } } ``` ## Limites por Plan | Plan | Webhooks | Eventos/mes | |------|----------|-------------| | Free | 0 | 0 | | Starter | 0 | 0 | | Pro | 5 | 10,000 | | Enterprise | 20 | 100,000 | ## Entregables | Entregable | Estado | Archivo | |------------|--------|---------| | webhooks.module.ts | Pendiente | `modules/webhooks/` | | webhook.service.ts | Pendiente | `services/` | | webhook.dispatcher.ts | Pendiente | `services/` | | webhook.worker.ts | Pendiente | `workers/` | | DDL webhooks schema | Pendiente | `ddl/schemas/webhooks/` | ## Dependencias ### Depende de - SAAS-002 (Tenants) - SAAS-005 (Plans - feature flag) - BullMQ para queue ### Bloquea a - Integraciones de terceros - Automatizaciones externas ## Criterios de Aceptacion - [ ] CRUD webhooks funciona - [ ] Eventos se disparan - [ ] Firma es correcta - [ ] Reintentos funcionan - [ ] Test endpoint funciona - [ ] Logs se registran ## Seguridad - Secrets encriptados en BD - HTTPS requerido para URLs - Timeout de 30 segundos - Rate limit por tenant - No seguir redirects --- **Ultima actualizacion:** 2026-01-07