From a17b1eae566c1d8ce94b86daa1d4cc4b6de9bff1 Mon Sep 17 00:00:00 2001 From: Marcos Fargas Date: Mon, 12 Oct 2026 13:47:47 +0200 Subject: [PATCH] docs(integrations): complete API specification and n8n HMAC verification snippet --- docs/API_INTEGRACIONES.md | 364 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 docs/API_INTEGRACIONES.md diff --git a/docs/API_INTEGRACIONES.md b/docs/API_INTEGRACIONES.md new file mode 100644 index 0000000..1e3a102 --- /dev/null +++ b/docs/API_INTEGRACIONES.md @@ -0,0 +1,364 @@ +# Especificación y Contrato de API — Integraciones n8n y Claude + +Esta documentación describe todos los endpoints disponibles en `https://app.4ventures.es/api/integrations/...` para la ingesta de métricas de Meta Ads, detección de fatiga, relevo creativo con Claude, aprobaciones y publicación. + +--- + +## 1. Autenticación y Seguridad + +Todas las peticiones a la API de integraciones deben incluir la cabecera `X-Api-Key`: +- **Para n8n:** Clave asignada en `N8N_API_KEY` +- **Para Claude:** Clave asignada en `CLAUDE_API_KEY` + +```http +X-Api-Key: +Content-Type: application/json +``` + +### Códigos de estado HTTP globales +- `200 OK`: Petición procesada correctamente. +- `400 Bad Request`: Parámetros obligatorios faltantes o payload JSON malformado. +- `401 Unauthorized`: Cabecera `X-Api-Key` ausente o no válida. +- `403 Forbidden`: El cliente (`client_id` / `tenant_id`) no tiene activo el flag `creatives_enabled = true`. +- `404 Not Found`: El cliente o recurso solicitado no existe. +- `500 Internal Server Error`: Error inesperado en el servidor. + +--- + +## 2. Endpoints para n8n (Ingesta, Fatiga y Publicación) + +### 2.1 Obtener Clientes Activos +Obtiene la lista de clientes que tienen activado el módulo de creatividades y su cuenta de anuncios de Meta. + +- **Método:** `GET` +- **Ruta:** `/api/integrations/clients?creatives_enabled=true` +- **Headers:** `X-Api-Key: ` + +#### Respuesta de ejemplo (`200 OK`): +```json +[ + { + "id": "8bec977a-6032-4759-aa7f-b5042081f450", + "name": "Blue Parrot School", + "slug": "bps", + "meta_ad_account_id": "act_1052328112282270", + "slack_channel_id": null, + "drive_folder_id": null, + "creatives_enabled": true, + "client_id": "8bec977a-6032-4759-aa7f-b5042081f450", + "tenant_id": "8bec977a-6032-4759-aa7f-b5042081f450" + } +] +``` + +--- + +### 2.2 Ingesta Diaria de Métricas (Bulk Upsert) +Inserta o actualiza las métricas por anuncio y fecha. Si el anuncio (`meta_ad_id`) no existía en la base de datos, lo crea automáticamente en estado `publicada`. + +- **Método:** `POST` +- **Ruta:** `/api/integrations/metrics/daily` +- **Headers:** `X-Api-Key: `, `Content-Type: application/json` + +#### Payload de ejemplo (`Request Body`): +```json +{ + "client_id": "8bec977a-6032-4759-aa7f-b5042081f450", + "metrics": [ + { + "meta_ad_id": "1202058493829102", + "date": "2026-10-11", + "campaign_name": "Matrículas Octubre 2026", + "impressions": 4200, + "reach": 3800, + "clicks": 135, + "spend": 54.20, + "results": 6, + "raw": { + "cpm": 12.90, + "ctr": 3.21 + } + } + ] +} +``` + +#### Respuesta de ejemplo (`200 OK`): +```json +{ + "ok": true, + "count": 1, + "rows": [ + { + "id": "7e417520-6089-4cc8-9e44-f08b4e948b2a", + "meta_ad_id": "1202058493829102", + "date": "2026-10-11" + } + ] +} +``` + +--- + +### 2.3 Obtener Candidatos de Fatiga +Devuelve las creatividades actualmente publicadas con sus métricas dentro de la ventana de análisis (`lookback_days`) y las reglas de fatiga del cliente. + +- **Método:** `GET` +- **Ruta:** `/api/integrations/fatigue/candidates?client_id=8bec977a-6032-4759-aa7f-b5042081f450` +- **Headers:** `X-Api-Key: ` + +#### Respuesta de ejemplo (`200 OK`): +```json +{ + "client_id": "8bec977a-6032-4759-aa7f-b5042081f450", + "tenant_id": "8bec977a-6032-4759-aa7f-b5042081f450", + "lookback_days": 7, + "rules": { + "lookback_days": 7, + "min_impressions": 2000, + "max_frequency": 3.0, + "ctr_drop_pct": 30.0, + "cpa_rise_pct": 25.0, + "signals_required": 2, + "auto_request_relief": true + }, + "candidates": [ + { + "id": "c05d4143-f19d-421a-b8d9-9b03d6b0889a", + "meta_ad_id": "1202058493829102", + "format": "estatico", + "status": "publicada", + "metrics": [ + { + "date": "2026-10-10", + "impressions": 3000, + "clicks": 90, + "spend": 45.0, + "results": 5 + } + ] + } + ] +} +``` + +--- + +### 2.4 Marcar Creatividad Fatigada +Cambia el estado de la creatividad a `fatigada`, guarda el evento de auditoría y, si `auto_request_relief` está activo, crea automáticamente una petición de relevo en `generation_requests`. + +- **Método:** `POST` +- **Ruta:** `/api/integrations/fatigue/mark` +- **Headers:** `X-Api-Key: `, `Content-Type: application/json` + +#### Payload de ejemplo (`Request Body`): +```json +{ + "client_id": "8bec977a-6032-4759-aa7f-b5042081f450", + "creative_id": "c05d4143-f19d-421a-b8d9-9b03d6b0889a", + "signals": { + "ctr_drop_pct": 32.5, + "frequency": 3.4 + } +} +``` + +#### Respuesta de ejemplo (`200 OK`): +```json +{ + "ok": true, + "creative_id": "c05d4143-f19d-421a-b8d9-9b03d6b0889a", + "status": "fatigada", + "signals": { "ctr_drop_pct": 32.5, "frequency": 3.4 }, + "generation_request": { + "id": "377cf30e-81e5-4d80-8f4f-f30a8dd2d239", + "status": "pendiente", + "reason": "fatiga", + "brief": { + "client_slug": "bps", + "variants_requested": 2, + "avoid_angles": ["objecion_precio"], + "format": "estatico", + "aspect_ratio": "4:5" + } + } +} +``` + +--- + +### 2.5 Listar Creatividades Aprobadas para Publicar +Permite a n8n consultar las creatividades aprobadas pendientes de subirse a Meta Ads. + +- **Método:** `GET` +- **Ruta:** `/api/integrations/creatives?status=aprobada&client_id=8bec977a-6032-4759-aa7f-b5042081f450` +- **Headers:** `X-Api-Key: ` + +#### Respuesta de ejemplo (`200 OK`): +```json +[ + { + "id": "1800fa1e-e969-469c-9913-6c973acba346", + "headline": "Aprende a tu ritmo", + "primary_text": "Clases de 25 minutos adaptadas a tu agenda laboral con nativos.", + "cta": "Empezar gratis", + "asset_url": "https://storage.4ventures.es/creatives/bps_v1.jpg", + "status": "aprobada", + "format": "estatico" + } +] +``` + +--- + +### 2.6 Notificar Publicación de Anuncio en Meta +Tras crear el anuncio en Meta Ads, n8n actualiza la creatividad con su identificador oficial y la pasa a `publicada`. + +- **Método:** `PATCH` +- **Ruta:** `/api/integrations/creatives/:id` +- **Headers:** `X-Api-Key: `, `Content-Type: application/json` + +#### Payload de ejemplo (`Request Body`): +```json +{ + "client_id": "8bec977a-6032-4759-aa7f-b5042081f450", + "meta_ad_id": "1202059998123456", + "status": "publicada" +} +``` + +#### Respuesta de ejemplo (`200 OK`): +```json +{ + "id": "1800fa1e-e969-469c-9913-6c973acba346", + "meta_ad_id": "1202059998123456", + "status": "publicada", + "updated_at": "2026-10-12T11:47:10.455Z" +} +``` + +--- + +## 3. Endpoints para Claude (Generación de Relevo Creativo) + +### 3.1 Obtener Contexto de la Creatividad Fatigada +Devuelve a Claude todos los datos necesarios para generar el relevo: el anuncio fatigado, sus métricas, las 3 mejores creatividades históricas del cliente y las reglas de fatiga. + +- **Método:** `GET` +- **Ruta:** `/api/integrations/creatives/:id/context?client_id=8bec977a-6032-4759-aa7f-b5042081f450` +- **Headers:** `X-Api-Key: ` + +--- + +### 3.2 Crear Variantes Generadas +Inserta las variantes generadas por Claude. Nacen en estado `borrador`, con visibilidad `agency` y `created_by = 'claude'`. + +- **Método:** `POST` +- **Ruta:** `/api/integrations/creatives` +- **Headers:** `X-Api-Key: `, `Content-Type: application/json` + +#### Payload de ejemplo: +```json +{ + "client_id": "8bec977a-6032-4759-aa7f-b5042081f450", + "generation_request_id": "377cf30e-81e5-4d80-8f4f-f30a8dd2d239", + "creatives": [ + { + "format": "estatico", + "angle": "objecion_horarios", + "hook": "¿Sin tiempo para aprender inglés?", + "primary_text": "Clases de 25 minutos adaptadas a tu agenda.", + "headline": "Aprende a tu ritmo", + "cta": "Empezar gratis", + "asset_url": "https://storage.4ventures.es/creatives/bps_v1.jpg" + } + ] +} +``` + +--- + +### 3.3 Completar Petición de Generación +Actualiza el estado de la tarea en la cola de Claude (`completada` o `fallida`), anotando los IDs generados y el coste estimado en tokens/créditos. + +- **Método:** `PATCH` +- **Ruta:** `/api/integrations/generation-requests/:id` +- **Headers:** `X-Api-Key: `, `Content-Type: application/json` + +#### Payload de ejemplo: +```json +{ + "client_id": "8bec977a-6032-4759-aa7f-b5042081f450", + "status": "completada", + "result": { + "creatives": ["1800fa1e-e969-469c-9913-6c973acba346"], + "notes": "Variante enfocada en flexibilidad horaria", + "skills_used": ["cliente-blue-parrot", "formato-estatico-4-5"] + }, + "cost_estimate": { "tokens": 8500, "credits": 1 } +} +``` + +--- + +## 4. Webhooks Salientes hacia n8n + +La aplicación emite llamadas HTTP POST hacia la URL configurada en `N8N_WEBHOOK_BASE` (`https://n8n-agencia.4ventures.es/webhook/creative-...`). + +Cada webhook se envía con la cabecera: +```http +X-Signature: +``` + +### Eventos emitidos: +1. `creative.approved`: Un usuario cliente o de agencia aprueba la creatividad. +2. `creative.rejected`: Un usuario rechaza la creatividad (requiere comentario obligatorio). +3. `creative.sent_to_client`: La agencia envía la creatividad al cliente para su revisión. + +--- + +## 5. Verificación de Firma HMAC en n8n (Nodo Code) + +En el flujo de n8n que recibe el webhook (`Webhook Node`), agrega un nodo **Code** en modo `Run Once for Each Item` con el siguiente código JavaScript para verificar la autenticidad antes de procesar la petición: + +```javascript +const crypto = require('crypto'); + +// 1. Obtener la firma enviada en la cabecera +const signature = $input.item.headers['x-signature']; +if (!signature) { + throw new Error('Cabecera X-Signature no proporcionada en el Webhook'); +} + +// 2. Secreto compartido (obtenido de las credenciales de n8n o variables de entorno) +const sharedSecret = $env['N8N_WEBHOOK_SECRET'] || 'whsec_4v_7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d'; + +// 3. Obtener el cuerpo recibido (raw string exacto o serializado) +const bodyPayload = typeof $input.item.body === 'string' + ? $input.item.body + : JSON.stringify($input.item.body); + +// 4. Calcular el HMAC esperado +const expectedSignature = crypto + .createHmac('sha256', sharedSecret) + .update(bodyPayload) + .digest('hex'); + +// 5. Comparación segura +const isValid = crypto.timingSafeEqual( + Buffer.from(signature), + Buffer.from(expectedSignature) +); + +if (!isValid) { + throw new Error('Firma HMAC no válida: webhook rechazado'); +} + +return { + json: { + verified: true, + event: $input.item.body.event, + data: $input.item.body.data + } +}; +```