{"openapi":"3.0.0","paths":{"/api/templates/submit":{"post":{"description":"Envía una plantilla de WhatsApp a revisión de Meta. La aprobación es asíncrona: el estado final llega por webhook a /chat/updateTemplateStatus. Requiere scope templates:manage.","operationId":"MetaTemplatesController_submit","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitTemplateDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitTemplateResponseDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Crear/enviar plantilla a aprobación","tags":["Plantillas"]}},"/api/templates/list":{"post":{"description":"Lista las plantillas del canal y su estado. Requiere scope templates:manage.","operationId":"MetaTemplatesController_list","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListTemplatesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Listar plantillas","tags":["Plantillas"]}},"/api/templates/delete":{"delete":{"description":"Borra una plantilla por nombre (todas las traducciones) o por hsm_id (una sola). Requiere scope templates:manage.","operationId":"MetaTemplatesController_deleteTemplate","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteTemplateDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteTemplateResponseDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Borrar plantilla","tags":["Plantillas"]}},"/api/outbound/send":{"post":{"description":"Encola un mensaje saliente hacia el canal indicado (WhatsApp, Instagram, Messenger). En WhatsApp, `to` acepta un teléfono E.164 sin `+` o un BSUID (`US.…` / `US.ENT.…`); Neike detecta el formato y lo rutea al campo correcto de la Cloud API (error 131062 = el mensaje no soporta destinatarios BSUID). Responde 202 y el resultado llega por webhook de estado. Requiere scope messages:send.","operationId":"OutboundController_send","parameters":[{"name":"Idempotency-Key","in":"header","description":"Clave propia de la operación. Si se repite, Neike devuelve la respuesta de la primera vez en lugar de mandar el mensaje otra vez, y responde con `Idempotent-Replay: true`.","required":false,"schema":{"type":"string"}},{"name":"idempotency-key","required":true,"in":"header","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageDto"}}}},"responses":{"202":{"description":"Mensaje encolado.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendMessageResponseDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Enviar mensaje","tags":["Mensajes"]}},"/api/webhooks/config":{"get":{"description":"URL base a la que Neike despacha, las URLs finales por plataforma (WhatsApp/Instagram/Facebook), el interruptor de despacho de cada plataforma y de cada servicio, y las URLs de los eventos de plataforma (marketing, publicaciones y signup embebido) con los nombres de evento que llegan a cada una. El token compartido (x-webhook-token) NO se devuelve: solo un hint enmascarado. Se ve completo una única vez al generarlo. Requiere webhooks:manage.","operationId":"WebhooksController_get","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookConfigResponseDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Config de webhook del cliente","tags":["Webhooks"]},"patch":{"description":"Setea la URL base y/o el token compartido (webhook_secret, write-only: se guarda cifrado y no vuelve en ninguna respuesta). El mismo token va en tu backend para validar el header x-webhook-token de cada dispatch. Requiere webhooks:manage.","operationId":"WebhooksController_update","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWebhookConfigDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookConfigResponseDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Actualizar config de webhook","tags":["Webhooks"]}},"/api/webhooks/dispatch/{platform}":{"patch":{"description":"El segmento admite plataformas de mensajería y también claves de servicio: whatsapp | instagram | facebook | marketing | publishing. Con `enabled: false` Neike sigue recibiendo de Meta (y midiendo) todo lo de esa clave, pero deja de golpear tu backend. En una plataforma eso cubre mensajes, estados, resultados de envío, avisos de plantillas y avisos de canal; en un servicio (marketing, publishing), sus eventos. Lo recibido queda en el historial de eventos con estado `skipped`, así que no se pierde nada y se puede ver qué llegó mientras estaba apagado. Una plataforma se apaga entera, no por canal: `facebook` cubre todos tus canales de Messenger. Los servicios son independientes de las plataformas: apagar `instagram` no apaga los comentarios, y apagar `publishing` no apaga los mensajes. Mientras está apagada, el reintento de esos eventos tampoco re-despacha. Requiere webhooks:manage.","operationId":"WebhooksController_setDispatchPlatform","parameters":[{"name":"platform","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDispatchPlatformDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookConfigResponseDto"}}}},"400":{"description":"Clave de despacho desconocida, o `enabled` ausente o con un valor que no es un booleano."}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Prender o apagar el despacho de una plataforma o de un servicio","tags":["Webhooks"]}},"/api/webhooks/config/generate-secret":{"post":{"description":"Genera un token aleatorio (whsec_...) y lo guarda cifrado como webhook_secret. Esta es la ÚNICA respuesta que trae el token en claro: copialo y ponelo en tu backend.","operationId":"WebhooksController_generateSecret","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GenerateSecretResponseDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Generar token de webhook","tags":["Webhooks"]}},"/api/embed/sessions":{"post":{"description":"Devuelve una URL de un solo uso para embeber en un iframe. El token en claro se muestra una única vez. Al conectar el canal, Neike avisa por webhook (`channel.connected`) y el resultado queda disponible en GET de la sesión.","operationId":"EmbedSessionsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmbedSessionDto"}}}},"responses":{"201":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Crear una sesión de signup embebible","tags":["Signup embebible"]},"get":{"operationId":"EmbedSessionsController_list","parameters":[],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Listar sesiones de signup del cliente","tags":["Signup embebible"]}},"/api/embed/sessions/{sessionId}":{"get":{"description":"Fuente de verdad del resultado: cuando `status` es `completed`, `result` trae el canal con waba_id, phone_number_id y el namespace de plantillas.","operationId":"EmbedSessionsController_get","parameters":[{"name":"sessionId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Estado y resultado de una sesión","tags":["Signup embebible"]}},"/api/embed/sessions/{sessionId}/revoke":{"post":{"operationId":"EmbedSessionsController_revoke","parameters":[{"name":"sessionId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Revocar una sesión antes de que se use","tags":["Signup embebible"]}},"/api/marketing/terms":{"get":{"description":"Devuelve la versión vigente de los términos y si este cliente la aceptó. `accepted` mira **la versión vigente**: si el texto cambió, una aceptación anterior no sirve y `current_version_accepted` dice cuál se había aceptado. Requiere scope marketing:read.","operationId":"MarketingTermsController_status","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingTermsStatusDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Estado de la aceptación de responsabilidad","tags":["Marketing"]}},"/api/marketing/terms/accept":{"post":{"description":"Registra que alguien se hizo responsable del gasto en anuncios de este cliente y habilita las mutaciones de `/api/marketing`. **Es de la consola**: una API key recibe 403 aunque tenga el scope, porque el que se hace responsable es una persona. Solo el dueño o un administrador pueden. `version` tiene que ser la vigente (la que devuelve `GET /terms`); una anterior es 400. Requiere scope marketing:manage.","operationId":"MarketingTermsController_accept","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptMarketingTermsDto"}}}},"responses":{"200":{"description":"El estado ya actualizado, **con el mismo shape que `GET /terms`**: quien acepta puede pintar 'aceptado por Fulana el tal día' sin una segunda vuelta. No es 204 a propósito.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketingTermsStatusDto"}}}},"403":{"description":"Lo intentó una API key, o el rol no alcanza."}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Aceptar la responsabilidad por el gasto publicitario","tags":["Marketing"]}},"/api/marketing/oauth/url":{"get":{"description":"Devuelve el diálogo de Facebook Login al que mandar al usuario y el `state` firmado que lo acompaña. El `state` vence a los 10 minutos y solo sirve para el cliente que lo pidió: hay que reenviarlo tal cual en el canje. Requiere scope marketing:manage.","operationId":"AdAccountsController_oauthUrl","parameters":[{"name":"redirect_uri","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OauthUrlResponseDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"URL para conectar una cuenta publicitaria","tags":["Marketing"]}},"/api/marketing/oauth/exchange":{"post":{"description":"Valida el `state`, canjea el `code` por un token long-lived y da de alta una cuenta por cada cuenta publicitaria a la que el usuario dio acceso, en estado `available`. El cliente después elige cuáles activar. Requiere scope marketing:manage.","operationId":"AdAccountsController_exchange","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExchangeOauthDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdAccountDto"}}}}},"400":{"description":"El usuario no otorgó ads_management (o el state no es válido).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MissingPermissionResponseDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Canjear el code de Meta","tags":["Marketing"]}},"/api/marketing/ad-accounts":{"get":{"description":"Cuentas conectadas por el cliente. Requiere scope marketing:read.","operationId":"AdAccountsController_list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdAccountDto"}}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Listar cuentas publicitarias","tags":["Marketing"]}},"/api/marketing/ad-accounts/{id}":{"get":{"description":"`:id` es el uuid de Neike, no el `act_…` de Meta. Una cuenta de otro cliente responde 404. Requiere scope marketing:read.","operationId":"AdAccountsController_detail","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdAccountDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Detalle de una cuenta publicitaria","tags":["Marketing"]},"patch":{"description":"`active` la habilita para operar desde Neike, `disabled` la pausa sin desconectarla. Requiere scope marketing:manage.","operationId":"AdAccountsController_update","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAdAccountDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdAccountDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Activar o pausar una cuenta publicitaria","tags":["Marketing"]},"delete":{"description":"Borra la cuenta de Neike y con ella el token guardado. No toca nada en Meta: las campañas siguen en Ads Manager. Volver a conectarla exige pasar de nuevo por el OAuth. Requiere scope marketing:manage.","operationId":"AdAccountsController_remove","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Desconectar una cuenta publicitaria","tags":["Marketing"]}},"/api/marketing/ad-accounts/{id}/refresh":{"post":{"description":"Relee nombre, moneda y estado de la cuenta desde la Graph API y actualiza `last_synced_at`. Requiere scope marketing:manage.","operationId":"AdAccountsController_refresh","parameters":[{"name":"id","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdAccountDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Resincronizar con Meta","tags":["Marketing"]}},"/api/marketing/pages":{"get":{"description":"Páginas a las que llega el token de la cuenta publicitaria, con su cuenta de Instagram vinculada si tiene. Son las que pueden ir en `object_story_spec.page_id` de una creatividad. Requiere scope marketing:read.","operationId":"AdAccountsController_pages","parameters":[{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Páginas de Facebook del usuario","tags":["Marketing"]}},"/api/marketing/enums":{"get":{"description":"Valores válidos de objetivo, categoría de anuncio especial, optimización, evento de facturación, estrategia de puja, llamado a la acción y estado, con su etiqueta para mostrar. Es estático: no pega a Graph. Los objetivos vienen con `recommended` (los `OUTCOME_*` son los vigentes; el resto son legacy) y los `date_presets` con `suggested`, porque ese sí es un catálogo abierto y Meta acepta más valores de los que listamos. Requiere scope marketing:read.","operationId":"MarketingEnumsController_enums","parameters":[],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Catálogo de enums de la Marketing API","tags":["Marketing"]}},"/api/marketing/campaigns":{"get":{"description":"Campañas de la cuenta publicitaria indicada en `ad_account_id`. El filtro `status` aplica sobre el `effective_status` que calcula Meta (el que incluye revisión y estados heredados). Los montos vienen en centavos de la moneda de la cuenta. Requiere scope marketing:read.","operationId":"CampaignsController_list","parameters":[{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}},{"name":"after","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CampaignDto"}}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Listar campañas","tags":["Marketing"]},"post":{"description":"Crea la campaña en Meta y devuelve el objeto releído. Sin `status` nace `PAUSED`. `special_ad_categories` viaja siempre (vacío si no se declara ninguna) porque Meta lo exige. `daily_budget` y `lifetime_budget` son excluyentes: mandar los dos es 400. Los montos van en centavos de la moneda de la cuenta, sin convertir. La combinación válida de objetivo, optimización y facturación la valida Meta del lado del servidor: Neike valida solo que cada valor pertenezca a su enum y devuelve el error de Meta mapeado. Requiere scope marketing:manage.","operationId":"CampaignsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCampaignDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Crear una campaña","tags":["Marketing"]}},"/api/marketing/campaigns/{campaignId}":{"get":{"description":"`ad_account_id` es obligatorio: es lo que resuelve el token y lo que garantiza que la cuenta sea del cliente autenticado. Requiere scope marketing:read.","operationId":"CampaignsController_detail","parameters":[{"name":"campaignId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Detalle de una campaña","tags":["Marketing"]},"patch":{"description":"Solo se mandan a Meta los campos presentes; un `null` no borra el valor anterior. `daily_budget` y `lifetime_budget` siguen siendo excluyentes. Requiere scope marketing:manage.","operationId":"CampaignsController_update","parameters":[{"name":"campaignId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCampaignDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CampaignDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Editar una campaña","tags":["Marketing"]},"delete":{"description":"Borrado real en Meta. Si la campaña tiene conjuntos o anuncios activos, Meta lo rechaza y ese error se devuelve tal cual: no se disimula pausando. Requiere scope marketing:manage.","operationId":"CampaignsController_remove","parameters":[{"name":"campaignId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Borrar una campaña","tags":["Marketing"]}},"/api/marketing/adsets":{"get":{"description":"Conjuntos de la cuenta, o de una campaña si se pasa `campaign_id`. El filtro `status` aplica sobre el `effective_status` de Meta. Requiere scope marketing:read.","operationId":"AdSetsController_list","parameters":[{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"campaign_id","required":false,"in":"query","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}},{"name":"after","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdSetDto"}}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Listar conjuntos de anuncios","tags":["Marketing"]},"post":{"description":"Crea el conjunto en Meta y devuelve el objeto releído. Sin `status` nace `PAUSED`. `daily_budget` y `lifetime_budget` son excluyentes (400 si vienen los dos) y un `lifetime_budget` exige `end_time` (400 si falta). Los montos van en centavos de la moneda de la cuenta. `targeting` viaja como objeto y Neike lo serializa a JSON. La combinación válida de `billing_event`, `optimization_goal` y el objetivo de la campaña la valida Meta del lado del servidor y no está publicada completa: Neike valida solo que cada valor pertenezca a su enum y devuelve el error de Meta mapeado. Requiere scope marketing:manage.","operationId":"AdSetsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAdSetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdSetDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Crear un conjunto de anuncios","tags":["Marketing"]}},"/api/marketing/adsets/{adSetId}":{"get":{"description":"`ad_account_id` es obligatorio: resuelve el token y garantiza que la cuenta sea del cliente autenticado. Requiere scope marketing:read.","operationId":"AdSetsController_detail","parameters":[{"name":"adSetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdSetDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Detalle de un conjunto de anuncios","tags":["Marketing"]},"patch":{"description":"Solo se mandan a Meta los campos presentes. Siguen valiendo las dos reglas de presupuesto: excluyentes entre sí, y `lifetime_budget` acompañado de `end_time`. Requiere scope marketing:manage.","operationId":"AdSetsController_update","parameters":[{"name":"adSetId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAdSetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdSetDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Editar un conjunto de anuncios","tags":["Marketing"]},"delete":{"description":"Borrado real en Meta. Si el conjunto tiene anuncios activos, Meta lo rechaza y el error se devuelve tal cual. Requiere scope marketing:manage.","operationId":"AdSetsController_remove","parameters":[{"name":"adSetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Borrar un conjunto de anuncios","tags":["Marketing"]}},"/api/marketing/ads":{"get":{"description":"Anuncios de la cuenta; con `adset_id` o `campaign_id` se acotan a ese conjunto o campaña (el filtro más fino gana). El filtro `status` aplica sobre el `effective_status` de Meta, que es el que incluye el estado de revisión. Requiere scope marketing:read.","operationId":"AdsController_list","parameters":[{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"campaign_id","required":false,"in":"query","schema":{"type":"string"}},{"name":"adset_id","required":false,"in":"query","schema":{"type":"string"}},{"name":"status","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}},{"name":"after","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AdDto"}}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Listar anuncios","tags":["Marketing"]},"post":{"description":"Une un conjunto con una creatividad ya existente (`creative.creative_id`) y devuelve el anuncio releído. Sin `status` nace `PAUSED`. Requiere scope marketing:manage.","operationId":"AdsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAdDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Crear un anuncio","tags":["Marketing"]}},"/api/marketing/ads/{adId}":{"get":{"description":"`ad_account_id` es obligatorio: resuelve el token y garantiza que la cuenta sea del cliente autenticado. Requiere scope marketing:read.","operationId":"AdsController_detail","parameters":[{"name":"adId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Detalle de un anuncio","tags":["Marketing"]},"patch":{"description":"Se pueden cambiar nombre, estado, creatividad y `tracking_specs`. **`adset_id` es inmutable**: si viene, la respuesta es 400 — para mover un anuncio de conjunto hay que crear uno nuevo. Requiere scope marketing:manage.","operationId":"AdsController_update","parameters":[{"name":"adId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAdDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Editar un anuncio","tags":["Marketing"]},"delete":{"description":"Borrado real en Meta. Si Meta lo rechaza, el error se devuelve tal cual. Requiere scope marketing:manage.","operationId":"AdsController_remove","parameters":[{"name":"adId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Borrar un anuncio","tags":["Marketing"]}},"/api/marketing/creatives":{"get":{"description":"Biblioteca de creatividades de la cuenta publicitaria. Requiere scope marketing:read.","operationId":"CreativesController_list","parameters":[{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}},{"name":"after","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CreativeDto"}}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Listar creatividades","tags":["Marketing"]},"post":{"description":"El `object_story_spec` lleva la página que publica y **exactamente uno** de `link_data` o `video_data`: mandar los dos, o ninguno, es 400. La imagen se referencia por el `hash` de `POST /api/marketing/images` y el video por el `id` de `POST /api/marketing/videos`. Requiere scope marketing:manage.","operationId":"CreativesController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCreativeDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreativeDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Crear una creatividad","tags":["Marketing"]}},"/api/marketing/creatives/images":{"get":{"description":"Devuelve las URLs reales de las imágenes de la biblioteca de la cuenta a partir de sus `image_hash` (de 1 a 20, separados por coma). Una creatividad armada con `image_hash` no trae `image_url`: este endpoint es la forma de ver el archivo. Un hash que no está en la biblioteca de la cuenta no vuelve en la respuesta. Requiere scope marketing:read.","operationId":"CreativesController_listImages","parameters":[{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"hashes","required":true,"in":"query","description":"Hashes de imagen separados por coma (de 1 a 20, hexadecimal de 32).","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdImagesResponseDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Resolver imágenes por hash","tags":["Marketing"]}},"/api/marketing/creatives/{creativeId}":{"get":{"description":"`ad_account_id` es obligatorio: resuelve el token y garantiza que la cuenta sea del cliente autenticado. Requiere scope marketing:read.","operationId":"CreativesController_detail","parameters":[{"name":"creativeId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreativeDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Detalle de una creatividad","tags":["Marketing"]},"delete":{"description":"Borrado real en Meta. Si algún anuncio la sigue usando, Meta lo rechaza y el error se devuelve tal cual. Requiere scope marketing:manage.","operationId":"CreativesController_remove","parameters":[{"name":"creativeId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Borrar una creatividad","tags":["Marketing"]}},"/api/marketing/images":{"post":{"description":"Sube el archivo a la biblioteca de la cuenta y devuelve el `hash` que va en `link_data.image_hash`. El contenido viaja en base64 y el archivo no puede superar los 7 MB: codificado pesa un tercio más y el body parser de Neike corta en 10 MB. Requiere scope marketing:manage.","operationId":"CreativesController_uploadImage","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadImageDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadedImageDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Subir una imagen","tags":["Marketing"]}},"/api/marketing/videos":{"post":{"description":"Devuelve el `id` que va en `video_data.video_id`. Acepta dos formas: el archivo en `multipart/form-data` (campo `file`, hasta 100 MB; más pesado es 413) o `file_url`, una URL pública que descarga Meta. Mandar las dos es 400. Neike elige solo cómo subirlo a Meta: un archivo chico va en un request y uno grande por partes. Meta codifica el video después de aceptarlo: hasta que `GET /api/marketing/videos/{videoId}` diga `ready`, el video no se puede usar en una creatividad. Requiere scope marketing:manage.","operationId":"CreativesController_uploadVideo","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/UploadVideoDto"}},"application/json":{"schema":{"$ref":"#/components/schemas/UploadVideoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UploadedVideoDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Subir un video","tags":["Marketing"]}},"/api/marketing/videos/{videoId}":{"get":{"description":"Estado de la codificación del video en Meta (`status`: `processing`, `ready`, `error` o `expired`) y las miniaturas que Meta generó, que sirven como `video_data.image_url`. Las miniaturas aparecen recién cuando el video está listo. `ad_account_id` es obligatorio: resuelve el token y acota la búsqueda a los videos de esa cuenta, así que un video de otra cuenta responde 404. Se buscan los 25 videos más recientes de la cuenta, que es de sobra para seguir una subida (el recién subido es el primero), pero un `video_id` viejo con más videos subidos después también responde 404. Requiere scope marketing:read.","operationId":"CreativesController_readVideo","parameters":[{"name":"videoId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Estado de un video","tags":["Marketing"]}},"/api/marketing/insights":{"get":{"description":"Consulta sincrónica. Sin `object_id` mide la cuenta entera; con él, una campaña, un conjunto o un anuncio. `fields` es un CSV de métricas (por defecto impresiones, clics, gasto, alcance, frecuencia, CPC, CPM, CTR y acciones). `date_preset` se acepta como **string libre**, no como enum cerrado: la referencia de parámetros de Meta no está publicada completa, así que Neike lo pasa tal cual y Meta valida — los valores de uso corriente están en `GET /api/marketing/enums` como sugerencias. `time_increment` funciona igual (string libre): un número de días (`1`, `7`, `28`), `monthly` o `all_days`. `1` es el valor típico para una serie diaria; si no se manda, Graph agrega todo el rango en una sola fila. Requiere scope marketing:read.","operationId":"InsightsController_query","parameters":[{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"object_id","required":false,"in":"query","schema":{"type":"string"}},{"name":"level","required":false,"in":"query","schema":{"type":"string"}},{"name":"date_preset","required":false,"in":"query","schema":{"type":"string"}},{"name":"time_range","required":false,"in":"query","schema":{"type":"string"}},{"name":"time_increment","required":false,"in":"query","schema":{"type":"string"}},{"name":"fields","required":false,"in":"query","schema":{"type":"string"}},{"name":"breakdowns","required":false,"in":"query","schema":{"type":"string"}},{"name":"action_breakdowns","required":false,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}},{"name":"after","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Métricas de entrega","tags":["Marketing"]}},"/api/marketing/insights/reports":{"post":{"description":"Mismos parámetros que la consulta sincrónica (incluidos `date_preset` y `time_increment`, los dos string libre), pero para rangos o desgloses que Graph no puede responder en una sola llamada. Devuelve `{ report_run_id }`; el avance se consulta en `GET /api/marketing/insights/reports/:reportRunId` y las filas en `…/results`. **Los reportes de Meta expiran a los 30 días.** Requiere scope marketing:manage.","operationId":"InsightsController_createReport","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightsQueryDto"}}}},"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Pedir un reporte asincrónico","tags":["Marketing"]}},"/api/marketing/insights/reports/{reportRunId}":{"get":{"description":"`async_status` va de `Job Not Started` a `Job Completed` (o `Job Failed`) y `async_percent_completion` de 0 a 100. `error_code` y `error_message` solo vienen si Meta falló el reporte. Un reporte de más de 30 días ya no existe del lado de Meta y responde el error correspondiente. Requiere scope marketing:read.","operationId":"InsightsController_reportStatus","parameters":[{"name":"reportRunId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InsightsReportRunDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Avance de un reporte asincrónico","tags":["Marketing"]}},"/api/marketing/insights/reports/{reportRunId}/results":{"get":{"description":"Filas del reporte, paginadas por cursor. Pedirlas antes de que `async_status` sea `Job Completed` devuelve lo que haya (o vacío). Requiere scope marketing:read.","operationId":"InsightsController_reportResults","parameters":[{"name":"reportRunId","required":true,"in":"path","schema":{"type":"string"}},{"name":"ad_account_id","required":true,"in":"query","schema":{"type":"string"}},{"name":"limit","required":false,"in":"query","schema":{"type":"string"}},{"name":"after","required":false,"in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Resultados de un reporte asincrónico","tags":["Marketing"]}},"/api/embed/ads-sessions":{"post":{"description":"Devuelve una URL de un solo uso para embeber en un iframe: el usuario final hace el login de Meta ahí adentro y elige qué cuentas publicitarias activar. El token en claro se muestra una única vez. Al activar, Neike avisa por webhook (`ads.connected`) y el resultado queda en el GET de la sesión. Exige que alguien haya aceptado los términos de Marketing: sin eso responde 403 al crear, no adentro del iframe. Requiere scope marketing:manage.","operationId":"EmbedAdsSessionsController_create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmbedAdsSessionDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmbedAdsSessionCreatedDto"}}}},"400":{"description":"Orígenes inválidos, ttl fuera de rango o `provider` no soportado."},"403":{"description":"Nadie aceptó la versión vigente de los términos de Marketing (`error: marketing_terms_not_accepted`)."}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Crear una sesión embebible de conexión de anuncios","tags":["Marketing"]},"get":{"description":"Las más recientes primero (hasta 200). Nunca mezcla con las sesiones de signup de canales: cada flujo tiene su superficie. Requiere scope marketing:read.","operationId":"EmbedAdsSessionsController_list","parameters":[],"responses":{"200":{"description":""}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Listar sesiones de conexión de anuncios del cliente","tags":["Marketing"]}},"/api/embed/ads-sessions/{sessionId}":{"get":{"description":"Fuente de verdad del resultado: cuando `status` es `completed`, `result` trae las cuentas activadas (`{ provider, ad_accounts: [{ ad_account_id, name, status }] }`). Requiere scope marketing:read.","operationId":"EmbedAdsSessionsController_get","parameters":[{"name":"sessionId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmbedAdsSessionViewDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Estado y resultado de una sesión de conexión de anuncios","tags":["Marketing"]}},"/api/embed/ads-sessions/{sessionId}/revoke":{"post":{"description":"Mata el token en el acto: el iframe deja de resolver. Es la salida si la `embed_url` se filtró antes de vencer. Una sesión completada no se puede revocar. Requiere scope marketing:manage.","operationId":"EmbedAdsSessionsController_revoke","parameters":[{"name":"sessionId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmbedAdsSessionViewDto"}}}}},"security":[{"userJwt":[]},{"apiKey":[]}],"summary":"Revocar una sesión antes de que se use","tags":["Marketing"]}}},"info":{"title":"Neike API","description":"API de integración de Neike — conectá **tu backend** con nosotros para enviar y recibir\nmensajes de WhatsApp, Instagram y Facebook vía una sola API. Somos tu BSP: vos hablás con\nnosotros, nosotros con Meta. Tu cuenta y tus canales se gestionan desde la consola **Neike**;\nesta referencia es solo lo que tu equipo técnico necesita para la integración.\n\n## Autenticación\nUsá tu **API key** en la cabecera `x-neike-api-key: whk_live_…`. Se emite y rota desde Neike\n(no por esta API). Puede limitarse por scope (`messages:send`, `templates:manage`, …).\n\n## Cómo ENVIÁS mensajes\n`POST /api/outbound/send` con tu API key. Indicás el canal (`channelId`), el destinatario\n(`to`) y el contenido. En WhatsApp, `to` acepta teléfono E.164 sin `+` o un BSUID (`US.…`).\nVer el endpoint **Mensajes** para el payload exacto.\n\n## Cómo RECIBÍS mensajes y estados (webhooks)\nConfigurás **una URL base** (tu backend) y un **token compartido** en Neike (endpoint\n**Webhooks**). A esa base te hacemos `POST` con la terminación de cada plataforma:\n- WhatsApp entrante → `{tu_url}/chat/whatsapp/receive`\n- Instagram entrante → `{tu_url}/chat/instagram/receive`\n- Facebook entrante → `{tu_url}/chat/facebook/receive`\n- Actualizaciones de estado (enviado/entregado/leído/falló) → `{tu_url}/chat/whatsapp/update`\n\n**Validación:** en cada webhook mandamos la cabecera `x-webhook-token` con el token que\nconfiguraste. Tu backend debe compararlo antes de procesar (igual que vos validás a tu\nproveedor). El cuerpo entrante es `{ conversation, message }`.\n\n## Plantillas\nAlta y gestión de plantillas de WhatsApp desde el endpoint **Plantillas**.","version":"1.0","contact":{}},"tags":[],"servers":[],"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-neike-api-key","description":"API key de cliente (formato whk_live_…), emitida desde Neike."},"userJwt":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"JWT de usuario de la consola Neike. Agregar x-neike-client-id si el usuario pertenece a varios clientes."}},"schemas":{"SubmitTemplateDto":{"type":"object","properties":{"channel_id":{"type":"string","description":"channel_id del canal (prefijo `nk_`).","example":"nk_dvzobn7m4h4y"},"template_data":{"type":"object","additionalProperties":true,"description":"Definición de la plantilla en el formato de Meta: `{ name, language, category, components }`.","example":{"name":"bienvenida","language":"es","category":"MARKETING","components":[{"type":"BODY","text":"Hola {{1}}, bienvenido.","example":{"body_text":[["Juan"]]}}]}}},"required":["channel_id","template_data"]},"SubmitTemplateResponseDto":{"type":"object","properties":{"externalCode":{"type":"number","description":"Código de Meta (200 = aceptada a revisión).","example":200},"status":{"type":"string","description":"Estado inicial de la plantilla.","enum":["PENDING","APPROVED","REJECTED"],"example":"PENDING"},"id_external":{"type":"string","description":"Id de la plantilla en Meta.","example":"123456789"},"name":{"type":"string","description":"Nombre de la plantilla.","example":"bienvenida"},"message":{"type":"string","description":"Mensaje de error de Meta, si `externalCode` != 200."}},"required":["externalCode"]},"ListTemplatesDto":{"type":"object","properties":{"channel_id":{"type":"string","description":"channel_id del canal (prefijo `nk_`).","example":"nk_dvzobn7m4h4y"}},"required":["channel_id"]},"DeleteTemplateDto":{"type":"object","properties":{"channel_id":{"type":"string","description":"channel_id del canal (prefijo `nk_`).","example":"nk_dvzobn7m4h4y"},"template_name":{"type":"string","description":"Nombre de la plantilla a borrar.","example":"bienvenida"},"hsm_id":{"type":"string","description":"Id de una traducción específica (hsm_id). Si se omite, borra todas.","example":"123456789"}},"required":["channel_id","template_name"]},"DeleteTemplateResponseDto":{"type":"object","properties":{"success":{"type":"boolean","example":true},"status_code":{"type":"number","example":200}},"required":["success","status_code"]},"SendMessageDto":{"type":"object","properties":{"channelId":{"type":"string","description":"channel_id del canal conectado (prefijo `nk_`).","example":"nk_dvzobn7m4h4y"},"to":{"type":"string","description":"Destinatario. En WhatsApp, número en formato E.164 sin `+`, o un BSUID (`US.13491208655302741918` / parent `US.ENT.…`): Neike detecta el formato y lo rutea al campo correcto de la Cloud API. En Messenger es el PSID del contacto y en Instagram su IGSID, los dos tal como llegan en el webhook de entrada.","example":"5491112345678"},"type":{"type":"string","description":"Tipo de mensaje. `text` y los de media (`image`, `video`, `audio`, `document`) valen en las tres plataformas. El resto no: `template` e `interactive` son exclusivos de WhatsApp, `reaction` está en WhatsApp e Instagram pero no en Messenger, y `file` está en Instagram y en Messenger pero no en WhatsApp. Mandar un tipo que la plataforma del canal no admite devuelve 400 con la lista de los que sí. Se acepta en cualquier combinación de mayúsculas y con espacios al borde: Neike lo normaliza a minúscula antes de enviarlo.","enum":["text","image","video","audio","document","file","template","interactive","reaction"],"example":"text"},"content":{"type":"object","additionalProperties":true,"description":"Contenido según `type`. text → `{ text }`; media → `{ [type]: { link|id, caption?, filename? } }` (en Instagram y Messenger, `{ [type]: { url } }`); template → `{ template: {...} }`; interactive → `{ interactive: {...} }`; reaction → `{ reaction: { message_id, emoji } }`.","example":{"text":"Hola, este es un mensaje de prueba"}},"humanAgent":{"type":"boolean","description":"Instagram y Messenger: agrega el tag HUMAN_AGENT, que extiende la ventana de respuesta de 24 horas a 7 días. Solo para envíos escritos por una persona, nunca automatizados. WhatsApp lo ignora.","example":false}},"required":["channelId","to","type","content"]},"SendMessageResponseDto":{"type":"object","properties":{"ok":{"type":"boolean","description":"true si el mensaje se encoló correctamente.","example":true},"jobId":{"type":"string","description":"Id del job en la cola de salida.","example":"outbound_nk_dvzobn7m4h4y_1712345678901"},"reason":{"type":"string","description":"Motivo, cuando `ok=false`."}},"required":["ok"]},"PlatformDispatchDto":{"type":"object","properties":{"receive":{"type":"string","example":"https://tu-backend.com/chat/whatsapp/receive"},"status":{"type":"string","example":"https://tu-backend.com/chat/whatsapp/update"}},"required":["receive","status"]},"DispatchUrlsDto":{"type":"object","properties":{"whatsapp":{"$ref":"#/components/schemas/PlatformDispatchDto"},"instagram":{"$ref":"#/components/schemas/PlatformDispatchDto"},"facebook":{"$ref":"#/components/schemas/PlatformDispatchDto"}},"required":["whatsapp","instagram","facebook"]},"DispatchPlatformsDto":{"type":"object","properties":{"whatsapp":{"type":"boolean","example":true},"instagram":{"type":"boolean","example":true},"facebook":{"type":"boolean","example":false},"marketing":{"type":"boolean","description":"Eventos de anuncios de Meta.","example":true},"publishing":{"type":"boolean","description":"Comentarios sobre tus publicaciones.","example":true}},"required":["whatsapp","instagram","facebook","marketing","publishing"]},"EventTargetDto":{"type":"object","properties":{"url":{"type":"string","example":"https://tu-backend.com/ads/events"},"events":{"description":"Nombres de evento (`event` del payload) que Neike despacha a esta URL.","example":["ad_account.connected","campaign.status_changed"],"type":"array","items":{"type":"string"}}},"required":["url","events"]},"EventUrlsDto":{"type":"object","properties":{"marketing":{"description":"Eventos de anuncios de Meta (`webhook_url` + `MARKETING_WEBHOOK_PATH`).","allOf":[{"$ref":"#/components/schemas/EventTargetDto"}]},"publishing":{"description":"Comentarios sobre tus publicaciones (`webhook_url` + `PUBLISHING_WEBHOOK_PATH`).","allOf":[{"$ref":"#/components/schemas/EventTargetDto"}]},"embed":{"description":"Aviso `channel.connected` del signup embebido (`webhook_url` + `CHANNEL_CONNECTED_PATH`).","allOf":[{"$ref":"#/components/schemas/EventTargetDto"}]}},"required":["marketing","publishing","embed"]},"WebhookConfigResponseDto":{"type":"object","properties":{"webhook_url":{"type":"string","description":"URL base configurada.","example":"https://tu-backend.com"},"webhook_secret_set":{"type":"boolean","description":"true si hay token compartido (x-webhook-token) configurado.","example":true},"webhook_secret_hint":{"type":"string","nullable":true,"description":"Hint enmascarado del token para reconocerlo (`whsec_…abcd`). El valor completo solo se devuelve al generarlo, una única vez.","example":"whsec_…abcd"},"configured":{"type":"boolean","description":"true si hay `webhook_url` configurada.","example":true},"signature_header":{"type":"string","description":"Cabecera con la que Neike firma cada dispatch.","example":"x-webhook-token"},"dispatch_urls":{"$ref":"#/components/schemas/DispatchUrlsDto"},"dispatch_platforms":{"description":"Interruptor de despacho, por plataforma de mensajería y por servicio. Por defecto las cinco están en true. Se cambia con `PATCH /api/webhooks/dispatch/{platform}`.","allOf":[{"$ref":"#/components/schemas/DispatchPlatformsDto"}]},"event_urls":{"$ref":"#/components/schemas/EventUrlsDto"}},"required":["webhook_url","webhook_secret_set","webhook_secret_hint","configured","signature_header","dispatch_urls","dispatch_platforms","event_urls"]},"UpdateWebhookConfigDto":{"type":"object","properties":{"webhook_url":{"type":"string","description":"URL base de tu backend a la que Neike despacha los webhooks.","example":"https://tu-backend.com"},"webhook_secret":{"type":"string","nullable":true,"description":"Token compartido (write-only: se guarda cifrado y no vuelve en ninguna respuesta). Neike lo manda en la cabecera `x-webhook-token` de cada dispatch; tu backend lo valida. `null` para limpiarlo.","example":"whsec_abc123…"}}},"UpdateDispatchPlatformDto":{"type":"object","properties":{"enabled":{"type":"boolean","description":"true prende el despacho de esa clave, false lo apaga. Booleano estricto: cualquier otro valor, y también el campo ausente, es 400. Lo que llega mientras está apagado queda en el historial de eventos con estado `skipped`, no se pierde.","example":false}},"required":["enabled"]},"GenerateSecretResponseDto":{"type":"object","properties":{"webhook_secret":{"type":"string","description":"Token generado. Se muestra UNA única vez: copialo y ponelo en tu backend. Después solo queda el hint enmascarado.","example":"whsec_abc123…"}},"required":["webhook_secret"]},"CreateEmbedSessionDto":{"type":"object","properties":{"allowed_origins":{"description":"Orígenes autorizados a embeber el iframe. Alimentan el `frame-ancestors` del CSP y el targetOrigin del postMessage de resultado.","example":["https://app.tu-dominio.com"],"type":"array","items":{"type":"string"}},"platforms":{"type":"array","description":"Canales ofrecidos dentro del iframe.","default":["whatsapp"],"items":{"type":"string","enum":["whatsapp","instagram","facebook"]}},"external_ref":{"type":"string","description":"Id del negocio en el sistema del cliente. Vuelve tal cual en el webhook y en el resultado, para correlacionar el canal con su tenant.","example":"tenant_8842"},"ttl_minutes":{"type":"number","description":"Minutos de validez de la sesión.","default":30,"minimum":1,"maximum":1440},"prefill":{"type":"object","additionalProperties":true,"description":"Datos del negocio que ya conoce el cliente; precargan el formulario de Meta (business.name, business.email, business.website, phone.displayName, phone.category).","example":{"business":{"name":"Café Central","email":"hola@cafecentral.com"}}}},"required":["allowed_origins"]},"MarketingTermsAcceptedByDto":{"type":"object","properties":{"user_id":{"type":"number","example":42},"name":{"type":"string","example":"Dennis Hesler"},"email":{"type":"string","example":"dennis@ejemplo.com"}},"required":["user_id","name","email"]},"MarketingTermsStatusDto":{"type":"object","properties":{"version":{"type":"string","description":"Versión vigente del texto. Es la que hay que mandar al aceptar.","example":"2026-08-30"},"accepted":{"type":"boolean","description":"Si el cliente aceptó **la versión vigente**. Es lo único que mira la puerta: una aceptación de una versión anterior no habilita nada."},"accepted_at":{"type":"string","nullable":true,"description":"Cuándo fue la última aceptación, **sea de la versión que sea**. `null` solo si este cliente no aceptó nunca: si aceptó una versión vieja, viene con esa fecha aunque `accepted` sea false."},"accepted_by":{"nullable":true,"description":"Quién hizo esa última aceptación, con la misma regla que `accepted_at`: si fue de una versión vieja, viene igual. `null` solo si nunca hubo una.","type":"object","allOf":[{"$ref":"#/components/schemas/MarketingTermsAcceptedByDto"}]},"current_version_accepted":{"type":"boolean","description":"Si lo aceptado es la versión vigente. Es el mismo valor que `accepted`, con el nombre dicho entero: el par `accepted_at` + `current_version_accepted: false` es lo que deja decir 'la versión anterior la aceptó Fulana el tal día' en vez de un 'no aceptado' pelado."}},"required":["version","accepted","current_version_accepted"]},"AcceptMarketingTermsDto":{"type":"object","properties":{"version":{"type":"string","minLength":1,"maxLength":32,"description":"La versión que se está aceptando. Tiene que ser la vigente: si el texto cambió mientras la pantalla estaba abierta, mandar la vieja es 400 y hay que releerlo.","example":"2026-08-30"}},"required":["version"]},"OauthUrlResponseDto":{"type":"object","properties":{"url":{"type":"string","description":"URL del diálogo de Facebook Login a la que hay que mandar al usuario."},"state":{"type":"string","description":"El `state` firmado que va en esa URL. Viaja también acá porque la consola lo guarda para reenviarlo en el canje y detectar un callback que no arrancó ella."}},"required":["url","state"]},"ExchangeOauthDto":{"type":"object","properties":{"code":{"type":"string","description":"`code` que Meta devolvió en el redirect."},"redirect_uri":{"type":"string","description":"El MISMO redirect_uri con el que se pidió la URL. Meta lo compara y rechaza el canje si no coincide exactamente."},"state":{"type":"string","description":"El `state` firmado que emitió `GET /oauth/url`."}},"required":["code","redirect_uri","state"]},"AdAccountDto":{"type":"object","properties":{"id":{"type":"string","description":"Id de la cuenta en Neike (uuid)."},"provider":{"type":"string","description":"Proveedor de anuncios de la cuenta. Hoy solo hay uno: `meta`.","enum":["meta"]},"ad_account_id":{"type":"string","description":"Id de la cuenta en Meta, con prefijo.","example":"act_123456789"},"account_id":{"type":"string","description":"El mismo id sin el prefijo `act_`.","example":"123456789"},"name":{"type":"string","example":"Cuenta principal"},"currency":{"type":"string","description":"Moneda de la cuenta en Meta.","example":"ARS"},"timezone_name":{"type":"string","example":"America/Argentina/Buenos_Aires"},"account_status":{"type":"number","description":"Enum numérico de Meta (1 = activa, 2 = deshabilitada…).","example":1},"account_status_label":{"type":"string","description":"El mismo estado en texto, ya traducido.","example":"active"},"business_id":{"type":"string","nullable":true},"business_name":{"type":"string","nullable":true},"status":{"enum":["available","active","disabled","revoked"],"type":"string","description":"Estado en Neike: `available` (conectada, sin elegir), `active` (operable), `disabled` (pausada por el cliente) o `revoked` (Meta invalidó el token)."},"last_synced_at":{"type":"string","nullable":true,"format":"date-time"},"created_at":{"type":"string","format":"date-time"}},"required":["id","provider","ad_account_id","account_id","name","currency","timezone_name","account_status","account_status_label","status","last_synced_at","created_at"]},"MissingPermissionResponseDto":{"type":"object","properties":{"error":{"type":"string","enum":["missing_permission"]},"message":{"type":"string"},"missing":{"description":"Permisos que el usuario no otorgó en el diálogo de Meta.","example":["ads_management"],"type":"array","items":{"type":"string"}}},"required":["error","message"]},"UpdateAdAccountDto":{"type":"object","properties":{"status":{"enum":["active","disabled"],"type":"string","description":"`active` la habilita para operar desde Neike; `disabled` la pausa sin desconectarla. Una cuenta con el token revocado hay que volver a conectarla por OAuth."}},"required":["status"]},"CampaignDto":{"type":"object","properties":{"id":{"type":"string","example":"120210000000000000"},"name":{"type":"string","example":"Tráfico · septiembre"},"objective":{"enum":["APP_INSTALLS","BRAND_AWARENESS","CONVERSIONS","EVENT_RESPONSES","LEAD_GENERATION","LINK_CLICKS","LOCAL_AWARENESS","MESSAGES","OFFER_CLAIMS","OUTCOME_APP_PROMOTION","OUTCOME_AWARENESS","OUTCOME_ENGAGEMENT","OUTCOME_LEADS","OUTCOME_SALES","OUTCOME_TRAFFIC","PAGE_LIKES","POST_ENGAGEMENT","PRODUCT_CATALOG_SALES","REACH","STORE_VISITS","VIDEO_VIEWS"],"type":"string"},"status":{"enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"type":"string"},"effective_status":{"type":"string","description":"Estado efectivo calculado por Meta (incluye los de revisión y los heredados de la cuenta). Es de solo lectura y tiene más valores que `status`.","example":"PAUSED"},"special_ad_categories":{"type":"array","items":{"type":"string","enum":["NONE","EMPLOYMENT","HOUSING","CREDIT","ISSUES_ELECTIONS_POLITICS","ONLINE_GAMBLING_AND_GAMING","FINANCIAL_PRODUCTS_SERVICES"]}},"buying_type":{"enum":["AUCTION","RESERVED"],"type":"string"},"daily_budget":{"type":"string","description":"Presupuesto diario en centavos.","example":"100000"},"lifetime_budget":{"type":"string","description":"Presupuesto total en centavos.","example":"500000"},"spend_cap":{"type":"string","description":"Tope de gasto en centavos.","example":"1000000"},"bid_strategy":{"enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"],"type":"string"},"start_time":{"type":"string","example":"2026-09-01T00:00:00-0300"},"stop_time":{"type":"string","example":"2026-09-30T23:59:59-0300"},"created_time":{"type":"string"},"updated_time":{"type":"string"}},"required":["id","name"]},"CreateCampaignDto":{"type":"object","properties":{"ad_account_id":{"type":"string","description":"Cuenta publicitaria de Meta, con prefijo.","example":"act_123"},"name":{"type":"string","example":"Tráfico · septiembre"},"objective":{"enum":["APP_INSTALLS","BRAND_AWARENESS","CONVERSIONS","EVENT_RESPONSES","LEAD_GENERATION","LINK_CLICKS","LOCAL_AWARENESS","MESSAGES","OFFER_CLAIMS","OUTCOME_APP_PROMOTION","OUTCOME_AWARENESS","OUTCOME_ENGAGEMENT","OUTCOME_LEADS","OUTCOME_SALES","OUTCOME_TRAFFIC","PAGE_LIKES","POST_ENGAGEMENT","PRODUCT_CATALOG_SALES","REACH","STORE_VISITS","VIDEO_VIEWS"],"type":"string"},"special_ad_categories":{"type":"array","items":{"type":"string","enum":["NONE","EMPLOYMENT","HOUSING","CREDIT","ISSUES_ELECTIONS_POLITICS","ONLINE_GAMBLING_AND_GAMING","FINANCIAL_PRODUCTS_SERVICES"]},"description":"Categorías de anuncio especial. Meta lo exige siempre: si no viene se manda `[]`.","default":[]},"status":{"enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"type":"string","description":"Si no viene, la campaña se crea `PAUSED` (nadie empieza a gastar sin pedirlo).","default":"PAUSED"},"buying_type":{"enum":["AUCTION","RESERVED"],"type":"string"},"daily_budget":{"type":"number","nullable":true,"description":"Presupuesto diario en centavos. Excluyente con `lifetime_budget`.","example":100000},"lifetime_budget":{"type":"number","nullable":true,"description":"Presupuesto total en centavos. Excluyente con `daily_budget`.","example":null},"bid_strategy":{"nullable":true,"enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"],"type":"string"},"spend_cap":{"type":"number","nullable":true,"description":"Tope de gasto de la campaña, en centavos."},"start_time":{"type":"string","nullable":true,"example":"2026-09-01T00:00:00-0300"},"stop_time":{"type":"string","nullable":true,"example":"2026-09-30T23:59:59-0300"}},"required":["ad_account_id","name","objective"]},"UpdateCampaignDto":{"type":"object","properties":{"ad_account_id":{"type":"string","description":"Cuenta publicitaria dueña de la campaña.","example":"act_123"},"name":{"type":"string"},"status":{"enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"type":"string"},"special_ad_categories":{"type":"array","items":{"type":"string","enum":["NONE","EMPLOYMENT","HOUSING","CREDIT","ISSUES_ELECTIONS_POLITICS","ONLINE_GAMBLING_AND_GAMING","FINANCIAL_PRODUCTS_SERVICES"]}},"daily_budget":{"type":"number","nullable":true,"description":"Presupuesto diario en centavos."},"lifetime_budget":{"type":"number","nullable":true,"description":"Presupuesto total en centavos."},"bid_strategy":{"nullable":true,"enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"],"type":"string"},"spend_cap":{"type":"number","nullable":true,"description":"Tope de gasto de la campaña, en centavos."},"start_time":{"type":"string","nullable":true},"stop_time":{"type":"string","nullable":true}},"required":["ad_account_id"]},"AdSetDto":{"type":"object","properties":{"id":{"type":"string","example":"120210000000000000"},"name":{"type":"string","example":"AR 25-45 · feed"},"campaign_id":{"type":"string","example":"120210000000000000"},"status":{"enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"type":"string"},"effective_status":{"type":"string","description":"Estado calculado por Meta, de solo lectura."},"daily_budget":{"type":"string","description":"Presupuesto diario en centavos.","example":"50000"},"lifetime_budget":{"type":"string","description":"Presupuesto total en centavos."},"billing_event":{"enum":["APP_INSTALLS","CLICKS","IMPRESSIONS","LINK_CLICKS","NONE","OFFER_CLAIMS","PAGE_LIKES","POST_ENGAGEMENT","THRUPLAY","PURCHASE","LISTING_INTERACTION"],"type":"string"},"optimization_goal":{"enum":["NONE","APP_INSTALLS","AD_RECALL_LIFT","ENGAGED_USERS","EVENT_RESPONSES","IMPRESSIONS","LEAD_GENERATION","QUALITY_LEAD","LINK_CLICKS","OFFSITE_CONVERSIONS","PAGE_LIKES","POST_ENGAGEMENT","QUALITY_CALL","REACH","LANDING_PAGE_VIEWS","VISIT_INSTAGRAM_PROFILE","ENGAGED_PAGE_VIEWS","VALUE","THRUPLAY","DERIVED_EVENTS","APP_INSTALLS_AND_OFFSITE_CONVERSIONS","CONVERSATIONS","IN_APP_VALUE","MESSAGING_PURCHASE_CONVERSION","MESSAGING_DEEP_CONVERSATION_AND_FOLLOW","SUBSCRIBERS","REMINDERS_SET","MEANINGFUL_CALL_ATTEMPT","PROFILE_VISIT","PROFILE_AND_PAGE_ENGAGEMENT","ADVERTISER_SILOED_VALUE","AUTOMATIC_OBJECTIVE","MESSAGING_APPOINTMENT_CONVERSION"],"type":"string"},"bid_amount":{"type":"string","description":"Puja en centavos."},"bid_strategy":{"enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"],"type":"string"},"start_time":{"type":"string","example":"2026-09-01T00:00:00-0300"},"end_time":{"type":"string","example":"2026-09-30T23:59:59-0300"},"targeting":{"type":"object","additionalProperties":true,"description":"Segmentación tal como la guarda Meta."},"promoted_object":{"type":"object","additionalProperties":true,"description":"Objeto promocionado (página, app o píxel)."},"created_time":{"type":"string"},"updated_time":{"type":"string"}},"required":["id","name"]},"CreateAdSetDto":{"type":"object","properties":{"ad_account_id":{"type":"string","description":"Cuenta publicitaria de Meta, con prefijo.","example":"act_123"},"campaign_id":{"type":"string","description":"Campaña a la que cuelga el conjunto."},"name":{"type":"string","example":"AR 25-45 · feed"},"status":{"enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"type":"string","description":"Si no viene, el conjunto se crea `PAUSED`.","default":"PAUSED"},"daily_budget":{"type":"number","nullable":true,"description":"Presupuesto diario en centavos. Excluyente con `lifetime_budget`.","example":50000},"lifetime_budget":{"type":"number","nullable":true,"description":"Presupuesto total en centavos. Excluyente con `daily_budget` y exige `end_time`."},"billing_event":{"enum":["APP_INSTALLS","CLICKS","IMPRESSIONS","LINK_CLICKS","NONE","OFFER_CLAIMS","PAGE_LIKES","POST_ENGAGEMENT","THRUPLAY","PURCHASE","LISTING_INTERACTION"],"type":"string"},"optimization_goal":{"enum":["NONE","APP_INSTALLS","AD_RECALL_LIFT","ENGAGED_USERS","EVENT_RESPONSES","IMPRESSIONS","LEAD_GENERATION","QUALITY_LEAD","LINK_CLICKS","OFFSITE_CONVERSIONS","PAGE_LIKES","POST_ENGAGEMENT","QUALITY_CALL","REACH","LANDING_PAGE_VIEWS","VISIT_INSTAGRAM_PROFILE","ENGAGED_PAGE_VIEWS","VALUE","THRUPLAY","DERIVED_EVENTS","APP_INSTALLS_AND_OFFSITE_CONVERSIONS","CONVERSATIONS","IN_APP_VALUE","MESSAGING_PURCHASE_CONVERSION","MESSAGING_DEEP_CONVERSATION_AND_FOLLOW","SUBSCRIBERS","REMINDERS_SET","MEANINGFUL_CALL_ATTEMPT","PROFILE_VISIT","PROFILE_AND_PAGE_ENGAGEMENT","ADVERTISER_SILOED_VALUE","AUTOMATIC_OBJECTIVE","MESSAGING_APPOINTMENT_CONVERSION"],"type":"string"},"bid_amount":{"type":"number","nullable":true,"description":"Puja en centavos."},"bid_strategy":{"nullable":true,"enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"],"type":"string"},"start_time":{"type":"string","nullable":true,"example":"2026-09-01T00:00:00-0300"},"end_time":{"type":"string","nullable":true,"example":"2026-09-30T23:59:59-0300"},"targeting":{"type":"object","additionalProperties":true,"description":"Segmentación. Viaja como objeto y Neike la serializa a JSON antes de mandarla a Graph.","example":{"geo_locations":{"countries":["AR"]},"age_min":25,"age_max":45,"publisher_platforms":["facebook","instagram"]}},"promoted_object":{"type":"object","additionalProperties":true,"nullable":true,"description":"`{page_id}`, `{application_id, object_store_url}` o `{pixel_id}`."},"dsa_payor":{"type":"string","nullable":true,"description":"Pagador declarado (DSA europea)."},"dsa_beneficiary":{"type":"string","nullable":true,"description":"Beneficiario declarado (DSA europea)."}},"required":["ad_account_id","campaign_id","name"]},"UpdateAdSetDto":{"type":"object","properties":{"ad_account_id":{"type":"string","description":"Cuenta publicitaria dueña del conjunto.","example":"act_123"},"name":{"type":"string"},"status":{"enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"type":"string"},"daily_budget":{"type":"number","nullable":true,"description":"Presupuesto diario en centavos."},"lifetime_budget":{"type":"number","nullable":true,"description":"Presupuesto total en centavos. Exige `end_time`."},"billing_event":{"enum":["APP_INSTALLS","CLICKS","IMPRESSIONS","LINK_CLICKS","NONE","OFFER_CLAIMS","PAGE_LIKES","POST_ENGAGEMENT","THRUPLAY","PURCHASE","LISTING_INTERACTION"],"type":"string"},"optimization_goal":{"enum":["NONE","APP_INSTALLS","AD_RECALL_LIFT","ENGAGED_USERS","EVENT_RESPONSES","IMPRESSIONS","LEAD_GENERATION","QUALITY_LEAD","LINK_CLICKS","OFFSITE_CONVERSIONS","PAGE_LIKES","POST_ENGAGEMENT","QUALITY_CALL","REACH","LANDING_PAGE_VIEWS","VISIT_INSTAGRAM_PROFILE","ENGAGED_PAGE_VIEWS","VALUE","THRUPLAY","DERIVED_EVENTS","APP_INSTALLS_AND_OFFSITE_CONVERSIONS","CONVERSATIONS","IN_APP_VALUE","MESSAGING_PURCHASE_CONVERSION","MESSAGING_DEEP_CONVERSATION_AND_FOLLOW","SUBSCRIBERS","REMINDERS_SET","MEANINGFUL_CALL_ATTEMPT","PROFILE_VISIT","PROFILE_AND_PAGE_ENGAGEMENT","ADVERTISER_SILOED_VALUE","AUTOMATIC_OBJECTIVE","MESSAGING_APPOINTMENT_CONVERSION"],"type":"string"},"bid_amount":{"type":"number","nullable":true,"description":"Puja en centavos."},"bid_strategy":{"nullable":true,"enum":["LOWEST_COST_WITHOUT_CAP","LOWEST_COST_WITH_BID_CAP","COST_CAP","LOWEST_COST_WITH_MIN_ROAS"],"type":"string"},"start_time":{"type":"string","nullable":true},"end_time":{"type":"string","nullable":true},"targeting":{"type":"object","additionalProperties":true},"promoted_object":{"type":"object","additionalProperties":true,"nullable":true},"dsa_payor":{"type":"string","nullable":true},"dsa_beneficiary":{"type":"string","nullable":true}},"required":["ad_account_id"]},"AdCreativeRefDto":{"type":"object","properties":{"creative_id":{"type":"string","description":"Id de la creatividad ya existente."},"id":{"type":"string"},"name":{"type":"string"},"thumbnail_url":{"type":"string"}}},"AdDto":{"type":"object","properties":{"id":{"type":"string","example":"120210000000000000"},"name":{"type":"string","example":"Anuncio tráfico · imagen"},"adset_id":{"type":"string"},"campaign_id":{"type":"string"},"status":{"enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"type":"string"},"effective_status":{"type":"string","description":"Estado calculado por Meta, incluye el de revisión."},"creative":{"$ref":"#/components/schemas/AdCreativeRefDto"},"created_time":{"type":"string"},"updated_time":{"type":"string"}},"required":["id","name"]},"CreateAdDto":{"type":"object","properties":{"ad_account_id":{"type":"string","description":"Cuenta publicitaria de Meta, con prefijo.","example":"act_123"},"name":{"type":"string","example":"Anuncio tráfico · imagen"},"adset_id":{"type":"string","description":"Conjunto al que cuelga el anuncio. Después no se puede cambiar."},"creative":{"description":"Creatividad a mostrar, por id: `{ \"creative_id\": \"123\" }`.","allOf":[{"$ref":"#/components/schemas/AdCreativeRefDto"}]},"status":{"enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"type":"string","description":"Si no viene, el anuncio se crea `PAUSED`.","default":"PAUSED"},"tracking_specs":{"type":"array","description":"Specs de seguimiento de Meta. Se pasan tal cual.","items":{"type":"object"}}},"required":["ad_account_id","name","adset_id","creative"]},"UpdateAdDto":{"type":"object","properties":{"ad_account_id":{"type":"string","description":"Cuenta publicitaria dueña del anuncio.","example":"act_123"},"name":{"type":"string"},"status":{"enum":["ACTIVE","PAUSED","DELETED","ARCHIVED"],"type":"string"},"creative":{"$ref":"#/components/schemas/AdCreativeRefDto"},"tracking_specs":{"type":"array","items":{"type":"object"}},"adset_id":{"type":"string","description":"NO se acepta: mover un anuncio de conjunto es 400. Se crea uno nuevo.","deprecated":true}},"required":["ad_account_id"]},"CallToActionDto":{"type":"object","properties":{"type":{"enum":["LEARN_MORE","SHOP_NOW","SIGN_UP","BOOK_TRAVEL","DOWNLOAD","GET_QUOTE","CONTACT_US","SUBSCRIBE","APPLY_NOW","GET_OFFER","MESSAGE_PAGE","WHATSAPP_MESSAGE","CALL_NOW","DONATE_NOW","SEE_MENU","ORDER_NOW","PLAY_GAME","INSTALL_MOBILE_APP","NO_BUTTON"],"type":"string"},"value":{"type":"object","additionalProperties":true,"description":"Destino del botón, p. ej. `{ \"link\": \"https://…\" }`."}},"required":["type"]},"LinkDataDto":{"type":"object","properties":{"message":{"type":"string","description":"Texto principal del anuncio."},"link":{"type":"string","description":"URL de destino.","example":"https://mitienda.com/promo"},"name":{"type":"string","description":"Título."},"description":{"type":"string","description":"Bajada."},"image_hash":{"type":"string","description":"Hash devuelto por `POST /api/marketing/images`."},"call_to_action":{"$ref":"#/components/schemas/CallToActionDto"}},"required":["link"]},"VideoDataDto":{"type":"object","properties":{"video_id":{"type":"string","description":"Id devuelto por `POST /api/marketing/videos`."},"image_url":{"type":"string","description":"Miniatura del video."},"image_hash":{"type":"string","description":"Hash de la miniatura, alternativa a `image_url`."},"call_to_action":{"$ref":"#/components/schemas/CallToActionDto"}},"required":["video_id"]},"ObjectStorySpecDto":{"type":"object","properties":{"page_id":{"type":"string","description":"Página de Facebook que publica. Sale de `GET /api/marketing/pages`."},"instagram_actor_id":{"type":"string","nullable":true,"description":"Cuenta de Instagram vinculada, si el anuncio también se muestra ahí."},"link_data":{"description":"Excluyente con `video_data`.","allOf":[{"$ref":"#/components/schemas/LinkDataDto"}]},"video_data":{"description":"Excluyente con `link_data`.","allOf":[{"$ref":"#/components/schemas/VideoDataDto"}]}},"required":["page_id"]},"CreativeDto":{"type":"object","properties":{"id":{"type":"string","example":"120210000000000000"},"name":{"type":"string","example":"Creatividad tráfico"},"status":{"type":"string"},"object_story_spec":{"$ref":"#/components/schemas/ObjectStorySpecDto"},"object_story_id":{"type":"string"},"effective_object_story_id":{"type":"string"},"thumbnail_url":{"type":"string"},"image_hash":{"type":"string"},"image_url":{"type":"string"},"video_id":{"type":"string"},"call_to_action_type":{"enum":["LEARN_MORE","SHOP_NOW","SIGN_UP","BOOK_TRAVEL","DOWNLOAD","GET_QUOTE","CONTACT_US","SUBSCRIBE","APPLY_NOW","GET_OFFER","MESSAGE_PAGE","WHATSAPP_MESSAGE","CALL_NOW","DONATE_NOW","SEE_MENU","ORDER_NOW","PLAY_GAME","INSTALL_MOBILE_APP","NO_BUTTON"],"type":"string"}},"required":["id"]},"AdImageDto":{"type":"object","properties":{"hash":{"type":"string","description":"Hash con el que la creatividad referencia la imagen."},"url":{"type":"string","nullable":true,"description":"URL del archivo original."},"url_128":{"type":"string","nullable":true,"description":"Miniatura de 128 px."},"name":{"type":"string","nullable":true},"width":{"type":"number","nullable":true},"height":{"type":"number","nullable":true},"permalink_url":{"type":"string","nullable":true,"description":"Enlace a la imagen en el Administrador de anuncios de Meta."}},"required":["hash"]},"AdImagesResponseDto":{"type":"object","properties":{"images":{"type":"array","items":{"$ref":"#/components/schemas/AdImageDto"}}},"required":["images"]},"CreateCreativeDto":{"type":"object","properties":{"ad_account_id":{"type":"string","description":"Cuenta publicitaria de Meta, con prefijo.","example":"act_123"},"name":{"type":"string","example":"Creatividad tráfico"},"object_story_spec":{"$ref":"#/components/schemas/ObjectStorySpecDto"},"degrees_of_freedom_spec":{"type":"object","additionalProperties":true,"nullable":true,"description":"Configuración de variaciones automáticas de Meta. Se pasa tal cual."}},"required":["ad_account_id","object_story_spec"]},"UploadImageDto":{"type":"object","properties":{"ad_account_id":{"type":"string","description":"Cuenta publicitaria de Meta, con prefijo.","example":"act_123"},"name":{"type":"string","description":"Nombre con el que queda archivada en Meta."},"bytes_base64":{"type":"string","description":"Contenido del archivo en base64, sin el prefijo `data:`. Máximo 7 MB."}},"required":["ad_account_id","bytes_base64"]},"UploadedImageDto":{"type":"object","properties":{"hash":{"type":"string","description":"Hash a usar en `link_data.image_hash`."},"url":{"type":"string","nullable":true},"width":{"type":"number","nullable":true},"height":{"type":"number","nullable":true},"name":{"type":"string","nullable":true}},"required":["hash"]},"UploadVideoDto":{"type":"object","properties":{"ad_account_id":{"type":"string","description":"Cuenta publicitaria de Meta, con prefijo.","example":"act_123"},"name":{"type":"string","description":"Nombre con el que queda archivado en Meta."},"file_url":{"type":"string","description":"URL pública del video: Meta lo descarga desde ahí. Es la alternativa a mandar el archivo en `file`; uno de los dos es obligatorio.","example":"https://cdn.mitienda.com/spot.mp4"},"file":{"type":"string","format":"binary","description":"Archivo del video, en `multipart/form-data`. Máximo 100 MB."}},"required":["ad_account_id"]},"UploadedVideoDto":{"type":"object","properties":{"id":{"type":"string","description":"Id a usar en `video_data.video_id`."}},"required":["id"]},"VideoThumbnailDto":{"type":"object","properties":{"id":{"type":"string"},"uri":{"type":"string","description":"URL de la imagen, para usar en `video_data.image_url`."},"width":{"type":"number","nullable":true},"height":{"type":"number","nullable":true},"is_preferred":{"type":"boolean","nullable":true,"description":"La que Meta eligió como portada por defecto."}},"required":["id","uri"]},"VideoDto":{"type":"object","properties":{"id":{"type":"string","example":"1203450000000000"},"title":{"type":"string","nullable":true},"status":{"type":"string","nullable":true,"description":"`status.video_status` de Meta. Hasta que sea `ready` el video no se puede usar en una creatividad.","enum":["ready","processing","error","expired"]},"processing_status":{"type":"string","nullable":true,"description":"Fase de procesamiento (`status.processing_phase.status`)."},"error":{"type":"string","nullable":true,"description":"Motivo del fallo cuando `status` es `error`."},"thumbnails":{"description":"Miniaturas que generó Meta. Vienen recién cuando el video está listo.","type":"array","items":{"$ref":"#/components/schemas/VideoThumbnailDto"}}},"required":["id","thumbnails"]},"InsightsQueryDto":{"type":"object","properties":{"ad_account_id":{"type":"string","description":"Cuenta publicitaria de Meta, con prefijo.","example":"act_123"},"object_id":{"type":"string","description":"Campaña, conjunto o anuncio sobre el que consultar. Si no viene, se consulta la cuenta entera."},"level":{"enum":["account","campaign","adset","ad"],"type":"string"},"date_preset":{"type":"string","description":"Rango con nombre. Es string libre: Meta valida el valor. Los de uso corriente están en `GET /api/marketing/enums` como sugerencias.","example":"last_7d"},"time_range":{"type":"object","description":"Rango explícito `{\"since\":\"YYYY-MM-DD\",\"until\":\"YYYY-MM-DD\"}`. En el GET viaja como JSON en el query string; en el POST puede venir como objeto."},"time_increment":{"type":"string","description":"Corte temporal de las filas. String libre: un número de días (`1`, `7`, `28`), `monthly` o `all_days`. `1` es el valor típico para una serie diaria. Si no se manda, Graph agrega todo el rango en una sola fila.","example":"1"},"fields":{"type":"string","description":"Métricas separadas por coma. Por defecto: `impressions,clicks,spend,reach,frequency,cpc,cpm,ctr,actions,cost_per_action_type,date_start,date_stop`."},"breakdowns":{"type":"string","description":"Desgloses separados por coma, p. ej. `age,gender`."},"action_breakdowns":{"type":"string","description":"Desgloses de acciones separados por coma."}},"required":["ad_account_id"]},"InsightsReportRunDto":{"type":"object","properties":{"report_run_id":{"type":"string","description":"Id del reporte. Vence a los 30 días."},"async_status":{"type":"string","nullable":true,"description":"`Job Not Started`, `Job Started`, `Job Running`, `Job Completed`, `Job Failed`…"},"async_percent_completion":{"type":"number","nullable":true,"description":"0 a 100."},"error_code":{"type":"number","nullable":true},"error_message":{"type":"string","nullable":true}},"required":["report_run_id","async_status","async_percent_completion","error_code","error_message"]},"CreateEmbedAdsSessionDto":{"type":"object","properties":{"allowed_origins":{"description":"Orígenes autorizados a embeber el iframe. Alimentan el `frame-ancestors` del CSP y el targetOrigin del postMessage de resultado. Hasta 10.","example":["https://app.tu-dominio.com"],"type":"array","items":{"type":"string"}},"external_ref":{"type":"string","description":"Id del negocio en el sistema del cliente. Vuelve tal cual en el webhook y en el resultado, para correlacionar las cuentas con su tenant.","example":"tenant_8842"},"ttl_minutes":{"type":"number","description":"Minutos de validez de la sesión.","default":30,"minimum":1,"maximum":1440},"provider":{"type":"string","description":"Proveedor de anuncios. Hoy solo `meta`; otro valor responde 400 `unsupported_provider`.","enum":["meta"],"default":"meta"}},"required":["allowed_origins"]},"EmbedAdsSessionCreatedDto":{"type":"object","properties":{"session_id":{"type":"string","example":"nkes_9fQ2aB…"},"token":{"type":"string","description":"Token de la sesión. **Se devuelve una única vez**; en base queda su SHA-256.","example":"nkst_…"},"embed_url":{"type":"string","description":"URL lista para embeber en el iframe.","example":"https://api.neike.dev/embed/ads?token=nkst_…"},"iframe_snippet":{"type":"string","description":"Snippet `<iframe>` listo para pegar (sandbox incluido)."},"provider":{"type":"string","enum":["meta"],"example":"meta"},"allowed_origins":{"type":"array","items":{"type":"string"}},"external_ref":{"type":"string","nullable":true},"expires_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["pending"],"example":"pending"}},"required":["session_id","token","embed_url","iframe_snippet","provider","allowed_origins","expires_at","status"]},"EmbedAdsSessionViewDto":{"type":"object","properties":{"session_id":{"type":"string","example":"nkes_9fQ2aB…"},"status":{"type":"string","description":"`pending` (nadie abrió el iframe), `opened`, `completed`, `revoked` o `expired` (calculado al leer).","enum":["pending","opened","completed","revoked","expired"]},"provider":{"type":"string","nullable":true,"enum":["meta"]},"allowed_origins":{"type":"array","items":{"type":"string"}},"external_ref":{"type":"string","nullable":true},"result":{"type":"object","additionalProperties":true,"nullable":true,"description":"Con `status: completed`, `{ provider, ad_accounts: [{ ad_account_id, name, status }] }` — las cuentas que el usuario final activó. Mientras la sesión está en curso trae las cuentas que el login de Meta descubrió."},"last_error":{"type":"string","nullable":true},"expires_at":{"type":"string","format":"date-time"},"opened_at":{"type":"string","nullable":true,"format":"date-time"},"completed_at":{"type":"string","nullable":true,"format":"date-time"},"created_at":{"type":"string","format":"date-time"}},"required":["session_id","status","provider","allowed_origins","expires_at","created_at"]}}}}