Nuevas Épicas (MCH-029 a MCH-033): - Infraestructura SaaS multi-tenant - Auth Social (OAuth2) - Auditoría Empresarial - Feature Flags - Onboarding Wizard Nuevas Integraciones (INT-010 a INT-014): - Email Providers (SendGrid, Mailgun, SES) - Storage Cloud (S3, GCS, Azure) - OAuth Social - Redis Cache - Webhooks Outbound Nuevos ADRs (0004 a 0011): - Notifications Realtime - Feature Flags Strategy - Storage Abstraction - Webhook Retry Strategy - Audit Log Retention - Rate Limiting - OAuth Social Implementation - Email Multi-provider Actualizados: - MASTER_INVENTORY.yml - CONTEXT-MAP.yml - HERENCIA-SIMCO.md - Mapas de documentación Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
430 lines
10 KiB
Markdown
430 lines
10 KiB
Markdown
---
|
|
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<WebhookJobData>) {
|
|
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
|