docs(integrations): complete API specification and n8n HMAC verification snippet
This commit is contained in:
364
docs/API_INTEGRACIONES.md
Normal file
364
docs/API_INTEGRACIONES.md
Normal file
@@ -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: <TU_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: <N8N_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: <N8N_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: <N8N_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: <N8N_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: <N8N_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: <N8N_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: <CLAUDE_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: <CLAUDE_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: <CLAUDE_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: <hex_digest_hmac_sha256>
|
||||
```
|
||||
|
||||
### 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
|
||||
}
|
||||
};
|
||||
```
|
||||
Reference in New Issue
Block a user