{"openapi": "3.1.0", "info": {"title": "Funnelchat API", "version": "v0", "description": "> **Especificación unificada del portal** · Toda la API pública de Funnelchat detrás de una sola URL base (`https://api.funnelchat.com`), con las rutas completas `/api/v1/…`. La arma el portal a partir de las instantáneas de cada dominio: conversations `68cce1b4e` (2026-10-22 16:30 (-03:00)); communities `6f32f0b4` (2026-10-22 09:46 (-03:00)); accounts `300cc8435` (2026-10-22 09:46 (-03:00)).\n\nEl dominio que atiende cada ruta lo decide el borde de la API, no el portal: cada operación lo dice en `x-fc-domain`. Una ruta que figura en la instantánea de un dominio pero que el borde manda a otro no entra y queda listada en `x-fc-excluded-routes`. Cada operación lleva el permiso que pide (`x-fc-permission`) y, si no está abierta a claves en v0, una «Nota del portal». Si dos dominios usan el mismo nombre para cosas distintas, el componente lleva el nombre del dominio delante. Las especificaciones de cada dominio siguen en `specs/<dominio>.json`; de dónde salió cada una, en `x-fc-sources`.\n\nv0 en desarrollo: cambia sin aviso."}, "servers": [{"url": "https://api.funnelchat.com"}], "security": [{"http": []}], "tags": [{"name": "Conversation", "x-displayName": "Conversaciones", "description": "Una conversación es el chat de WhatsApp entre tu negocio y un contacto: puede estar activa o no (`active_conversation`), asignada a un asesor, marcada como favorita o sin leer, y la atiende uno de tus números. Acá podés listarlas con filtros, contarlas, gestionarlas traer el historial de mensajes de un contacto y ver un mensaje por su id, por ejemplo el que te devolvió un envío. Para armar tu propia bandeja, empezá por *Listar conversaciones* con `updated_since`."}, {"name": "ShowMessage"}, {"name": "Message", "x-displayName": "Mensajes", "description": "Contiene el envío de un mensaje de texto o archivo indicando en el cuerpo el `contact_id` y el número desde el que sale (`device_id`). Para enviar desde la ruta del contacto o leer el historial de mensajes, mirá Conversaciones. Requiere el permiso `conversations:send`; revisá la nota de disponibilidad de la operación."}, {"name": "MessageTemplate", "x-displayName": "Plantillas", "description": "Una plantilla es un mensaje de WhatsApp que Meta aprueba antes de usarse, con encabezado, cuerpo con variables, pie y botones, asociado a un número (`device_id`) y su WABA. Tiene `category` (`UTILITY`, `MARKETING` o `AUTHENTICATION`), `language_code` y un `status` de aprobación, como `PENDING`, `APPROVED` o `REJECTED`. La usan las difusiones y la necesitás cuando la ventana de 24 horas del contacto está cerrada."}, {"name": "MessageReaction", "x-displayName": "Reacciones", "description": "Una reacción es un emoji que ponés sobre un mensaje de una conversación, por ejemplo para acusar recibo sin escribir una respuesta. Podés reaccionar (`emoji`) o quitar tu reacción; las reacciones de cada mensaje vienen en `reactions` al listar los mensajes de un contacto. Requiere el permiso `conversations:send`; revisá la nota de disponibilidad de cada operación."}, {"name": "ForwardMessage", "x-displayName": "Reenvío de mensajes", "description": "Permite reenviar un mensaje de una conversación a otros contactos, eligiendo en `targets` desde qué número (`device_id`) sale cada envío. La respuesta trae un resultado por destino (`status`, `reason`). Requiere el permiso `conversations:send`; revisá la nota de disponibilidad de la operación."}, {"name": "ContactCard", "x-displayName": "Tarjetas de contacto", "description": "Una tarjeta de contacto le comparte a un contacto los datos de otra persona, por ejemplo de un vendedor o de soporte. Se envía desde uno de tus números (`device_id`) e indica en `contact_card_ids` qué tarjetas mandar. Requiere el permiso `conversations:send`; revisá la nota de disponibilidad de la operación."}, {"name": "WhatsappFormSend", "x-displayName": "Formularios de WhatsApp", "description": "Un formulario de WhatsApp es un formulario de varias pantallas que el contacto completa dentro del chat. Acá se envía uno ya publicado a un contacto, con `form_id` (el identificador estable del formulario) y el número de WhatsApp Cloud por el que sale (`device_id`). Cuando el contacto lo completa llega el evento `whatsapp_form.completed`. Requiere el permiso `conversations:send`."}, {"name": "Contact", "x-displayName": "Contactos", "description": "Un contacto es la persona con la que tu negocio conversa por WhatsApp, identificada por `phone` o `whatsapp_id`. Su ficha reúne nombre, email, país, notas, campos personalizados, asesor asignado (`adviser_id`), el número que lo atiende (`device_id`) y su `lead_score`. Se relaciona con etiquetas, etapas de tableros, flujos y difusiones. Para sincronizar tu CRM sin duplicados, empezá por *Crear o actualizar un contacto*."}, {"name": "ContactNote", "x-displayName": "Notas de contactos", "description": "Las notas son comentarios que el equipo deja en la ficha de un contacto, con texto (`body`), autor (`author_name`) y fecha. Sirven para registrar desde tu sistema algo que pasó fuera del chat, como una llamada, una compra o un reclamo. Podés listarlas con búsqueda por texto, agregarlas y eliminarlas."}, {"name": "Tag", "x-displayName": "Etiquetas", "description": "Las etiquetas (`name`, `description`, `color`) marcan contactos para segmentarlos según los criterios de tu negocio, y cada una puede informar cuántos contactos la tienen (`contact_count`). Se usan como filtro en contactos, conversaciones y difusiones, y un tablero en modo `tags` se arma con etiquetas. Para exportar un segmento, usá *Listar los contactos con una etiqueta*."}, {"name": "CustomField", "x-displayName": "Campos personalizados", "description": "Un campo personalizado es un dato propio de tu negocio que guardás en los contactos, como plan, ciudad o fecha de compra. Acá definís el campo (`name`, `description`, `type`; el tipo no se cambia después); los valores se cargan con *Asignar un campo personalizado a contactos*. Un tablero puede usar campos como valor y ganancia (`value_custom_field_id`, `profit_custom_field_id`)."}, {"name": "Grupos", "x-displayName": "Grupos", "description": "Un grupo es un grupo de WhatsApp que administra uno de tus números, con o sin campaña asignada; acá también aparecen los canales y los grupos de anuncios de una comunidad. Su ficha trae `uid`, nombre, link de invitación, administradores, si el dueño está conectado, participantes, clics, `status` e `isFull` (lleno). Podés leer sus datos en vivo desde WhatsApp, sus acciones, sus mensajes y qué envíos programados lo incluyen."}, {"name": "Participantes", "x-displayName": "Participantes", "description": "Un participante es una persona que está o estuvo en un grupo de una campaña, identificada por teléfono (`phone`), con su grupo actual, fechas de entrada y salida (`joined_at`, `left_at`), estado y si es administrador. También hay un registro de entradas y salidas (`join`, `leave`) con fecha y país, por campaña o por grupo. Para sincronizar miembros con tu CRM, empezá por *Listar los participantes con su grupo actual*."}, {"name": "Device", "x-displayName": "Números", "description": "Un número es una línea de WhatsApp conectada a tu cuenta (en las rutas, `devices`), por código QR o por WhatsApp Cloud. Trae `alias`, `number`, `provider`, `waba_id`, si está conectado (`connected`) y su salud (`health`). Acá también están su cola de mensajes y las operaciones irreversibles (reiniciar, limpiar, eliminar, vaciar la cola), que piden `accounts:operate`. Empezá por *Listar números*: sus IDs los usan las demás operaciones."}, {"name": "DeviceHealth", "x-displayName": "Salud del número", "description": "La salud describe cómo ve Meta a un número de WhatsApp Cloud: `health`, identidad, entidades, nivel de throughput (`throughput_level`) e historial de cambios. *Ver la salud de un número* lee el último dato guardado sin consultar a Meta; *Actualizar la salud de un número* se lo pide a Meta, con una espera de 60 segundos entre consultas, y `refresh_outcome` dice si se refrescó."}, {"name": "DeviceConnectionEvent", "x-displayName": "Historial de conexión", "description": "Registra cada vez que un número se conectó o se desconectó (`event`), con el teléfono y la fecha, del evento más reciente al más antiguo. Sirve para investigar cortes; para enterarte en el momento, suscribite a `device.disconnected` desde Endpoints de webhooks."}, {"name": "DeviceLogs", "x-displayName": "Registros del número", "description": "Los registros de actividad de un número muestran cada petición con método, URL, código HTTP, duración, si fue error y los cuerpos de pedido y respuesta, en una ventana de tiempo y paginados por cursor. *Ver la señal de salud de los registros* mira las últimas 5 peticiones que no son envíos y da un veredicto: `healthy`, `review` o `attention`."}, {"name": "DeviceFleetLogs", "x-displayName": "Registros globales", "description": "Son los mismos registros de actividad de Registros del número, pero de todos tus números juntos: método, URL, código HTTP, duración y errores en una ventana de tiempo, del más reciente al más antiguo y paginados por cursor. Cada registro indica de qué número es y la primera página trae un resumen. Sirve para diagnosticar fallas en toda la cuenta."}, {"name": "DeviceUsername", "x-displayName": "Nombre de usuario", "description": "Es el nombre de usuario de WhatsApp que se reserva en Meta para uno de tus números. Podés consultar el reservado (el que informa Meta y el guardado en Funnelchat), pedir sugerencias y reservar uno. Empezá por *Sugerir nombres de usuario* y después usá *Configurar el nombre de usuario*."}, {"name": "DeviceDisconnectSurvey", "x-displayName": "Encuesta de desconexión", "description": "Es la encuesta sobre el motivo por el que se desconectó un número. Podés saber si está pendiente (`pending`) y responderla con uno de los motivos predefinidos (`reason`)."}, {"name": "Flow", "x-displayName": "Flujos", "description": "Un flujo es una automatización: un disparador (`trigger_type`, `trigger_values`, `trigger_device_ids`) inicia una cadena de nodos (`nodes`) que se ejecuta para un contacto. Cada ejecución queda en curso, completada, invalidada o cancelada, y la podés cancelar. Los flujos se ordenan en Carpetas de flujos. Para diagnosticar por qué un contacto recibió o no una automatización, usá *Listar los flujos en curso de un contacto*."}, {"name": "Folder", "x-displayName": "Carpetas de flujos", "description": "Las carpetas organizan tus flujos de automatización en una jerarquía: cada una tiene `name`, una carpeta madre opcional (`parent_id`), subcarpetas (`children`) y la cantidad de flujos que contiene (`flows_count`). Usá sus IDs para filtrar *Listar flujos* por `folder_id`. Las carpetas de difusiones son otras y están en Difusiones."}, {"name": "ContactAutomation", "x-displayName": "Automatizaciones del contacto", "description": "Reúne lo que define cómo participa un contacto de las automatizaciones de la cuenta. *Pausar un contacto* lo pone en pausa (`paused_at`), y después podés filtrar a los pausados en conversaciones y difusiones; *Listar los flujos de un contacto* muestra en qué flujos está y si alguno sigue corriendo (`running`). Para que no ejecute flujos, cambiá `can_execute_flows` con *Actualizar un contacto*."}, {"name": "Campañas", "x-displayName": "Campañas", "description": "Una campaña agrupa grupos de WhatsApp de tu cuenta detrás de un link de redirección y define cómo se distribuye el tráfico entre ellos (`traffic_distribution_type`), con límites de clics y participantes (`clicks_limit`, `participants_limit`). Su ficha incluye administradores, píxel, moderación, grupos desconectados o con error y reglas condicionales de redirección. Empezá por *Listar campañas*: sus IDs los piden grupos, participantes y estadísticas."}, {"name": "Grupos de una campaña y operaciones", "x-displayName": "Operaciones de campaña", "description": "Reúne los grupos de una campaña (los que tiene y los que se pueden sumar) y las operaciones masivas sobre ellos: cambios de nombre, foto, descripción, administradores o participantes. Los planes de operación las programan con tope diario, espaciado y franja horaria; los pipelines muestran su avance por estado, y las acciones de sesión, qué cambió en cada grupo (valor anterior y nuevo) y con qué resultado."}, {"name": "Mensajes programados", "x-displayName": "Mensajes programados", "description": "Un envío programado manda uno o más mensajes a grupos de WhatsApp en una fecha (`send_at`), una vez o con recurrencia, a campañas o grupos puntuales y con grupos excluidos. Su ficha trae estado, audiencia, lecturas, reproducciones, clics en links y velocidad de envío; además podés ver el resultado en cada grupo y cada ocurrencia de un envío recurrente. Empezá por *Listar envíos programados*."}, {"name": "Respuestas y carpetas", "x-displayName": "Respuestas y carpetas", "description": "Complementa a los mensajes programados. Las carpetas los organizan e informan cuántos envíos tiene cada una y cuántos quedan sin clasificar; las respuestas son los votos en encuestas y las respuestas a eventos de un envío: quién respondió, en qué grupo, qué eligió y cuándo. También muestra qué etiquetas se aplican a quien responde según la opción elegida."}, {"name": "Estadísticas", "x-displayName": "Estadísticas", "description": "Métricas de tus grupos o campañas por hora, día o mes: entradas y salidas de participantes, participantes activos e inactivos, grupos por estado y clics en links. Suma métricas por país y los registros individuales (país, dispositivo, navegador, referencia, grupo y datos de contacto si los hay). Con clave, las series por período piden `start_date` y `end_date`, con un rango de hasta 366 días."}, {"name": "Broadcast", "x-displayName": "Difusiones", "description": "Una difusión es un envío masivo de una plantilla (`template_id`) desde uno de tus números a una audiencia segmentada por etiquetas, países, etapa de un tablero o lead score. Desde la API la podés consultar, medir (estadísticas, costo y estado por destinatario), cancelar y ordenar en carpetas. Para dimensionar una audiencia, empezá por *Contar contactos para una difusión*."}, {"name": "KanbanSetting", "x-displayName": "Tableros", "description": "Un tablero kanban modela un proceso, como tu embudo de ventas o de atención, y ubica a cada contacto en una de sus etapas. Se arma por etapas o por etiquetas (`mode`), define a los cuántos días un contacto queda estancado (`stagnant_days`) y puede usar campos personalizados como valor y ganancia. Empezá por *Listar tableros*: sus IDs los piden las etapas, las conversaciones y las difusiones."}, {"name": "KanbanStage", "x-displayName": "Etapas de tableros", "description": "Las etapas son los pasos de un tablero (`name`, `position`, `contacts_count`), y en cada tablero un contacto tiene una etapa actual (`current_stage_id`). Acá creás, reordenás y eliminás etapas, y movés o sacás contactos, de a uno o en bloque. Para reflejar el avance de un lead cuando algo pasa en tu sistema, usá *Mover un contacto de etapa*."}, {"name": "LeadScoreSettings", "x-displayName": "Configuración del lead score", "description": "Define cómo se calcula el lead score de la cuenta: los umbrales de caliente y tibio (`hot_threshold`, `warm_threshold`), las señales mínimas (`min_signals`), la histéresis, el decaimiento de puntos con los días (`decay_steps`) y las reglas. También incluye *Ajustar el lead score de un contacto*, para sumar o restar puntos a mano con un motivo."}, {"name": "LeadScoreDistribution", "x-displayName": "Distribución del lead score", "description": "Muestra cómo se reparte tu base de contactos entre las categorías del lead score (`hot`, `warm`, `cold` e `insufficient_data`), con el total y los umbrales vigentes. Sirve para un tablero de calidad de leads; para cambiar los umbrales, mirá Configuración del lead score."}, {"name": "LeadScoreEventIngest", "x-displayName": "Eventos de lead score", "description": "Recibe señales de interacción que alimentan el lead score (`signal`): entradas y salidas de grupos, reacciones y votos en grupos, y clics en links. Cada evento identifica al contacto por `contact_id` o `phone` y lleva `occurred_at` y un `external_id`. Se mandan hasta 100 por llamada y se procesan después: la respuesta es `202`."}, {"name": "ContactLeadScore", "x-displayName": "Lead score del contacto", "description": "El lead score estima qué tan caliente está un contacto a partir de señales de interacción. Para un contacto trae `score`, `category` (`hot`, `warm`, `cold` o `insufficient_data`), `confidence`, si está descalificado y el historial de señales que lo explican (`events`), paginado por cursor. Las reglas del cálculo están en Configuración del lead score."}, {"name": "SalesDashboard", "x-displayName": "Ventas", "description": "Reúne las ventas que reportan tus plataformas de pagos: cada venta trae producto, monto (`amount_minor`) y moneda, plataforma, tipo de evento, comprador (con su `contact_id` si lo hay) y atribución. Además del listado, hay métricas, el embudo de conversaciones a leads calificados y compras, y totales por plataforma, siempre en ventanas de 7, 30 o 90 días."}, {"name": "PaymentProduct", "x-displayName": "Productos de pago", "description": "Lista los productos de una plataforma de pagos (`{platform}`) con su ID externo (`external_product_id`) y nombre. Sirve para relacionarlos con las ventas que reporta Funnelchat y con los flujos, que guardan `payment_platform` y `payment_external_product_id`."}, {"name": "QuickReply", "x-displayName": "Respuestas rápidas", "description": "Las respuestas rápidas son mensajes guardados, de texto o con archivo, que el equipo usa para contestar lo frecuente sin escribirlo cada vez. Cada una tiene `name`, `message`, `file`, un `type`, una visibilidad (`visibility`) y los agentes que la tienen disponible (`agent_ids`). Los IDs de agentes salen de *Listar agentes*."}, {"name": "Ticket", "x-displayName": "Tickets", "description": "Un ticket es un caso abierto sobre un contacto (`contact_id`), con título, descripción, fecha límite (`deadline`) y un usuario del equipo responsable (`user_id`). Se puede crear, editar, reasignar, cerrar y reabrir, y listar por estado o por usuario. Si tu sistema detecta un reclamo, empezá por *Crear un ticket*; los IDs de usuario salen de *Listar agentes*."}, {"name": "User", "x-displayName": "Agentes", "description": "Los agentes son las personas del equipo que atienden en la cuenta, con `name`, `email` y su alcance como asesor (`advisor_scope`). Sus IDs son los que necesitás para asignar conversaciones o tickets y para filtrar por asesor. Empezá por *Listar agentes*."}, {"name": "AlertCatalog", "x-displayName": "Catálogo de alertas", "description": "Las alertas operativas avisan de lo que pasa en tu cuenta: números desconectados, consumo de MACs, cobros, campañas, grupos y más. El catálogo lista cada una con `module`, `action`, categoría, si es crítica (`is_critical`), nombre (`label`) y explicación (`tooltip`). Consultalo antes de activarlas en un destino; las alertas propias de Stripe solo aparecen si tu cuenta paga por Stripe."}, {"name": "AlertDestination", "x-displayName": "Destinos de alertas", "description": "Un destino es a dónde llegan tus alertas: un teléfono o un webhook HTTPS (`type`). Tiene que estar verificado (`verified`) antes de activarle alertas, y trae las que tiene activas (`settings`). Si es un webhook, guardá el secreto de firma: se muestra una sola vez. Empezá por *Crear un destino de alertas*."}, {"name": "AlertOtp", "x-displayName": "Verificación de teléfonos", "description": "Un destino de alertas de tipo teléfono se verifica con un código. *Enviar un código de verificación* lo manda o lo reenvía, con espera entre envíos y tope por hora, y *Verificar el código* lo confirma. Una vez verificado, ya le podés activar alertas."}, {"name": "AlertSetting", "x-displayName": "Activación de alertas", "description": "Define qué alertas recibe cada destino verificado. Cada alerta se identifica por `module` (por ejemplo `device`, `mac`, `billing`, `campaign` o `group`) y `action` (por ejemplo `disconnected` o `payment_failed`), y se prende o se apaga con `enabled`. Los pares válidos salen de *Ver el catálogo de alertas*."}, {"name": "AlertQuietHour", "x-displayName": "Horario de silencio", "description": "Es una franja horaria sin alertas, por ejemplo de noche, definida con `start_time` y `end_time` y activable con `enabled`. Hay una sola configuración por cuenta, que podés consultar o actualizar."}, {"name": "AlertDailyReport", "x-displayName": "Reporte diario", "description": "El reporte diario es una alerta que se envía a la hora que elijas (`send_time`) y cubre todas tus campañas de grupos (`all_campaigns`) o solo algunas (`campaign_ids`). Hay una sola configuración por cuenta; las campañas para elegir salen de *Listar las campañas para alertas*."}, {"name": "AlertCampaign", "x-displayName": "Campañas para alertas", "description": "Lista, con búsqueda por nombre, las campañas de grupos de WhatsApp de la cuenta que podés elegir al configurar alertas como el reporte diario. Cada una trae `id`, `name`, si está activa, cantidad de grupos y tipo; el `id` es el que va en `campaign_ids` de *Configurar el reporte diario*."}, {"name": "AlertWebhookTest", "x-displayName": "Prueba de destinos webhook", "description": "Envía un payload de prueba fijo a un destino de alertas de tipo webhook y te devuelve el estado, si respondió bien (`ok`) y la latencia. Usalo para comprobar que tu receptor de alertas funciona; solo vale para destinos webhook."}, {"name": "Account", "x-displayName": "Mi cuenta", "description": "Es la cuenta de Funnelchat a la que pertenece tu clave de API: su plan, los límites de números y agentes, tu rol, la zona horaria y el idioma. Usá *Ver mi cuenta* al conectar tu integración, para confirmar con qué cuenta estás trabajando."}, {"name": "CustomDomain", "x-displayName": "Dominios propios", "description": "Un dominio propio es un dominio tuyo, por ejemplo para los links de tus campañas, que apunta al servicio de redirección de Funnelchat. Al registrarlo (`hostname`) te devuelve los registros DNS que tenés que cargar (`dns_records`) y su estado; después podés forzar la validación o cambiar el método de validación SSL entre HTTP y TXT. Eliminarlo es irreversible y pide `accounts:operate`."}, {"name": "CapiConnection", "x-displayName": "Conversions API", "description": "Conecta el WABA de un número con un dataset de Meta Conversions API. *Configurar el dataset de Conversions API* lo crea o lo recupera (solo el titular de la cuenta); *Probar la conexión con Conversions API* verifica los permisos del token y el dataset sin enviar eventos y te da el link al Events Manager; el listado muestra el estado de todos tus WABA sin llamar a Meta."}, {"name": "Mac", "x-displayName": "Contactos activos (MAC)", "description": "Un MAC es un contacto activo que cuenta para el límite de contactos activos de tu plan en el ciclo. Cuando lo alcanzás empiezan 24 horas de gracia en las que todo sigue funcionando; si terminan sin que sumes capacidad, tu cuenta entra en bloqueo. *Ver el consumo de MACs* devuelve si el control está activo (`enforcement_enabled`), el estado (`state`: `normal`, `grace`, `enforcing` o `cleared`) y cuántos contactos quedaron registrados como inactivos en el ciclo (`inactive_contacts_count`). Para saber si un contacto puntual es inactivo, cada contacto y cada conversación traen `is_mac_inactive`. En bloqueo, un envío a un contacto inactivo responde `429` con `mac_lockout_24h`, sin `Retry-After`: vuelve a funcionar cuando el contacto deja de ser inactivo. También podés ver qué contactos cuentan como MAC y el historial."}, {"name": "WhatsappCost", "x-displayName": "Costos de WhatsApp", "description": "Muestra cuánto gastaste en WhatsApp en un mes calendario: el actual o el que indiques con `month` (`AAAA-MM`). Sirve para controlar costos o conciliar tu facturación."}, {"name": "WebhookSubscription", "x-displayName": "Suscripciones de webhooks", "description": "Una suscripción le indica a Funnelchat a qué URL HTTPS enviar, con un `POST` firmado, los eventos que elegiste en `events`, de cualquiera de las tres áreas: por ejemplo `message.received`, `group_participant.joined` o `device.disconnected`. Tiene un solo secreto, que se muestra una única vez al crearla, y un solo historial de entregas. Con una clave, cada evento pide además el permiso de lectura de su área. Empezá por *Crear un webhook* y verificá cada entrega con la cabecera `X-Funnelchat-Signature`."}, {"name": "WebhookSubscriptionDelivery", "x-displayName": "Entregas de una suscripción", "description": "Cada intento de entregar un evento a una suscripción queda registrado, del más nuevo al más viejo. Usalo para depurar tu receptor."}, {"name": "WebhookSubscriptionTest", "x-displayName": "Prueba de una suscripción", "description": "Envía ya a la URL de una suscripción una entrega de prueba firmada y te devuelve el resultado. Solo funciona con suscripciones `active` o `paused`."}, {"name": "WebhookEventCatalog", "x-displayName": "Catálogo de eventos", "description": "Lista todos los eventos de las tres áreas (`accounts`, `conversations` y `communities`) a los que te podés suscribir: en cada uno, `area`, si ya se emite (`active`) o está planificado (`planned`), si se puede suscribir y un ejemplo del cuerpo que vas a recibir. Consultalo antes de *Crear un webhook* para elegir los valores de `events`."}, {"name": "WebhookEndpoint", "x-displayName": "Endpoints de webhooks (sistema anterior)", "description": "El sistema anterior de webhooks de la cuenta, en `/api/v1/webhooks/endpoints`: sigue disponible con los permisos `accounts:*`, pero solo para los eventos de la cuenta y sus números, como `device.connected` o `device.disconnected`. Para una integración nueva usá las suscripciones de `/api/v1/webhooks`, que reciben eventos de las tres áreas."}, {"name": "WebhookEndpointEvent", "x-displayName": "Eventos del endpoint", "description": "Define a qué tipos de evento queda suscripto un endpoint, activando o desactivando cada uno (`event_type`, `enabled`). Los tipos cubren números, cobros, suscripción, cuota de IA, MACs y la cuenta, por ejemplo `device.disconnected`, `billing.payment_failed` o `mac.usage_80`; en *Listar tipos de evento* ves cuáles ya se emiten."}, {"name": "WebhookDelivery", "x-displayName": "Entregas de webhooks", "description": "Cada intento de entregar un evento a un endpoint queda registrado con `event_id`, `event_type`, número de intento, URL, código de respuesta, si tuvo éxito (`succeeded`), error y duración, del más reciente al más antiguo. Usalo para depurar tu receptor: `event_id` agrupa los intentos de un mismo evento."}, {"name": "Webhooks de campaña (legado)", "x-displayName": "Webhooks de campaña (legado)", "description": "Son los webhooks configurados dentro de una campaña: un producto anterior, con otro formato, que no recibe los eventos de plataforma. Podés ver cuáles hay (tipo, URL, si están activos) y sus entregas, con payload, respuesta, tiempo de ejecución y error. Para integraciones nuevas, usá las suscripciones de Webhooks."}, {"name": "Webhooks", "x-displayName": "Webhooks", "description": "Una suscripción envía a una URL HTTPS pública, con un `POST` firmado, los eventos de grupos, participantes, campañas y envíos programados que elegiste, por ejemplo `group.created` o `group_participant.joined`. Hay hasta 10 activas por cuenta y el `secret` se muestra una sola vez. Acá también están el catálogo de eventos, la prueba firmada y el historial de entregas; empezá por *Crear un webhook*."}], "x-tagGroups": [{"name": "Mensajes", "x-fc-bloque": "Piezas", "tags": ["Conversation", "ShowMessage", "Message", "MessageTemplate", "MessageReaction", "ForwardMessage", "ContactCard", "WhatsappFormSend"]}, {"name": "Contactos y datos", "x-fc-bloque": "Piezas", "tags": ["Contact", "ContactNote", "Tag", "CustomField"]}, {"name": "Grupos", "x-fc-bloque": "Piezas", "tags": ["Grupos", "Participantes"]}, {"name": "Números", "x-fc-bloque": "Piezas", "tags": ["Device", "DeviceHealth", "DeviceConnectionEvent", "DeviceLogs", "DeviceFleetLogs", "DeviceUsername", "DeviceDisconnectSurvey"]}, {"name": "Flows", "x-fc-bloque": "Funciones de Funnelchat", "tags": ["Flow", "Folder", "ContactAutomation"]}, {"name": "Campañas de grupos", "x-fc-bloque": "Funciones de Funnelchat", "tags": ["Campañas", "Grupos de una campaña y operaciones", "Mensajes programados", "Respuestas y carpetas", "Estadísticas"]}, {"name": "Difusiones", "x-fc-bloque": "Funciones de Funnelchat", "tags": ["Broadcast"]}, {"name": "Embudo y ventas", "x-fc-bloque": "Funciones de Funnelchat", "tags": ["KanbanSetting", "KanbanStage", "LeadScoreSettings", "LeadScoreDistribution", "LeadScoreEventIngest", "ContactLeadScore", "SalesDashboard", "PaymentProduct"]}, {"name": "Atención y equipo", "x-fc-bloque": "Funciones de Funnelchat", "tags": ["QuickReply", "Ticket", "User"]}, {"name": "Alertas", "x-fc-bloque": "Funciones de Funnelchat", "tags": ["AlertCatalog", "AlertDestination", "AlertOtp", "AlertSetting", "AlertQuietHour", "AlertDailyReport", "AlertCampaign", "AlertWebhookTest"]}, {"name": "Cuenta y consumo", "tags": ["Account", "CustomDomain", "CapiConnection", "Mac", "WhatsappCost"]}, {"name": "Webhooks", "tags": ["WebhookSubscription", "WebhookSubscriptionDelivery", "WebhookSubscriptionTest", "WebhookEventCatalog"]}, {"name": "Webhooks (legado)", "tags": ["WebhookEndpoint", "WebhookEndpointEvent", "WebhookDelivery", "Webhooks de campaña (legado)"]}, {"name": "Otros", "tags": ["Webhooks"]}], "paths": {"/api/v1/broadcasts": {"get": {"operationId": "broadcast.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar difusiones", "tags": ["Broadcast"], "parameters": [{"name": "name", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "device_id", "in": "query", "schema": {"type": ["integer", "null"]}}, {"name": "status[]", "in": "query", "schema": {"type": "array", "enum": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], "items": {"type": "string", "enum": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]}}}, {"name": "folder_id", "in": "query", "schema": {"type": ["integer", "null"]}}, {"name": "unclassified", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "send_type", "in": "query", "schema": {"type": ["string", "null"], "enum": ["immediate", "scheduled", null]}}, {"name": "device_type", "in": "query", "schema": {"type": ["string", "null"], "enum": ["business", "api", null]}}, {"name": "send_at_from", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "send_at_to", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["name", "created_at", "updated_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/BroadcastsResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para armar un panel o reporte de tus difusiones: filtrá por número, estado, carpeta, tipo de envío o fecha programada y ubicá rápido la que te interesa.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/contacts": {"post": {"operationId": "broadcast.contacts", "description": "**Permiso requerido:** `conversations:write`", "summary": "Buscar contactos para una difusión", "tags": ["Broadcast"], "parameters": [{"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"tags": {"type": "array", "items": {"type": "string"}}, "some_tags": {"type": "array", "items": {"type": "string"}}, "exclude_tags": {"type": "array", "items": {"type": "string"}}, "countries": {"type": "array", "items": {"type": "string"}}, "date_from": {"type": ["string", "null"], "format": "date-time"}, "date_to": {"type": ["string", "null"], "format": "date-time"}, "only_with_phone": {"type": "boolean"}, "only_paused": {"type": "boolean"}, "kanban_setting_id": {"type": ["integer", "null"]}, "kanban_stage_ids": {"type": ["array", "null"], "items": {"type": "integer"}}, "only_stagnant": {"type": ["boolean", "null"]}, "exclude_never_replied": {"type": "boolean"}, "lead_score_categories": {"type": ["array", "null"], "items": {"type": "string", "enum": ["insufficient_data", "hot", "warm", "cold"]}}, "lead_score_min": {"type": ["integer", "null"], "minimum": 0, "maximum": 100}, "lead_score_max": {"type": ["integer", "null"], "minimum": 0, "maximum": 100}, "per_page": {"type": ["integer", "null"], "minimum": 1}}}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/BroadcastContactResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para previsualizar la audiencia de una difusión antes de crearla: le pasás los filtros (etiquetas, países, etapa del kanban, lead score) y te devuelve, paginados, los contactos que entrarían.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/contacts-count": {"post": {"operationId": "broadcast.contactsCount", "description": "**Permiso requerido:** `conversations:write`", "summary": "Contar contactos para una difusión", "tags": ["Broadcast"], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"tags": {"type": "array", "items": {"type": "string"}}, "some_tags": {"type": "array", "items": {"type": "string"}}, "exclude_tags": {"type": "array", "items": {"type": "string"}}, "countries": {"type": "array", "items": {"type": "string"}}, "date_from": {"type": ["string", "null"], "format": "date-time"}, "date_to": {"type": ["string", "null"], "format": "date-time"}, "only_with_phone": {"type": "boolean"}, "only_paused": {"type": "boolean"}, "kanban_setting_id": {"type": ["integer", "null"]}, "kanban_stage_ids": {"type": ["array", "null"], "items": {"type": "integer"}}, "only_stagnant": {"type": ["boolean", "null"]}, "exclude_never_replied": {"type": "boolean"}, "lead_score_categories": {"type": ["array", "null"], "items": {"type": "string", "enum": ["insufficient_data", "hot", "warm", "cold"]}}, "lead_score_min": {"type": ["integer", "null"], "minimum": 0, "maximum": 100}, "lead_score_max": {"type": ["integer", "null"], "minimum": 0, "maximum": 100}}}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"contactsCount": {"type": "integer"}, "unreachableCount": {"type": "integer"}, "pausedCount": {"type": "integer"}, "optedOutCount": {"type": "integer"}}, "required": ["contactsCount", "unreachableCount", "pausedCount", "optedOutCount"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para dimensionar una difusión antes de lanzarla: con los mismos filtros de segmentación te dice cuántos contactos entran, cuántos no son alcanzables y cuántos están pausados.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/cost-tiers": {"get": {"operationId": "broadcast.costTiers", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver los tramos de costo", "tags": ["Broadcast"], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BroadcastCostTiersResource"}}}}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para consultar los tramos de costo vigentes y la fecha de la última actualización de tarifas, como referencia para estimar cuánto puede costar una difusión.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/move-to-folder": {"post": {"operationId": "broadcast.moveToFolder", "description": "**Permiso requerido:** `conversations:write`", "summary": "Mover difusiones a una carpeta", "tags": ["Broadcast"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MoveBroadcastsToFolderRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"moved": {"type": "integer", "minimum": 0}}, "required": ["moved"]}}}}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para ordenar difusiones en bloque: mové varias a una carpeta con una sola llamada, por ejemplo al agruparlas por campaña o por cliente.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/{broadcast}": {"get": {"operationId": "broadcast.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver una difusión", "tags": ["Broadcast"], "parameters": [{"name": "broadcast", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BroadcastResource"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "No tienes permiso para ver este broadcast."}}, "required": ["message"]}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para ver la configuración completa de una difusión: número, plantilla, carpeta y los filtros de audiencia que usó (etiquetas, países, kanban, lead score).", "x-fc-domain": "conversations"}, "delete": {"operationId": "broadcast.destroy", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar una difusión", "tags": ["Broadcast"], "parameters": [{"name": "broadcast", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "No Content"}, "422": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Las campañas que ya fueron enviadas o están saliendo no pueden eliminarse."}, "error_code": {"type": "string", "const": "broadcast_delete_not_allowed"}}, "required": ["message", "error_code"]}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "No tienes permiso para eliminar este broadcast."}}, "required": ["message"]}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para quitar de tu cuenta una difusión que ya no necesitás. Si lo que querés es frenar un envío, mirá la operación de cancelar.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/{broadcast}/statistics": {"get": {"operationId": "broadcast.statistics", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver las estadísticas de una difusión", "tags": ["Broadcast"], "parameters": [{"name": "broadcast", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"broadcast_id": {"type": "integer"}, "statistics": {"type": "object", "properties": {"delivered_messages_count": {"type": "integer"}, "readed_messages_count": {"type": "integer"}, "failed_messages_count": {"type": "integer"}, "pending_messages_count": {}, "replied_messages_count": {"type": "integer"}, "skipped_messages_count": {"type": "number"}, "skipped_by_reason": {"type": "object", "additionalProperties": {"type": "string"}}}, "required": ["delivered_messages_count", "readed_messages_count", "failed_messages_count", "pending_messages_count", "replied_messages_count", "skipped_messages_count", "skipped_by_reason"]}, "button_interactions": {"type": "array", "items": {"type": "object", "properties": {"button_index": {"type": "integer"}, "clicks_count": {"type": "integer"}}, "required": ["button_index", "clicks_count"]}}}, "required": ["broadcast_id", "statistics", "button_interactions"]}, {"type": "object", "properties": {"broadcast_id": {"type": "integer"}, "statistics": {"type": "object", "properties": {"delivered_messages_count": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "readed_messages_count": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "failed_messages_count": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "pending_messages_count": {}, "replied_messages_count": {"type": "integer"}, "skipped_messages_count": {"type": "number"}, "skipped_by_reason": {"type": "object", "additionalProperties": {"type": "string"}}}, "required": ["delivered_messages_count", "readed_messages_count", "failed_messages_count", "pending_messages_count", "replied_messages_count", "skipped_messages_count", "skipped_by_reason"]}, "button_interactions": {"type": "array", "items": {"type": "object", "properties": {"button_index": {"type": "integer"}, "clicks_count": {"type": "integer"}}, "required": ["button_index", "clicks_count"]}}}, "required": ["broadcast_id", "statistics", "button_interactions"]}]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para medir el resultado de una difusión: entregados, leídos, respondidos, fallidos, pendientes y omitidos (con su motivo), más los clics en cada botón.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/{broadcast}/cost": {"get": {"operationId": "broadcast.cost", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver el costo de una difusión", "tags": ["Broadcast"], "parameters": [{"name": "broadcast", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BroadcastCostResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para saber cuánto cuesta una difusión: la estimación previa y el costo calculado de lo enviado, desglosado por mercado, con aviso de si el dato todavía es parcial.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/{broadcast}/contacts": {"get": {"operationId": "broadcast.broadcastContacts", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los contactos de una difusión", "tags": ["Broadcast"], "parameters": [{"name": "broadcast", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para obtener la lista de destinatarios de una difusión, por ejemplo para cruzarla con tu CRM o auditar a quién se le envió.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/{broadcast}/cancel": {"post": {"operationId": "broadcast.cancel", "description": "**Permiso requerido:** `conversations:write`", "summary": "Cancelar una difusión", "tags": ["Broadcast"], "parameters": [{"name": "broadcast", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Broadcast cancelled."}}, "required": ["message"]}}}}, "409": {"description": "Conflict", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Ese envío ya fue reanudado o cancelado."}, "error_code": {"type": "string", "const": "smart_send_already_transitioned"}}, "required": ["message", "error_code"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para frenar una difusión programada o en curso antes de que siga enviando. Si ya no se puede cancelar, responde `409`.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/{broadcast}/messages": {"get": {"operationId": "broadcast.broadcastMessages", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los mensajes de una difusión", "tags": ["Broadcast"], "parameters": [{"name": "broadcast", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "status[]", "in": "query", "required": true, "schema": {"type": "array", "enum": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], "items": {"type": "string", "enum": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]}}}, {"name": "tab", "in": "query", "schema": {"type": ["string", "null"], "enum": ["all", "delivered", "read", "replied", "failed", "skipped", null]}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/BroadcastMessageResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para ver el detalle por destinatario de una difusión: estado de cada mensaje, motivo de omisión y fecha de envío. Ideal para detectar fallidos o ver quién leyó o respondió.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/folders": {"get": {"operationId": "broadcastFolder.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar carpetas de difusiones", "tags": ["Broadcast"], "parameters": [{"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/BroadcastFolderResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para obtener las carpetas de difusiones con cuántas difusiones tiene cada una, por ejemplo para replicar esa organización en tu sistema.", "x-fc-domain": "conversations"}, "post": {"operationId": "broadcastFolder.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear una carpeta de difusiones", "tags": ["Broadcast"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreBroadcastFolderRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BroadcastFolderResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para crear una carpeta y agrupar difusiones por campaña, cliente o el criterio que uses.", "x-fc-domain": "conversations"}}, "/api/v1/broadcasts/folders/{broadcastFolder}": {"put": {"operationId": "broadcastFolder.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar una carpeta de difusiones", "tags": ["Broadcast"], "parameters": [{"name": "broadcastFolder", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateBroadcastFolderRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BroadcastFolderResource"}}}}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "You do not have permission to edit this folder."}}, "required": ["message"]}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para renombrar una carpeta de difusiones.", "x-fc-domain": "conversations"}, "delete": {"operationId": "broadcastFolder.destroy", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar una carpeta de difusiones", "tags": ["Broadcast"], "parameters": [{"name": "broadcastFolder", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "No Content"}, "403": {"description": "Forbidden", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "You do not have permission to delete this folder."}}, "required": ["message"]}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para eliminar una carpeta de difusiones que ya no usás.", "x-fc-domain": "conversations"}}, "/api/v1/contacts": {"get": {"operationId": "contact.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar contactos", "tags": ["Contact"], "parameters": [{"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "email", "in": "query", "schema": {"type": "string"}}, {"name": "phone", "in": "query", "schema": {"type": "string"}}, {"name": "whatsapp_id", "in": "query", "schema": {"type": "string"}}, {"name": "country_codes[]", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}}, {"name": "status", "in": "query", "schema": {"type": "string"}}, {"name": "image", "in": "query", "schema": {"type": "string"}}, {"name": "created_at", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "created_from", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "created_to", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "tag_ids[]", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}, {"name": "tag_mode", "in": "query", "schema": {"type": "string", "enum": ["all", "any", "exclude"]}}, {"name": "custom_field_ids[]", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}, {"name": "custom_field_mode", "in": "query", "schema": {"type": "string", "enum": ["all", "any", "exclude"]}}, {"name": "custom_field_value", "in": "query", "schema": {"type": "string"}}, {"name": "custom_field_date_from", "in": "query", "schema": {"type": "string", "format": "date"}}, {"name": "custom_field_date_to", "in": "query", "schema": {"type": "string", "format": "date"}}, {"name": "custom_field_numeric_operator", "in": "query", "schema": {"type": "string", "enum": ["lt", "eq", "gt"]}}, {"name": "custom_field_numeric_value", "in": "query", "schema": {"type": "number"}}, {"name": "only_with_phone", "in": "query", "schema": {"type": "string"}}, {"name": "only_with_mac", "in": "query", "schema": {"type": "string"}}, {"name": "scopeToAdvisor", "in": "query", "schema": {"type": "string"}}, {"name": "lead_score_categories[]", "in": "query", "schema": {"type": "array", "enum": ["insufficient_data", "hot", "warm", "cold"], "items": {"type": "string", "enum": ["insufficient_data", "hot", "warm", "cold"]}}}, {"name": "lead_score_min", "in": "query", "schema": {"type": "integer", "minimum": 0, "maximum": 100}}, {"name": "lead_score_max", "in": "query", "schema": {"type": "integer", "minimum": 0, "maximum": 100}}, {"name": "lead_score_disqualified", "in": "query", "schema": {"type": ["string", "null"], "enum": ["include", "exclude", "only", null]}}, {"name": "tag_filters", "in": "query", "schema": {"type": "array", "items": {"type": "object", "properties": {"mode": {"type": "string", "enum": ["all", "any", "exclude"]}, "tag_ids": {"type": "array", "items": {"type": "integer"}}}, "required": ["mode"]}}, "x-deepObject-style": "qs"}, {"name": "custom_field_filters", "in": "query", "schema": {"type": "array", "items": {"type": "object", "properties": {"type": {"type": "integer", "enum": ["0", "1", "2", "3", "4"]}, "mode": {"type": ["string", "null"], "enum": ["all", "any", "exclude", null]}, "value": {"type": ["string", "null"]}, "date_from": {"type": ["string", "null"], "format": "date"}, "date_to": {"type": ["string", "null"], "format": "date"}, "numeric_operator": {"type": ["string", "null"], "enum": ["lt", "eq", "gt", null]}, "numeric_value": {"type": ["number", "null"]}, "date_operator": {"type": ["string", "null"], "enum": ["lt", "gt", "eq", "period", null]}, "custom_field_ids": {"type": "array", "items": {"type": "integer"}}}, "required": ["type"]}}, "x-deepObject-style": "qs"}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["id", "name", "email", "phone", "status", "favorite", "unread_messages", "created_at", "updated_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ContactResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para buscar y segmentar contactos desde tu sistema: filtrá por nombre, teléfono, email, etiquetas, campos personalizados, país, fecha de alta o lead score. Es la base para sincronizar o exportar tu CRM.", "x-fc-domain": "conversations"}, "post": {"operationId": "contact.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear un contacto", "tags": ["Contact"], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreContactRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ContactResource"}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para dar de alta un contacto nuevo (teléfono o WhatsApp ID, nombre, email) cuando sabés que no existe. Si no estás seguro, usá la operación de crear o actualizar.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/update-or-create": {"post": {"operationId": "contact.updateOrCreate", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear o actualizar un contacto", "tags": ["Contact"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"phone": {"type": "string"}, "name": {"type": ["string", "null"]}, "email": {"type": ["string", "null"], "format": "email"}}, "required": ["phone"]}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ContactResource"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Contacto no encontrado."}}, "required": ["message"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para sincronizar contactos desde tu sistema sin duplicarlos: si el teléfono ya existe lo actualiza, si no lo crea. Es la opción segura para reintentos.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/attach-custom-field": {"post": {"operationId": "contact.attachCustomField", "description": "**Permiso requerido:** `conversations:write`", "summary": "Asignar un campo personalizado a contactos", "tags": ["Contact"], "requestBody": {"content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "properties": {"customFieldId": {"type": "integer"}, "contactId": {"type": "integer"}, "value": {"type": "string"}}, "required": ["customFieldId", "contactId", "value"]}}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "array", "items": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string", "const": "No autorizado: el campo personalizado no pertenece al usuario autenticado."}, "status": {"type": "integer", "const": 403}}, "required": ["message", "status"]}, {"type": "object", "properties": {"message": {"type": "string", "const": "No autorizado: el contacto no pertenece al usuario autenticado."}, "status": {"type": "integer", "const": 403}}, "required": ["message", "status"]}, {"type": "object", "properties": {"message": {"type": "string", "const": "Custom field value attached successfully."}, "status": {"type": "integer", "const": 201}}, "required": ["message", "status"]}]}}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para guardar datos propios de tu negocio en los contactos (plan, ciudad, fecha de compra): asigna valores de campos personalizados a uno o varios contactos en una sola llamada.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/detach-custom-field": {"post": {"operationId": "contact.detachCustomField", "description": "**Permiso requerido:** `conversations:write`", "summary": "Quitar un campo personalizado de contactos", "tags": ["Contact"], "requestBody": {"content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "properties": {"customFieldId": {"type": "integer"}, "contactId": {"type": "integer"}}, "required": ["customFieldId", "contactId"]}}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"messages": {"type": "array", "items": {"type": "string"}}}, "required": ["messages"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para borrar el valor de un campo personalizado en uno o varios contactos, por ejemplo cuando un dato quedó desactualizado en tu sistema.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}": {"get": {"operationId": "contact.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver un contacto", "tags": ["Contact"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"allOf": [{"$ref": "#/components/schemas/ContactResource"}, {"type": "object", "required": ["whatsapp_username", "custom_fields", "manually_unread", "name_mismatch"]}]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para traer la ficha completa de un contacto: datos, campos personalizados, estado de la conversación, mensajes sin leer y último mensaje.", "x-fc-domain": "conversations"}, "put": {"operationId": "contact.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar un contacto", "tags": ["Contact"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateContactRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para mantener al día la ficha de un contacto desde tu sistema: nombre, email, teléfono, notas, asesor asignado, favorito o si puede ejecutar flujos.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/tags": {"get": {"operationId": "contact.tags", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar las etiquetas de un contacto", "tags": ["Contact"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ContactTagResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para saber qué etiquetas tiene un contacto, por ejemplo para reflejar su segmento en tu CRM o decidir qué acción tomar.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/custom-fields": {"get": {"operationId": "contact.customFields", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los campos personalizados de un contacto", "tags": ["Contact"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ContactCustomFieldResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para leer los valores de los campos personalizados de un contacto, con búsqueda por nombre de campo.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/meta-window": {"get": {"operationId": "contact.metaWindow", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver la ventana de 24 horas de un contacto", "tags": ["Contact"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "device_id", "in": "query", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"is_open": {"type": "boolean"}, "meta_device_id": {"type": "null"}, "expires_at": {"type": "null"}}, "required": ["is_open", "meta_device_id", "expires_at"]}, {"type": "object", "properties": {"is_open": {"type": "boolean"}, "meta_device_id": {"type": "null"}, "expires_at": {"type": ["string", "null"]}}, "required": ["is_open", "meta_device_id", "expires_at"]}, {"type": "object", "properties": {"is_open": {"type": "boolean"}, "meta_device_id": {}, "expires_at": {"type": ["string", "null"]}}, "required": ["is_open", "meta_device_id", "expires_at"]}]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo antes de escribirle a un contacto para saber si su ventana de 24 horas de Meta está abierta en un número, y así decidir si podés mandar un mensaje libre o necesitás una plantilla.", "x-fc-domain": "conversations"}}, "/api/v1/devices/{device}/contacts": {"get": {"operationId": "contact.contactsByDevice", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los contactos de un número", "tags": ["Contact"], "parameters": [{"name": "device", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"allOf": [{"$ref": "#/components/schemas/ContactResource"}, {"type": "object", "required": ["whatsapp_username"]}]}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para listar los contactos asociados a uno de tus números conectados, útil si manejás varios números y querés separar la base por cada uno.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/delete": {"post": {"operationId": "contact.destroy", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar contactos", "tags": ["Contact"], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"all": {"type": "boolean"}, "contactIds": {"type": "array", "items": {"type": "integer"}}, "created_from": {"type": ["string", "null"], "format": "date-time"}, "created_to": {"type": ["string", "null"], "format": "date-time"}, "tag_mode": {"type": ["string", "null"], "enum": ["all", "any", "exclude", null]}, "tag_ids": {"type": ["array", "null"], "items": {"type": "integer"}}, "tag_filters": {"type": ["array", "null"], "items": {"type": "object", "properties": {"tag_ids": {"type": "array", "items": {"type": "integer"}, "minItems": 1}, "mode": {"type": "string", "enum": ["all", "any", "exclude"]}}, "required": ["tag_ids", "mode"]}}, "custom_field_mode": {"type": ["string", "null"], "enum": ["all", "any", "exclude", null]}, "custom_field_ids": {"type": ["array", "null"], "items": {"type": "integer"}}, "custom_field_value": {"type": ["string", "null"]}, "custom_field_date_from": {"type": ["string", "null"], "format": "date"}, "custom_field_date_to": {"type": ["string", "null"], "format": "date"}, "custom_field_numeric_operator": {"type": ["string", "null"], "enum": ["lt", "eq", "gt", null]}, "custom_field_numeric_value": {"type": ["number", "null"]}, "custom_field_filters": {"type": ["array", "null"], "items": {"type": "object", "properties": {"custom_field_ids": {"type": "array", "items": {"type": "integer"}, "minItems": 1}, "type": {"type": "integer", "enum": ["0", "1", "2", "3", "4"]}, "mode": {"type": ["string", "null"], "enum": ["all", "any", "exclude", null]}, "value": {"type": ["string", "null"]}, "date_from": {"type": ["string", "null"], "format": "date"}, "date_to": {"type": ["string", "null"], "format": "date"}, "numeric_operator": {"type": ["string", "null"], "enum": ["lt", "eq", "gt", null]}, "numeric_value": {"type": ["number", "null"]}, "date_operator": {"type": ["string", "null"], "enum": ["lt", "gt", "eq", "period", null]}}, "required": ["custom_field_ids", "type"]}}, "country_codes": {"type": "array", "items": {"type": "string"}}}}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para borrar contactos en bloque: por IDs, por filtros (etiquetas, campos personalizados, país, fecha de alta) o todos. Ideal para limpiezas masivas de la base.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/pause": {"post": {"operationId": "contactAutomation.pause", "description": "**Permiso requerido:** `conversations:write`", "summary": "Pausar un contacto", "tags": ["ContactAutomation"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ContactResource"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para pausar a un contacto en las automatizaciones de la cuenta; las conversaciones y las difusiones permiten filtrar a los contactos pausados.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/flows": {"get": {"operationId": "contactAutomation.flows", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los flujos de un contacto", "tags": ["ContactAutomation"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ContactFlowListResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para saber en qué flujos de automatización está un contacto y si alguno sigue corriendo.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/message/contact-card": {"post": {"operationId": "contactCard.store", "description": "**Permiso requerido:** `conversations:send`", "summary": "Enviar una tarjeta de contacto", "tags": ["ContactCard"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreContactCardRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"messages": {"type": "array", "items": {"$ref": "#/components/schemas/PublicMessageResource"}}}, "required": ["messages"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string"}}, "required": ["message"]}}}}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string"}, "errors": {"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}}, "required": ["message", "errors"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["window_closed", "antiblock_duplicate_content", "antiblock_unanswered_limit", "recipient_unsupported"]}, "message": {"type": "string"}}, "required": ["error", "message"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["device_unavailable", "not_sent"]}, "message": {"type": "string"}}, "required": ["error", "message"]}]}}}}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "enum": ["rate_limited"]}, "message": {"type": "string"}}, "required": ["error", "message"]}}}, "headers": {"Retry-After": {"required": true, "schema": {"type": "integer"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "enum": ["Server Error"]}}, "required": ["message"]}}}}}, "x-fc-permission": "conversations:send", "x-fc-proposito": "Sirve para compartirle a un contacto una o más tarjetas de contacto desde uno de tus números, por ejemplo para pasarle el contacto de un vendedor o de soporte.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/lead-score": {"get": {"operationId": "contactLeadScore.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver el lead score de un contacto", "tags": ["ContactLeadScore"], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ContactLeadScoreResource"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para saber qué tan caliente está un lead: puntaje, categoría, confianza, si está descalificado y el historial de señales que lo explican, paginado por cursor.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/notes": {"get": {"operationId": "contactNote.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar las notas de un contacto", "tags": ["ContactNote"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "search", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 255}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ContactNoteResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para leer las notas que el equipo dejó sobre un contacto, con autor y fecha, y buscar por texto.", "x-fc-domain": "conversations"}, "post": {"operationId": "contactNote.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Agregar una nota a un contacto", "tags": ["ContactNote"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreContactNoteRequest"}}}}, "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ContactNoteResource"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para dejar registro en la ficha del contacto de algo que pasó en tu sistema, como una llamada, una compra o un reclamo.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/notes/{note}": {"delete": {"operationId": "contactNote.destroy", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar una nota", "tags": ["ContactNote"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "note", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "No Content"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para borrar una nota de un contacto que quedó obsoleta o se cargó por error.", "x-fc-domain": "conversations"}}, "/api/v1/conversations": {"get": {"operationId": "conversation.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar conversaciones", "tags": ["Conversation"], "parameters": [{"name": "favorites", "in": "query", "schema": {"type": "boolean"}}, {"name": "ownContacts", "in": "query", "schema": {"type": "boolean"}}, {"name": "ownAndUnassigned", "in": "query", "schema": {"type": "boolean"}}, {"name": "perName", "in": "query", "schema": {"type": "string"}}, {"name": "deviceId", "in": "query", "schema": {"type": "integer"}}, {"name": "contactId", "in": "query", "schema": {"type": "integer"}}, {"name": "advisorId", "in": "query", "schema": {"type": "integer"}}, {"name": "advisorIds", "in": "query", "schema": {"type": "string"}}, {"name": "activeConversation", "in": "query", "schema": {"type": "boolean"}}, {"name": "tags", "in": "query", "schema": {"type": "string"}}, {"name": "kanbanSettingId", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "kanbanStageIds", "in": "query", "schema": {"type": "string"}}, {"name": "unreadMessages", "in": "query", "schema": {"type": "boolean"}}, {"name": "onlyWithMac", "in": "query", "schema": {"type": "boolean"}}, {"name": "cursor", "in": "query", "schema": {"type": "string"}}, {"name": "updated_since", "in": "query", "schema": {"type": "string"}}, {"name": "paused", "in": "query", "schema": {"type": "string", "enum": ["true", "false", "1", "0"]}}, {"name": "leadScoreCategories", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "leadScoreMin", "in": "query", "schema": {"type": "integer", "minimum": 0, "maximum": 100}}, {"name": "leadScoreMax", "in": "query", "schema": {"type": "integer", "minimum": 0, "maximum": 100}}, {"name": "leadScoreDisqualified", "in": "query", "schema": {"type": ["string", "null"], "enum": ["include", "exclude", "only", null]}}, {"name": "metaWindowOpen", "in": "query", "schema": {"type": "boolean"}}, {"name": "metaWindowDeviceIds", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "lastMessageDateFrom", "in": "query", "schema": {"type": ["string", "null"], "format": "date"}}, {"name": "lastMessageDateTo", "in": "query", "schema": {"type": ["string", "null"], "format": "date"}}, {"name": "timezone", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "customFieldFilters", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "const": "No se pudieron cargar las conversaciones"}}, "required": ["error"]}}}}, "200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object", "properties": {"is_mac_inactive": {"type": "boolean", "description": "`true` si el contacto es inactivo por el límite de contactos activos en el momento de la consulta: tu cuenta está en bloqueo (`state` = `enforcing` en `GET /mac/status`) y el contacto todavía no es un contacto activo del ciclo actual. Viene en `GET /contacts`, `GET /contacts/{contact}` y `GET /conversations`."}}, "required": ["is_mac_inactive"]}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"path": {"type": "string"}, "per_page": {"type": "integer"}, "next_cursor": {"type": ["string", "null"]}, "prev_cursor": {"type": ["string", "null"]}}, "required": ["path", "per_page", "next_cursor", "prev_cursor"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para construir tu propia bandeja de entrada o sincronizar conversaciones: filtrá por número, asesor, etiquetas, etapa del kanban, no leídas o lead score, y traé solo lo nuevo con `updated_since`.", "x-fc-domain": "conversations"}}, "/api/v1/conversations/count/unread": {"get": {"operationId": "conversation.countUnread", "description": "**Permiso requerido:** `conversations:read`", "summary": "Contar conversaciones sin leer", "tags": ["Conversation"], "parameters": [{"name": "advisorIds", "in": "query", "schema": {"type": "string"}}, {"name": "favorites", "in": "query", "schema": {"type": "boolean"}}, {"name": "ownAndUnassigned", "in": "query", "schema": {"type": "boolean"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"count": {"type": "integer", "minimum": 0}}, "required": ["count"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para mostrar un contador de conversaciones sin leer, por ejemplo en un badge o tablero, filtrable por asesores o favoritas.", "x-fc-domain": "conversations"}}, "/api/v1/conversations/count/open": {"get": {"operationId": "conversation.countOpen", "description": "**Permiso requerido:** `conversations:read`", "summary": "Contar conversaciones abiertas", "tags": ["Conversation"], "parameters": [{"name": "advisorIds", "in": "query", "schema": {"type": "string"}}, {"name": "favorites", "in": "query", "schema": {"type": "boolean"}}, {"name": "ownAndUnassigned", "in": "query", "schema": {"type": "boolean"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"count": {"type": "integer", "minimum": 0}}, "required": ["count"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para medir la carga de trabajo actual: cuántas conversaciones abiertas hay, en total o por asesor.", "x-fc-domain": "conversations"}}, "/api/v1/conversations/count/closed": {"get": {"operationId": "conversation.countClosed", "description": "**Permiso requerido:** `conversations:read`", "summary": "Contar conversaciones cerradas", "tags": ["Conversation"], "parameters": [{"name": "advisorIds", "in": "query", "schema": {"type": "string"}}, {"name": "favorites", "in": "query", "schema": {"type": "boolean"}}, {"name": "ownAndUnassigned", "in": "query", "schema": {"type": "boolean"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"count": {"type": "integer", "minimum": 0}}, "required": ["count"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para contar las conversaciones cerradas, por ejemplo para métricas de atención por asesor.", "x-fc-domain": "conversations"}}, "/api/v1/conversations/count/all": {"get": {"operationId": "conversation.countAll", "description": "**Permiso requerido:** `conversations:read`", "summary": "Contar todas las conversaciones", "tags": ["Conversation"], "parameters": [{"name": "advisorIds", "in": "query", "schema": {"type": "string"}}, {"name": "favorites", "in": "query", "schema": {"type": "boolean"}}, {"name": "ownAndUnassigned", "in": "query", "schema": {"type": "boolean"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"count": {"type": "integer", "minimum": 0}}, "required": ["count"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para obtener el total de conversaciones, como base para calcular la proporción de abiertas, cerradas o sin leer.", "x-fc-domain": "conversations"}}, "/api/v1/conversations/mark-all-read": {"post": {"operationId": "conversation.markAllAsRead", "description": "**Permiso requerido:** `conversations:write`", "summary": "Marcar todas como leídas", "tags": ["Conversation"], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"advisorIds": {"type": "string"}, "favorites": {"type": "boolean"}, "ownAndUnassigned": {"type": "boolean"}}}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Todas las conversaciones han sido marcadas como leídas"}, "updated_count": {"type": "integer"}}, "required": ["message", "updated_count"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para poner en cero los no leídos de una vez, por ejemplo cuando ya atendiste por otro canal; se puede acotar por asesores o favoritas.", "x-fc-domain": "conversations"}}, "/api/v1/conversations/mark-all-unread": {"post": {"operationId": "conversation.markAllAsUnread", "description": "**Permiso requerido:** `conversations:write`", "summary": "Marcar todas como no leídas", "tags": ["Conversation"], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"advisorIds": {"type": "string"}, "favorites": {"type": "boolean"}, "ownAndUnassigned": {"type": "boolean"}}}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Todas las conversaciones elegibles fueron marcadas como no leídas"}, "marked_count": {"type": "integer"}, "limit_reached": {"type": "boolean"}, "contact_ids": {"type": "object", "additionalProperties": {}}}, "required": ["message", "marked_count", "limit_reached", "contact_ids"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para volver a marcar como no leídas las conversaciones de un conjunto (por asesores o favoritas) y que el equipo las retome. Tiene un tope y la respuesta avisa si se alcanzó.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/update": {"put": {"operationId": "conversation.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar la conversación de un contacto", "tags": ["Conversation"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"favorite": {"type": "boolean"}, "active_conversation": {"type": "boolean"}, "unread_messages": {"type": "integer"}, "adviser_id": {"type": ["integer", "null"]}, "device_id": {"type": "integer"}, "text": {"type": "string"}, "notes": {"type": ["string", "null"]}, "custom_fields": {"type": "array", "items": {"type": "string"}}, "manually_unread": {"type": "boolean"}}}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para gestionar la conversación con un contacto: asignarla a un asesor, abrirla o cerrarla, marcarla como favorita o no leída, o cambiar el número que la atiende.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/message": {"post": {"operationId": "conversation.sendMessage", "description": "**Permiso requerido:** `conversations:send`", "summary": "Enviar un mensaje a un contacto", "tags": ["Conversation"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SendMessageRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"anyOf": [{"$ref": "#/components/schemas/PublicMessageResource"}, {"type": "null"}]}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string"}, "errors": {"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}}, "required": ["message", "errors"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["window_closed", "antiblock_duplicate_content", "antiblock_unanswered_limit", "recipient_mismatch", "recipient_unsupported"]}, "message": {"type": "string"}}, "required": ["error", "message"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["device_unavailable", "not_sent"]}, "message": {"type": "string"}}, "required": ["error", "message"]}]}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "enum": ["rate_limited"]}, "message": {"type": "string"}}, "required": ["error", "message"]}}}, "headers": {"Retry-After": {"required": true, "schema": {"type": "integer"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "enum": ["Server Error"]}}, "required": ["message"]}}}}}, "x-fc-permission": "conversations:send", "x-fc-proposito": "Usalo para enviarle a un contacto un mensaje de texto o un archivo desde uno de tus números, con opción de responder a un mensaje previo, sumar opciones o vista previa de link.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contactId}/messages": {"get": {"operationId": "conversation.getLatestMessages", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los mensajes de un contacto", "tags": ["Conversation"], "parameters": [{"name": "contactId", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "cursor", "in": "query", "schema": {"type": "string"}}, {"name": "updated_since", "in": "query", "schema": {"type": "string"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ConversationsMessageResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"path": {"type": "string"}, "per_page": {"type": "integer"}, "next_cursor": {"type": ["string", "null"]}, "prev_cursor": {"type": ["string", "null"]}}, "required": ["path", "per_page", "next_cursor", "prev_cursor"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para traer el historial de mensajes de un contacto y mantenerlo sincronizado en tu sistema, con paginación por cursor y `updated_since` para pedir solo lo nuevo.", "x-fc-domain": "conversations"}}, "/api/v1/custom-fields": {"get": {"operationId": "customField.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar campos personalizados", "tags": ["CustomField"], "parameters": [{"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "description", "in": "query", "schema": {"type": "string"}}, {"name": "type", "in": "query", "schema": {"type": "integer"}}, {"name": "withContactCount", "in": "query", "schema": {"type": "boolean"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["name", "created_at", "updated_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/CustomFieldResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para conocer los campos personalizados de la cuenta y sus IDs antes de leer o escribir esos datos en tus contactos.", "x-fc-domain": "conversations"}, "post": {"operationId": "customField.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear un campo personalizado", "tags": ["CustomField"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 254}, "description": {"type": ["string", "null"], "maxLength": 1000}, "type": {"type": "integer"}}, "required": ["name", "type"]}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CustomFieldResource"}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para crear un campo personalizado nuevo (por ejemplo «plan» o «fecha de compra») y guardar en Funnelchat datos propios de tu negocio.", "x-fc-domain": "conversations"}}, "/api/v1/custom-fields/delete": {"post": {"operationId": "customField.destroy", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar campos personalizados", "tags": ["CustomField"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"customFieldIds": {"type": "array", "items": {"type": "string"}, "minItems": 1}}, "required": ["customFieldIds"]}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para eliminar en bloque los campos personalizados que ya no usás.", "x-fc-domain": "conversations"}}, "/api/v1/custom-fields/{customField}": {"get": {"operationId": "customField.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver un campo personalizado", "tags": ["CustomField"], "parameters": [{"name": "customField", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CustomFieldResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para ver la definición de un campo personalizado: nombre, tipo, descripción y cuántos contactos lo tienen.", "x-fc-domain": "conversations"}, "put": {"operationId": "customField.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar un campo personalizado", "tags": ["CustomField"], "parameters": [{"name": "customField", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 254}, "description": {"type": ["string", "null"], "maxLength": 1000}}}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/CustomFieldResource"}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para renombrar un campo personalizado o actualizar su descripción; el tipo no se cambia por acá.", "x-fc-domain": "conversations"}}, "/api/v1/contact-flows/{contactId}": {"get": {"operationId": "flow.contactFlowHistory", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los flujos en curso de un contacto", "tags": ["Flow"], "parameters": [{"name": "contactId", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "search", "in": "query", "schema": {"type": "string", "maxLength": 255}}, {"name": "status_group", "in": "query", "schema": {"type": "string", "enum": ["en_curso", "completado", "invalidado", "cancelado"]}}, {"name": "date_from", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "date_to", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para ver el historial de ejecuciones de flujos de un contacto, filtrable por estado (en curso, completado, invalidado, cancelado) y fechas. Útil para diagnosticar por qué recibió o no una automatización.", "x-fc-domain": "conversations"}}, "/api/v1/contact-flows/{contactFlowId}/cancel": {"put": {"operationId": "flow.cancelContactFlow", "description": "**Permiso requerido:** `conversations:write`", "summary": "Cancelar un flujo en curso", "tags": ["Flow"], "parameters": [{"name": "contactFlowId", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"contact_flow": {"type": "object", "properties": {"id": {"type": "integer"}, "status": {"type": "integer"}}, "required": ["id", "status"]}}, "required": ["contact_flow"]}}}}, "422": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "No se puede cancelar esta automatización. Puede que ya haya finalizado o no exista."}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para frenar un flujo que está corriendo para un contacto, por ejemplo cuando tu sistema detecta que ya compró o pidió no recibir más mensajes.", "x-fc-domain": "conversations"}}, "/api/v1/flows": {"get": {"operationId": "flow.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar flujos", "tags": ["Flow"], "parameters": [{"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "folder_id", "in": "query", "schema": {"type": "integer"}}, {"name": "unclassified", "in": "query", "schema": {"type": "string"}}, {"name": "trigger_type", "in": "query", "schema": {"type": "string", "enum": ["1", "2", "3", "4", "5", "6", "7", "inbound_automation"]}}, {"name": "status", "in": "query", "schema": {"type": "string", "enum": ["active", "inactive"]}}, {"name": "date_from", "in": "query", "schema": {"type": "string", "format": "date"}}, {"name": "date_to", "in": "query", "schema": {"type": "string", "format": "date"}}, {"name": "date_field", "in": "query", "schema": {"type": "string", "enum": ["created_at", "updated_at"]}}, {"name": "device_id", "in": "query", "schema": {"type": "string"}}, {"name": "device_ids[]", "in": "query", "schema": {"type": "array", "items": {"type": "integer", "minimum": 1}}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["name", "created_at", "updated_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/FlowResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para listar tus flujos de automatización con filtros por carpeta, disparador, estado, número o fecha, por ejemplo para mostrarlos en tu producto o auditarlos.", "x-fc-domain": "conversations"}}, "/api/v1/flows/{flow}": {"get": {"operationId": "flow.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver un flujo", "tags": ["Flow"], "parameters": [{"name": "flow", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"allOf": [{"$ref": "#/components/schemas/FlowResource"}, {"type": "object", "required": ["folder"]}]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para ver la configuración de un flujo: si está activo y es válido, cuántas veces se ejecutó y qué lo dispara (tipo, valores y números).", "x-fc-domain": "conversations"}}, "/api/v1/flows/{flow}/nodes": {"get": {"operationId": "flow.nodes", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los nodos de un flujo", "tags": ["Flow"], "parameters": [{"name": "flow", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/NodeResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para leer los nodos de un flujo y cómo se encadenan, por ejemplo para documentar o visualizar la automatización fuera de Funnelchat.", "x-fc-domain": "conversations"}}, "/api/v1/flows/{flow}/active-contacts": {"get": {"operationId": "flow.activeContacts", "description": "**Permiso requerido:** `conversations:read`", "summary": "Contar los contactos activos de un flujo", "tags": ["Flow"], "parameters": [{"name": "flow", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "payment_event_type", "in": "query", "schema": {"type": "string", "enum": ["approved", "pending", "rejected", "cancelled", "refunded", "chargeback"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"active_contacts_count": {"type": "integer", "minimum": 0}}, "required": ["active_contacts_count"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para saber cuántos contactos están ahora dentro de un flujo, por ejemplo antes de editarlo o desactivarlo.", "x-fc-domain": "conversations"}}, "/api/v1/flows/{flow}/active-contacts-list": {"get": {"operationId": "flow.activeContactsList", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los contactos activos de un flujo", "tags": ["Flow"], "parameters": [{"name": "flow", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "search", "in": "query", "schema": {"type": "string"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para saber quiénes están ahora dentro de un flujo, con búsqueda, por ejemplo para revisar casos o cancelar ejecuciones puntuales.", "x-fc-domain": "conversations"}}, "/api/v1/flows/{flow}/flow-contacts": {"get": {"operationId": "flow.flowContactsList", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los contactos de un flujo", "tags": ["Flow"], "parameters": [{"name": "flow", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "search", "in": "query", "schema": {"type": "string", "maxLength": 255}}, {"name": "status_group", "in": "query", "schema": {"type": "string", "enum": ["en_curso", "completado", "invalidado", "cancelado"]}}, {"name": "date_from", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "date_to", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para ver todos los contactos que pasaron por un flujo, filtrando por estado (en curso, completado, invalidado, cancelado) y fechas. Ideal para medir su resultado.", "x-fc-domain": "conversations"}}, "/api/v1/folders": {"get": {"operationId": "folder.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar carpetas", "tags": ["Folder"], "parameters": [{"name": "name", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 255}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/FolderResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para listar las carpetas donde se organizan los flujos, con su jerarquía y cantidad de flujos, por ejemplo para filtrar flujos por carpeta.", "x-fc-domain": "conversations"}, "post": {"operationId": "folder.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear una carpeta", "tags": ["Folder"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreFolderRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/FolderResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para crear una carpeta (o una subcarpeta, con `parent_id`) y organizar tus flujos.", "x-fc-domain": "conversations"}}, "/api/v1/folders/{folder}": {"get": {"operationId": "folder.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver una carpeta", "tags": ["Folder"], "parameters": [{"name": "folder", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"allOf": [{"$ref": "#/components/schemas/FolderResource"}, {"type": "object", "required": ["children"]}]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para ver una carpeta de flujos con sus subcarpetas y cuántos flujos contiene.", "x-fc-domain": "conversations"}, "put": {"operationId": "folder.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar una carpeta", "tags": ["Folder"], "parameters": [{"name": "folder", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateFolderRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para renombrar una carpeta de flujos o moverla dentro de otra cambiando `parent_id`.", "x-fc-domain": "conversations"}, "delete": {"operationId": "folder.destroy", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar una carpeta", "tags": ["Folder"], "parameters": [{"name": "folder", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para eliminar una carpeta de flujos que ya no necesitás.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/messages/{message}/forward": {"post": {"operationId": "forwardMessage.store", "description": "**Permiso requerido:** `conversations:send`", "summary": "Reenviar un mensaje", "tags": ["ForwardMessage"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "message", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ForwardMessageRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "array", "items": {"type": "object", "properties": {"contact_id": {"type": "integer"}, "mode": {"type": "string", "enum": ["internal", "native"]}, "status": {"type": "string", "enum": ["queued", "blocked"]}, "reason": {"type": ["string", "null"]}, "message": {"anyOf": [{"$ref": "#/components/schemas/PublicMessageResource"}, {"type": "null"}]}}, "required": ["contact_id", "mode", "status", "reason", "message"]}}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string"}, "errors": {"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}}, "required": ["message", "errors"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["window_closed", "antiblock_duplicate_content", "antiblock_unanswered_limit", "recipient_unsupported"]}, "message": {"type": "string"}}, "required": ["error", "message"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["device_unavailable"]}, "message": {"type": "string"}}, "required": ["error", "message"]}]}}}}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "enum": ["rate_limited"]}, "message": {"type": "string"}}, "required": ["error", "message"]}}}, "headers": {"Retry-After": {"required": true, "schema": {"type": "integer"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "enum": ["Server Error"]}}, "required": ["message"]}}}}}, "x-fc-permission": "conversations:send", "x-fc-proposito": "Usalo para reenviar un mensaje de una conversación a otros contactos, eligiendo desde qué número sale cada envío; la respuesta indica el resultado por destino.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings": {"get": {"operationId": "kanbanSetting.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar tableros", "tags": ["KanbanSetting"], "parameters": [{"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/KanbanSettingResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para listar tus tableros kanban y obtener sus IDs, necesarios para mover contactos de etapa o filtrar conversaciones y difusiones por tablero.", "x-fc-domain": "conversations"}, "post": {"operationId": "kanbanSetting.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear un tablero", "tags": ["KanbanSetting"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreKanbanSettingRequest"}}}}, "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/KanbanSettingResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para crear un tablero kanban, basado en etapas o en etiquetas, por ejemplo para modelar tu embudo de ventas o de atención dentro de Funnelchat.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings/{kanbanSetting}": {"get": {"operationId": "kanbanSetting.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver un tablero", "tags": ["KanbanSetting"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"allOf": [{"$ref": "#/components/schemas/KanbanSettingResource"}, {"type": "object", "required": ["stages"]}]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para ver la configuración de un tablero: modo, etapas, días para considerar estancado a un contacto y los campos de valor y ganancia asociados.", "x-fc-domain": "conversations"}, "put": {"operationId": "kanbanSetting.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar un tablero", "tags": ["KanbanSetting"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateKanbanSettingRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/KanbanSettingResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para ajustar un tablero existente: nombre, estructura, días de estancamiento, vista, ícono o si se colorea por lead score.", "x-fc-domain": "conversations"}, "delete": {"operationId": "kanbanSetting.destroy", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar un tablero", "tags": ["KanbanSetting"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "No Content"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para eliminar un tablero kanban que ya no usás.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings/{kanbanSetting}/members": {"get": {"operationId": "kanbanSetting.members", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver los miembros de un tablero", "tags": ["KanbanSetting"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "contact_ids[]", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"member_contact_ids": {"type": "array", "items": {"type": "integer"}}}, "required": ["member_contact_ids"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para chequear en una sola llamada cuáles de un conjunto de contactos están en un tablero, por ejemplo antes de moverlos o agregarlos.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/kanban-stages": {"get": {"operationId": "kanbanStage.contactStages", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver las etapas de un contacto en los tableros", "tags": ["KanbanStage"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ContactKanbanStageBoardResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para saber en qué etapa está un contacto en cada tablero, útil para reflejar su avance en el embudo dentro de tu CRM.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings/{kanbanSetting}/move-contact": {"post": {"operationId": "kanbanStage.moveContact", "description": "**Permiso requerido:** `conversations:write`", "summary": "Mover un contacto de etapa", "tags": ["KanbanStage"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/MoveContactStageRequest"}}}}, "responses": {"200": {"description": "OK"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para mover un contacto a otra etapa del tablero cuando algo pasa en tu sistema, por ejemplo al recibir un pago o agendar una demo.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings/{kanbanSetting}/bulk-move": {"post": {"operationId": "kanbanStage.bulkMove", "description": "**Permiso requerido:** `conversations:write`", "summary": "Mover contactos de etapa en bloque", "tags": ["KanbanStage"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BulkMoveStageRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": ["object", "null"], "properties": {"excluded_inactive": {"type": "integer", "description": "Cantidad de contactos inactivos que no se movieron. El cuerpo es `null` cuando no se dejó afuera a ninguno.", "minimum": 1}}, "required": ["excluded_inactive"]}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para mover muchos contactos a una misma etapa de una sola vez, por ejemplo al cerrar una campaña o importar resultados.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings/{kanbanSetting}/remove-contact": {"post": {"operationId": "kanbanStage.removeContact", "description": "**Permiso requerido:** `conversations:write`", "summary": "Sacar un contacto del tablero", "tags": ["KanbanStage"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/RemoveContactStageRequest"}}}}, "responses": {"200": {"description": "OK"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para sacar a un contacto de un tablero cuando deja de estar en ese proceso, por ejemplo si se perdió o se descartó.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings/{kanbanSetting}/bulk-remove": {"post": {"operationId": "kanbanStage.bulkRemove", "description": "**Permiso requerido:** `conversations:write`", "summary": "Sacar contactos del tablero en bloque", "tags": ["KanbanStage"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/BulkRemoveStageRequest"}}}}, "responses": {"200": {"description": "OK"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para sacar varios contactos de un tablero en una sola llamada.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings/{kanbanSetting}/stages": {"post": {"operationId": "kanbanStage.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear una etapa", "tags": ["KanbanStage"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreKanbanStageRequest"}}}}, "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/KanbanStageResource"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para agregar una etapa nueva a un tablero, ubicándola después de una etapa existente.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings/{kanbanSetting}/stages/reorder": {"post": {"operationId": "kanbanStage.reorder", "description": "**Permiso requerido:** `conversations:write`", "summary": "Reordenar las etapas", "tags": ["KanbanStage"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ReorderKanbanStagesRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/KanbanStageResource"}}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para cambiar el orden de las etapas de un tablero enviando la lista de IDs en el orden que querés.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings/{kanbanSetting}/stages/{stage}": {"put": {"operationId": "kanbanStage.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar una etapa", "tags": ["KanbanStage"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "stage", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateKanbanStageRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/KanbanStageResource"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para renombrar una etapa de un tablero.", "x-fc-domain": "conversations"}, "delete": {"operationId": "kanbanStage.destroy", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar una etapa", "tags": ["KanbanStage"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "stage", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "reassign_to_stage_id", "in": "query", "schema": {"type": ["integer", "null"]}}], "responses": {"204": {"description": "No Content"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para eliminar una etapa de un tablero; con `reassign_to_stage_id` podés pasar sus contactos a otra etapa.", "x-fc-domain": "conversations"}}, "/api/v1/kanban-settings/{kanbanSetting}/stages/{stage}/contacts": {"get": {"operationId": "kanbanStage.contacts", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los contactos de una etapa", "tags": ["KanbanStage"], "parameters": [{"name": "kanbanSetting", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "stage", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "search", "in": "query", "schema": {"type": "string"}}, {"name": "messages", "in": "query", "schema": {"type": "string", "enum": ["all", "read", "unread"]}}, {"name": "dateFrom", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "dateTo", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "dateBasis", "in": "query", "schema": {"type": "string", "enum": ["board_entry", "created", "last_activity"]}}, {"name": "adviserId", "in": "query", "schema": {"type": "string"}}, {"name": "valueMin", "in": "query", "schema": {"type": "number"}}, {"name": "valueMax", "in": "query", "schema": {"type": "number"}}, {"name": "stagnant", "in": "query", "schema": {"type": "boolean"}}, {"name": "tagIds[]", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}, {"name": "leadScoreCategories[]", "in": "query", "schema": {"type": "array", "enum": ["insufficient_data", "hot", "warm", "cold"], "items": {"type": "string", "enum": ["insufficient_data", "hot", "warm", "cold"]}}}, {"name": "leadScoreMin", "in": "query", "schema": {"type": "integer", "minimum": 0, "maximum": 100}}, {"name": "leadScoreMax", "in": "query", "schema": {"type": "integer", "minimum": 0, "maximum": 100}}, {"name": "leadScoreDisqualified", "in": "query", "schema": {"type": ["string", "null"], "enum": ["include", "exclude", "only", null]}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["name", "last_message_time", "stage_since"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para traer los contactos de una etapa con filtros (asesor, valor, estancados, etiquetas, lead score, fechas), por ejemplo para armar seguimientos o reportes del embudo.", "x-fc-domain": "conversations"}}, "/api/v1/lead-score/distribution": {"get": {"operationId": "leadScoreDistribution.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver la distribución del lead score", "tags": ["LeadScoreDistribution"], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LeadScoreDistributionResource"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para ver cómo se reparte tu base entre leads calientes, tibios, fríos y sin datos suficientes, junto con los umbrales vigentes. Ideal para un tablero de calidad de leads.", "x-fc-domain": "conversations"}}, "/api/v1/lead-score/events": {"post": {"operationId": "leadScoreEventIngest.ingest", "description": "**Permiso requerido:** `conversations:write`", "summary": "Registrar un evento de lead score", "tags": ["LeadScoreEventIngest"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/IngestLeadScoreEventsRequest"}}}}, "responses": {"202": {"description": "Accepted", "content": {"application/json": {"schema": {"type": "object", "properties": {"success": {"type": "boolean"}}, "required": ["success"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para alimentar el lead score con señales de interacción (entradas y salidas de grupos, reacciones, votos en encuestas, clics en links) por contacto o teléfono. Se acepta para procesar después (`202`).", "x-fc-domain": "conversations"}}, "/api/v1/lead-score/settings": {"get": {"operationId": "leadScoreSettings.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver la configuración del lead score", "tags": ["LeadScoreSettings"], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LeadScoreSettingsResource"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para leer cómo se calcula el lead score de tu cuenta: umbrales de caliente y tibio, señales mínimas, histéresis, decaimiento y reglas.", "x-fc-domain": "conversations"}, "put": {"operationId": "leadScoreSettings.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar la configuración del lead score", "tags": ["LeadScoreSettings"], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LeadScoreSettingsResource"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para ajustar el cálculo del lead score a tu negocio: umbrales de categoría, señales mínimas, histéresis, decaimiento con el paso de los días y reglas.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/lead-score/adjustments": {"post": {"operationId": "leadScoreSettings.adjust", "description": "**Permiso requerido:** `conversations:write`", "summary": "Ajustar el lead score de un contacto", "tags": ["LeadScoreSettings"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreLeadScoreAdjustmentRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"score": {"type": "integer"}, "signals_count": {"type": "integer"}, "category": {"type": "string"}, "confidence": {"type": "string"}, "disqualified": {"type": "boolean"}, "disqualified_at": {"type": ["string", "null"]}}, "required": ["score", "signals_count", "category", "confidence", "disqualified", "disqualified_at"]}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para sumar o restar puntos al lead score de un contacto a mano, con un motivo, por ejemplo cuando tu equipo comercial confirma interés por otro canal.", "x-fc-domain": "conversations"}}, "/api/v1/mac/status": {"get": {"operationId": "mac.status", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver el consumo de MACs", "tags": ["Mac"], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"enforcement_enabled": {"type": "boolean"}, "state": {"type": "string", "description": "Estado del límite de contactos activos de tu cuenta: `normal`; `grace`, alcanzaste el límite y corren las 24 horas de gracia; `enforcing`, bloqueo: hay contactos inactivos; `cleared`, el último período de gracia o de bloqueo ya terminó.", "enum": ["normal", "grace", "enforcing", "cleared"]}, "inactive_contacts_count": {"type": "integer", "description": "Contactos registrados como inactivos en el ciclo actual: a los que se les frenó un mensaje, que te escribieron estando inactivos o que una difusión dejó afuera. No es el total de contactos que podrían serlo: para un contacto puntual, usa `is_mac_inactive`.", "minimum": 0}}, "required": ["enforcement_enabled", "state", "inactive_contacts_count"]}, {"type": "object", "properties": {"enforcement_enabled": {"type": "boolean"}, "current_count": {"type": "integer", "const": 0}, "limit": {"type": "integer", "const": 0}, "remaining": {"type": "integer", "const": 0}, "usage_percentage": {"type": "integer", "const": 0}, "state": {"type": "string", "const": "normal"}, "grace": {"type": "null"}, "period_start": {"type": "null"}}, "required": ["enforcement_enabled", "current_count", "limit", "remaining", "usage_percentage", "state", "grace", "period_start"]}]}}}}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para saber en qué estado está el límite de contactos activos de tu cuenta (normal, en las 24 horas de gracia o en bloqueo) y cuántos contactos quedaron inactivos en el ciclo, y anticiparte antes de que se frenen envíos.", "x-fc-domain": "conversations"}}, "/api/v1/mac/contacts": {"get": {"operationId": "mac.contacts", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los contactos que cuentan como MAC", "tags": ["Mac"], "parameters": [{"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para ver qué contactos están contando como MAC, útil para entender qué está consumiendo tu cupo.", "x-fc-domain": "conversations"}}, "/api/v1/mac/contacts/history": {"get": {"operationId": "mac.contactsHistory", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver el historial de MACs", "tags": ["Mac"], "parameters": [{"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para consultar el historial de MACs y analizar cómo evolucionó el consumo a lo largo del tiempo.", "x-fc-domain": "conversations"}}, "/api/v1/message/send-message": {"post": {"operationId": "message.sendMessage", "description": "**Permiso requerido:** `conversations:send`", "summary": "Enviar un mensaje", "tags": ["Message"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SendDirectMessageRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"anyOf": [{"$ref": "#/components/schemas/PublicMessageResource"}, {"type": "null"}]}}, "required": ["message"]}}}}, "422": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string"}, "errors": {"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}}, "required": ["message", "errors"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["window_closed", "antiblock_duplicate_content", "antiblock_unanswered_limit", "recipient_unsupported"]}, "message": {"type": "string"}}, "required": ["error", "message"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["device_unavailable", "not_sent"]}, "message": {"type": "string"}}, "required": ["error", "message"]}]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "enum": ["rate_limited"]}, "message": {"type": "string"}}, "required": ["error", "message"]}}}, "headers": {"Retry-After": {"required": true, "schema": {"type": "integer"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "enum": ["Server Error"]}}, "required": ["message"]}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"type": "object", "properties": {"status": {"type": "string", "enum": ["error"]}, "error": {"type": "string"}}, "required": ["status", "error"]}}}}}, "x-fc-permission": "conversations:send", "x-fc-proposito": "Sirve para enviar un mensaje de texto o un archivo a un contacto, indicando en el cuerpo el `contact_id` y el número desde el que sale.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/messages/{message}/reaction": {"put": {"operationId": "messageReaction.update", "description": "**Permiso requerido:** `conversations:send`", "summary": "Reaccionar a un mensaje", "tags": ["MessageReaction"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "message", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreMessageReactionRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string"}, "errors": {"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}}, "required": ["message", "errors"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["recipient_unsupported"]}, "message": {"type": "string"}}, "required": ["error", "message"]}]}}}}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "enum": ["rate_limited"]}, "message": {"type": "string"}}, "required": ["error", "message"]}}}, "headers": {"Retry-After": {"required": true, "schema": {"type": "integer"}}}}}, "x-fc-permission": "conversations:send", "x-fc-proposito": "Usalo para reaccionar con un emoji a un mensaje de una conversación, por ejemplo para acusar recibo sin escribir una respuesta.", "x-fc-domain": "conversations"}, "delete": {"operationId": "messageReaction.destroy", "description": "**Permiso requerido:** `conversations:send`", "summary": "Quitar una reacción", "tags": ["MessageReaction"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "message", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "enum": ["rate_limited"]}, "message": {"type": "string"}}, "required": ["error", "message"]}}}, "headers": {"Retry-After": {"required": true, "schema": {"type": "integer"}}}}, "422": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string"}, "errors": {"type": "object"}}, "required": ["message", "errors"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["recipient_unsupported"]}, "message": {"type": "string"}}, "required": ["error", "message"]}]}}}}}, "x-fc-permission": "conversations:send", "x-fc-proposito": "Sirve para quitar la reacción que pusiste en un mensaje.", "x-fc-domain": "conversations"}}, "/api/v1/templates": {"get": {"operationId": "messageTemplate.templates", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar plantillas", "tags": ["MessageTemplate"], "parameters": [{"name": "name", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "uid", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "language_code", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "status", "in": "query", "schema": {"type": ["string", "null"], "enum": ["PENDING", "APPROVED", "REJECTED", "PENDING_DELETION", "PAUSED", "DISABLED", "ARCHIVED", null]}}, {"name": "category", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "deviceId", "in": "query", "schema": {"type": ["integer", "null"]}}, {"name": "type", "in": "query", "schema": {"type": ["string", "null"], "enum": ["text", "image", "document", "video", "location", "default", null]}}, {"name": "created_by_webhook", "in": "query", "schema": {"type": "boolean"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["name", "created_at", "updated_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/TemplateResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para listar tus plantillas de WhatsApp con su estado de aprobación, categoría, idioma y número, y elegir cuál usar en un envío o una difusión.", "x-fc-domain": "conversations"}, "post": {"operationId": "messageTemplate.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear una plantilla", "tags": ["MessageTemplate"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreMessageTemplateRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TemplateResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para crear una plantilla de WhatsApp (encabezado, cuerpo con variables, pie y botones) desde tu sistema, sin pasar por el panel.", "x-fc-domain": "conversations"}}, "/api/v1/templates/{template}": {"put": {"operationId": "messageTemplate.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar una plantilla", "tags": ["MessageTemplate"], "parameters": [{"name": "template", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateMessageTemplateRequest"}}}}, "responses": {"204": {"description": "No Content"}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"error": {"type": "string", "const": "Approved templates can be edited up to 10 times in a 30-day period or once in a 24-hour period."}}, "required": ["error"]}, {"type": "object", "properties": {"error": {"type": "string", "const": "Templates with pending status cannot be updated."}}, "required": ["error"]}]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para editar el contenido de una plantilla existente: categoría, encabezado, cuerpo, variables, pie o botones.", "x-fc-domain": "conversations"}, "get": {"operationId": "messageTemplate.template", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver una plantilla", "tags": ["MessageTemplate"], "parameters": [{"name": "template", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TemplateResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para ver una plantilla completa, con su estado, variables y botones, por ejemplo para saber qué valores completar al enviarla.", "x-fc-domain": "conversations"}}, "/api/v1/templates/{template}/quick-reply-buttons": {"get": {"operationId": "messageTemplate.quickReplyButtons", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver los botones de respuesta rápida de una plantilla", "tags": ["MessageTemplate"], "parameters": [{"name": "template", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {}}}, "required": ["data"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para obtener los botones de respuesta rápida de una plantilla, por ejemplo para preparar qué hacer según el botón que toque el contacto.", "x-fc-domain": "conversations"}}, "/api/v1/devices/{device}/templates/{template}/send-template": {"post": {"operationId": "messageTemplate.send", "description": "**Permiso requerido:** `conversations:send`", "summary": "Enviar una plantilla", "tags": ["MessageTemplate"], "parameters": [{"name": "device", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "template", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"phone": {"type": "string"}, "contact_id": {"type": "integer"}, "template_variables": {"type": "object", "properties": {"body": {"type": "array", "items": {"type": ["string", "null"], "maxLength": 1024}, "maxItems": 100}}}, "client_reference": {"type": ["string", "null"], "pattern": "^[\\x21-\\x7E]{1,64}$", "maxLength": 64}}, "required": ["phone", "contact_id"]}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"anyOf": [{"$ref": "#/components/schemas/PublicMessageResource"}, {"type": "null"}]}}, "required": ["message"]}}}}, "422": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string"}, "errors": {"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}}, "required": ["message", "errors"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["antiblock_duplicate_content", "antiblock_unanswered_limit", "recipient_mismatch", "recipient_unsupported"]}, "message": {"type": "string"}}, "required": ["error", "message"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["device_unavailable", "not_sent"]}, "message": {"type": "string"}}, "required": ["error", "message"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["template_unresolved_placeholder"]}, "message": {"type": "string"}, "placeholders": {"type": "array", "items": {"type": "string"}}, "count": {"type": "integer"}}, "required": ["error", "message", "placeholders", "count"]}]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"error": {"type": "string", "enum": ["template_not_found"]}, "message": {"type": "string"}}, "required": ["error", "message"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["Contact not found"]}}, "required": ["error"]}, {"type": "object", "properties": {"message": {"type": "string"}}, "required": ["message"]}]}}}}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "enum": ["rate_limited"]}, "message": {"type": "string"}}, "required": ["error", "message"]}}}, "headers": {"Retry-After": {"required": true, "schema": {"type": "integer"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "enum": ["Server Error"]}}, "required": ["message"]}}}}}, "x-fc-permission": "conversations:send", "x-fc-proposito": "Sirve para enviar una plantilla desde uno de tus números a un teléfono o contacto, completando sus variables, por ejemplo un aviso, un recordatorio o una confirmación.", "x-fc-domain": "conversations"}}, "/api/v1/devices/{device}/templates/{template}": {"delete": {"operationId": "messageTemplate.delete", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar una plantilla de un número", "tags": ["MessageTemplate"], "parameters": [{"name": "device", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "template", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "No Content"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para eliminar una plantilla de uno de tus números cuando ya no la vas a usar.", "x-fc-domain": "conversations"}}, "/api/v1/payments/products/{platform}": {"get": {"operationId": "paymentProduct.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los productos de una plataforma de pagos", "tags": ["PaymentProduct"], "parameters": [{"name": "platform", "in": "path", "required": true, "schema": {"type": "string"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/PaymentProductResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para listar los productos de una plataforma de pagos con su ID externo y nombre, por ejemplo para relacionarlos con las ventas que reporta Funnelchat.", "x-fc-domain": "conversations"}}, "/api/v1/quick-replies": {"get": {"operationId": "quickReply.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar respuestas rápidas", "tags": ["QuickReply"], "parameters": [{"name": "search", "in": "query", "schema": {"type": "string"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["created_at", "updated_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/QuickReplyResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para listar las respuestas rápidas de la cuenta, por ejemplo para ofrecerlas en tu propia interfaz de atención.", "x-fc-domain": "conversations"}, "post": {"operationId": "quickReply.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear una respuesta rápida", "tags": ["QuickReply"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 30}, "message": {"type": ["string", "null"], "maxLength": 2500}, "file": {"type": ["string", "null"]}, "type": {"$ref": "#/components/schemas/MessageTypeEnum"}, "visibility": {"$ref": "#/components/schemas/QuickReplyVisibilityEnum"}, "agent_ids": {"type": ["array", "null"], "items": {"type": "integer"}}}, "required": ["name", "type", "visibility"]}}}}, "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuickReplyResource"}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para crear una respuesta rápida con texto o archivo y definir quién la ve (visibilidad y agentes), así el equipo responde más rápido lo frecuente.", "x-fc-domain": "conversations"}}, "/api/v1/quick-replies/{quickReply}": {"get": {"operationId": "quickReply.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver una respuesta rápida", "tags": ["QuickReply"], "parameters": [{"name": "quickReply", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuickReplyResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para ver una respuesta rápida con su contenido, su tipo y los agentes que la tienen disponible.", "x-fc-domain": "conversations"}, "put": {"operationId": "quickReply.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar una respuesta rápida", "tags": ["QuickReply"], "parameters": [{"name": "quickReply", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 30}, "message": {"type": ["string", "null"], "maxLength": 2500}, "file": {"type": ["string", "null"]}, "type": {"$ref": "#/components/schemas/MessageTypeEnum"}, "visibility": {"$ref": "#/components/schemas/QuickReplyVisibilityEnum"}, "agent_ids": {"type": ["array", "null"], "items": {"type": "integer"}}}, "required": ["name", "type", "visibility"]}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/QuickReplyResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para actualizar el contenido o la visibilidad de una respuesta rápida.", "x-fc-domain": "conversations"}, "delete": {"operationId": "quickReply.destroy", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar una respuesta rápida", "tags": ["QuickReply"], "parameters": [{"name": "quickReply", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "No Content"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para eliminar una respuesta rápida que ya no se usa.", "x-fc-domain": "conversations"}}, "/api/v1/sales/metrics": {"get": {"operationId": "salesDashboard.metrics", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver las métricas de ventas", "tags": ["SalesDashboard"], "parameters": [{"name": "window", "in": "query", "schema": {"type": "integer", "enum": ["7", "30", "90"]}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "per_page", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}, {"name": "event_type[]", "in": "query", "schema": {"type": "array", "items": {"$ref": "#/components/schemas/PaymentEventTypeEnum"}}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SalesMetricsResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para obtener métricas de ventas de los últimos 7, 30 o 90 días: totales por moneda, compradores distintos y tasa de recompra. Ideal para un tablero comercial.", "x-fc-domain": "conversations"}}, "/api/v1/sales/funnel": {"get": {"operationId": "salesDashboard.funnel", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver el embudo de ventas", "tags": ["SalesDashboard"], "parameters": [{"name": "window", "in": "query", "schema": {"type": "integer", "enum": ["7", "30", "90"]}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "per_page", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}, {"name": "event_type[]", "in": "query", "schema": {"type": "array", "items": {"$ref": "#/components/schemas/PaymentEventTypeEnum"}}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"conversations": {"type": "integer", "minimum": 0}, "qualified": {"type": ["integer", "null"], "minimum": 0}, "purchases": {"type": "object", "properties": {"buyers": {"type": "integer", "minimum": 0}, "amounts": {"type": "array", "items": {}}}, "required": ["buyers", "amounts"]}}, "required": ["conversations", "qualified", "purchases"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para ver el embudo de conversaciones a leads calificados y a compras en una ventana de 7, 30 o 90 días, y medir cuánto convierte tu atención por WhatsApp.", "x-fc-domain": "conversations"}}, "/api/v1/sales/by-platform": {"get": {"operationId": "salesDashboard.byPlatform", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver las ventas por plataforma", "tags": ["SalesDashboard"], "parameters": [{"name": "window", "in": "query", "schema": {"type": "integer", "enum": ["7", "30", "90"]}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "per_page", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}, {"name": "event_type[]", "in": "query", "schema": {"type": "array", "items": {"$ref": "#/components/schemas/PaymentEventTypeEnum"}}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"platforms": {"type": "array", "items": {"type": "object", "properties": {"platform": {"type": "string"}, "connected": {"type": "boolean"}, "totals": {"type": "array", "items": {"type": "object", "properties": {"currency": {"type": "string"}, "total_minor": {"type": "integer"}}, "required": ["currency", "total_minor"]}}}, "required": ["platform", "connected", "totals"]}}}, "required": ["platforms"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para comparar las ventas por plataforma de pagos en la ventana elegida y ver cuáles están conectadas.", "x-fc-domain": "conversations"}}, "/api/v1/sales": {"get": {"operationId": "salesDashboard.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar ventas", "tags": ["SalesDashboard"], "parameters": [{"name": "window", "in": "query", "schema": {"type": "integer", "enum": ["7", "30", "90"]}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "event_type[]", "in": "query", "schema": {"type": "array", "items": {"$ref": "#/components/schemas/PaymentEventTypeEnum"}}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/SaleResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para traer el detalle de ventas (producto, monto, plataforma, estado, comprador y atribución) de los últimos 7, 30 o 90 días, por ejemplo para conciliar con tu sistema.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/messages/{message}": {"get": {"operationId": "message.showMessage", "description": "**Permiso requerido:** `conversations:read`\n\nDevuelve un mensaje de tu cuenta por el id que te dio el envío o un webhook. Aceptado no es entregado: el estado final llega por `message.status_changed` o por este GET. `status` es `queued`, `sent`, `delivered`, `read` o `failed` en los mensajes que enviaste. `sent`, `delivered`, `read` y `failed` tienen el mismo significado que en `message.status_changed`; `queued` es un mensaje en cola que todavía no se envió (en cola no es enviado) y no se reintenta. `status` es `null` en los mensajes recibidos, las notas y las acciones. `status` también es `null` en un mensaje enviado cuyo resultado no se conoce (el proveedor no confirmó ni rechazó el envío o el envío se interrumpió): puede haber llegado y no cambia de estado; también cuando lleva más de una hora en cola sin resultado: un mensaje cuyo envío no empezó dentro de la hora ya no sale; pasada la hora sin resultado, `status` es `null`, o `failed` con `error.reason` `not_sent` y su aviso si su envío llega tarde. Reintentar es un envío nuevo, que sólo puede duplicar si el envío había empezado y se cortó. `error` trae `code`, `title` y `reason` solo cuando `status` es `failed`. `error.reason` es `null` cuando el rechazo lo informó el proveedor; si Funnelchat decidió que un mensaje enviado con clave no salía, trae `device_unavailable`, `insufficient_balance`, `window_closed`, `recipient_unreachable`, `template_not_found`, `template_unresolved_placeholder`, `not_queued` o `not_sent`, `code` es `null` y `title` es un texto llano en el idioma de la cuenta. `client_reference` es la referencia que mandaste al enviar, o `null`. Responde 404 si el mensaje no existe o no es de ese contacto.", "summary": "Ver un mensaje", "tags": ["ShowMessage"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "message", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/PublicMessageResource"}}}}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para saber qué pasó con un mensaje que enviaste: con el id que te devolvió el envío ves su estado (en cola, enviado, entregado, leído o fallido) y, si falló, el motivo.", "x-fc-domain": "conversations"}}, "/api/v1/tags": {"get": {"operationId": "tag.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar etiquetas", "tags": ["Tag"], "parameters": [{"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "withContactCount", "in": "query", "schema": {"type": "boolean"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["name", "created_at", "updated_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/TagResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para listar las etiquetas de la cuenta, opcionalmente con cuántos contactos tiene cada una, y obtener sus IDs para filtrar o segmentar.", "x-fc-domain": "conversations"}, "post": {"operationId": "tag.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear una etiqueta", "tags": ["Tag"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 254}, "description": {"type": ["string", "null"], "maxLength": 1024}, "color": {"type": ["string", "null"], "maxLength": 191}}, "required": ["name"]}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TagResource"}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para crear una etiqueta (nombre, descripción, color) y segmentar contactos según los criterios de tu negocio.", "x-fc-domain": "conversations"}}, "/api/v1/tags/{tag}": {"get": {"operationId": "tag.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver una etiqueta", "tags": ["Tag"], "parameters": [{"name": "tag", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "withContactCount", "in": "query", "schema": {"type": "boolean"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/TagResource"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para ver una etiqueta y, si lo pedís, cuántos contactos la tienen.", "x-fc-domain": "conversations"}, "put": {"operationId": "tag.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar una etiqueta", "tags": ["Tag"], "parameters": [{"name": "tag", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"name": {"type": "string"}, "description": {"type": ["string", "null"], "maxLength": 1024}, "color": {"type": ["string", "null"], "maxLength": 191}}, "required": ["name"]}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "400": {"description": "Bad Request", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "examples": ["Ya existe un tag con este nombre."]}}, "required": ["message"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para renombrar una etiqueta o cambiar su descripción o su color.", "x-fc-domain": "conversations"}}, "/api/v1/tags/{tag}/contacts": {"get": {"operationId": "tag.contacts", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar los contactos con una etiqueta", "tags": ["Tag"], "parameters": [{"name": "tag", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["name", "last_message_time"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}, {"name": "sortBy", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para obtener todos los contactos que tienen una etiqueta, por ejemplo para exportar un segmento o sincronizarlo con otra herramienta.", "x-fc-domain": "conversations"}}, "/api/v1/tags/delete": {"post": {"operationId": "tag.destroy", "description": "**Permiso requerido:** `conversations:write`", "summary": "Eliminar etiquetas", "tags": ["Tag"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"tagIds": {"type": "array", "items": {"type": "string"}, "minItems": 1}}, "required": ["tagIds"]}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object"}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para eliminar varias etiquetas de una vez.", "x-fc-domain": "conversations"}}, "/api/v1/tickets": {"get": {"operationId": "ticket.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar tickets", "tags": ["Ticket"], "parameters": [{"name": "status", "in": "query", "schema": {"type": "integer", "enum": ["0", "1", "2"]}}, {"name": "userId", "in": "query", "schema": {"type": "integer"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para listar los tickets de la cuenta filtrando por estado o por usuario asignado, por ejemplo para armar una vista de casos pendientes.", "x-fc-domain": "conversations"}, "post": {"operationId": "ticket.store", "description": "**Permiso requerido:** `conversations:write`", "summary": "Crear un ticket", "tags": ["Ticket"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"userId": {"type": "integer"}, "contactId": {"type": "integer"}, "title": {"type": "string"}, "description": {"type": "string"}}, "required": ["userId", "contactId", "title", "description"]}}}}, "responses": {"201": {"description": "Created", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Ticket"}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Contacto no encontrado."}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para abrir un ticket sobre un contacto (título y descripción) y asignárselo a un usuario del equipo, por ejemplo cuando tu sistema detecta un reclamo.", "x-fc-domain": "conversations"}}, "/api/v1/tickets/{id}": {"get": {"operationId": "ticket.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver un ticket", "tags": ["Ticket"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Ticket"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para ver un ticket con su contacto, su responsable, la descripción y la fecha límite.", "x-fc-domain": "conversations"}, "put": {"operationId": "ticket.update", "description": "**Permiso requerido:** `conversations:write`", "summary": "Actualizar un ticket", "tags": ["Ticket"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"title": {"type": "string"}, "description": {"type": "string"}}, "required": ["title", "description"]}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Ticket"}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para corregir o ampliar el título y la descripción de un ticket.", "x-fc-domain": "conversations"}}, "/api/v1/tickets/assign": {"post": {"operationId": "ticket.assignTicketToUser", "description": "**Permiso requerido:** `conversations:write`", "summary": "Asignar tickets", "tags": ["Ticket"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"ticketId": {"type": "integer"}, "userId": {"type": "integer"}}, "required": ["ticketId", "userId"]}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "string", "const": "Ticket successfully assigned"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para reasignar un ticket a otro usuario del equipo.", "x-fc-domain": "conversations"}}, "/api/v1/tickets/{id}/close": {"post": {"operationId": "ticket.closeTicket", "description": "**Permiso requerido:** `conversations:write`", "summary": "Cerrar un ticket", "tags": ["Ticket"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "string"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Usalo para cerrar un ticket cuando el caso quedó resuelto.", "x-fc-domain": "conversations"}}, "/api/v1/tickets/{id}/reopen": {"post": {"operationId": "ticket.reOpenTicket", "description": "**Permiso requerido:** `conversations:write`", "summary": "Reabrir un ticket", "tags": ["Ticket"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "string", "const": "Ticket re-opened"}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:write", "x-fc-proposito": "Sirve para reabrir un ticket cerrado si el caso vuelve a necesitar atención.", "x-fc-domain": "conversations"}}, "/api/v1/agents/{user}": {"get": {"operationId": "user.show", "description": "**Permiso requerido:** `conversations:read`", "summary": "Ver un agente", "tags": ["User"], "parameters": [{"name": "user", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"anyOf": [{"$ref": "#/components/schemas/ConversationsUserResource"}, {"type": "object"}]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "404": {"$ref": "#/components/responses/ConversationsModelNotFoundException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Usalo para ver los datos de un agente (nombre, email y alcance como asesor), por ejemplo para mostrar quién atiende una conversación.", "x-fc-domain": "conversations"}}, "/api/v1/agents": {"get": {"operationId": "user.index", "description": "**Permiso requerido:** `conversations:read`", "summary": "Listar agentes", "tags": ["User"], "parameters": [{"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "email", "in": "query", "schema": {"type": "string", "format": "email"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "default": 25, "minimum": 1, "maximum": 100}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["name", "email", "created_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "links": {"type": "array", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/ConversationsValidationException"}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}}, "x-fc-permission": "conversations:read", "x-fc-proposito": "Sirve para listar los agentes de la cuenta y obtener sus IDs, que necesitás para asignar conversaciones o tickets y filtrar por asesor.", "x-fc-domain": "conversations"}}, "/api/v1/contacts/{contact}/message/form": {"post": {"operationId": "whatsappFormSend.store", "description": "**Permiso requerido:** `conversations:send`\n\nEnvía a un contacto un formulario de WhatsApp publicado. `form_id` es el identificador estable del formulario (el mismo en todas sus versiones) y `device_id` el número de WhatsApp Cloud de tu cuenta por el que sale. `text` (hasta 1024 caracteres) acompaña al formulario y por defecto es su nombre; `button_text` (hasta 30 caracteres, sin emojis) es el botón y por defecto es «Abrir» u «Open» según el idioma del usuario de la clave (en inglés «Open»; en español y portugués «Abrir»). Sólo funciona dentro de las 24 horas siguientes al último mensaje del contacto a ese número. Responde `200` con el mensaje en cola (`status` `queued`, `type` `whatsapp_form`); aceptado no es entregado: el estado final llega por `message.status_changed` o por `GET /contacts/{contact}/messages/{message}`, y la respuesta del contacto por `whatsapp_form.completed`. Responde `404` si el contacto no existe o no es de tu cuenta, o `404` con `form_not_found` si el formulario no existe en tu cuenta. Responde `422` con `device_unavailable` si el número no es de tu cuenta o no está conectado, `number_not_eligible` si no es un número de WhatsApp Cloud que pueda enviar formularios, `form_not_published` si el formulario no tiene versión publicada, `form_not_on_number` si no está publicado en ese número, `recipient_unreachable` si el contacto no tiene teléfono ni identificador de WhatsApp y `window_closed` si pasaron más de 24 horas. También responde `422` por las guardas de envío y `429` con `rate_limited` si superas el ritmo del número o del contacto.\n\nSi tu cuenta está bloqueada por el límite de contactos activos, responde `429` con `mac_lockout_24h` (además del `429` `rate_limited` del ritmo) y no guarda ningún mensaje ni `client_reference`. Si Funnelchat no pudo poner el mensaje en cola, la respuesta no es un error: es `200` con el mensaje en estado `failed` y `error.reason` `not_queued`; no salió, así que reintentar no lo duplica.", "summary": "Enviar un formulario", "tags": ["WhatsappFormSend"], "parameters": [{"name": "contact", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SendWhatsappFormRequest"}}}}, "responses": {"200": {"description": "OK", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"anyOf": [{"$ref": "#/components/schemas/PublicMessageResource"}, {"type": "null"}]}}, "required": ["message"]}}}}, "404": {"description": "Not Found", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"error": {"type": "string", "enum": ["form_not_found"]}, "message": {"type": "string"}}, "required": ["error", "message"]}, {"type": "object", "properties": {"message": {"type": "string"}}, "required": ["message"]}]}}}}, "403": {"$ref": "#/components/responses/ConversationsAuthorizationException"}, "401": {"$ref": "#/components/responses/ConversationsAuthenticationException"}, "422": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string"}, "errors": {"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}}, "required": ["message", "errors"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["window_closed", "antiblock_duplicate_content", "antiblock_unanswered_limit", "recipient_unsupported"]}, "message": {"type": "string"}}, "required": ["error", "message"]}, {"type": "object", "properties": {"error": {"type": "string", "enum": ["device_unavailable", "not_sent", "number_not_eligible", "form_not_published", "form_not_on_number", "recipient_unreachable", "window_closed"]}, "message": {"type": "string"}}, "required": ["error", "message"]}]}}}}, "429": {"description": "Too Many Requests", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "enum": ["rate_limited"]}, "message": {"type": "string"}}, "required": ["error", "message"]}}}, "headers": {"Retry-After": {"required": true, "schema": {"type": "integer"}}}}, "500": {"description": "Internal Server Error", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "enum": ["Server Error"]}}, "required": ["message"]}}}}}, "x-fc-permission": "conversations:send", "x-fc-proposito": "Sirve para pedirle datos a un contacto dentro de WhatsApp, por ejemplo para una inscripción o una encuesta, con un formulario que ya publicaste.", "x-fc-domain": "conversations"}}, "/api/v1/campaigns": {"get": {"operationId": "campaign.index", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `created_at`, `id`, `name`, `updated_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar campañas", "tags": ["Campañas"], "parameters": [{"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "orderBy", "in": "query", "schema": {"type": "string"}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer"}}], "responses": {"200": {"description": "Paginated set of `CampaignsResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/CampaignsResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para listar las campañas de grupos de WhatsApp de tu cuenta, con búsqueda por nombre, y obtener sus IDs para consultar grupos, participantes o estadísticas.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}": {"get": {"operationId": "campaign.show", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Ver una campaña", "tags": ["Campañas"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "`CampaignResource`", "content": {"application/json": {"schema": {"allOf": [{"$ref": "#/components/schemas/CampaignResource"}, {"type": "object", "required": ["moderation"]}]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para ver la configuración completa de una campaña: sus grupos, administradores, link de redirección, distribución del tráfico, píxel y grupos desconectados o con error.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/conditionals": {"get": {"operationId": "campaignConditional.index", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Ver las condicionales de una campaña", "tags": ["Campañas"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "Array of `CampaignConditionalResource`", "content": {"application/json": {"schema": {"type": "array", "items": {"allOf": [{"$ref": "#/components/schemas/CampaignConditionalResource"}, {"type": "object", "required": ["redirect_group_name"]}]}}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para conocer las reglas condicionales de una campaña: qué condiciones evalúa y a qué URL o grupo redirige cuando se cumplen.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/available-groups": {"get": {"operationId": "campaign.availableGroups", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `pageSize` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar los grupos que se pueden sumar a una campaña", "tags": ["Grupos de una campaña y operaciones"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "device_id", "in": "query", "required": true, "schema": {"type": "integer"}}, {"name": "type", "in": "query", "required": true, "schema": {"type": "string", "enum": ["groups", "ad-groups", "channels"]}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "pageSize", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 250}}, {"name": "name", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 255}}], "responses": {"200": {"description": "Array of `AvailableGroupResource`", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/AvailableGroupResource"}}, "meta": {"type": "string"}}, "required": ["data", "meta"]}, {"type": "string"}]}}}}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}}, "x-fc-proposito": "Sirve para ver qué grupos, grupos de anuncios o canales de tu cuenta se pueden sumar a una campaña, filtrando por número o por nombre.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/groups": {"get": {"operationId": "campaign.groups", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `id`, `name`, `created_at`, `updated_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar los grupos de una campaña", "tags": ["Grupos de una campaña y operaciones"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "orderBy", "in": "query", "schema": {"type": "string"}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer"}}, {"name": "deviceId", "in": "query", "schema": {"type": "integer"}}, {"name": "status", "in": "query", "schema": {"type": "integer"}}, {"name": "origin", "in": "query", "schema": {"type": "integer", "enum": ["0", "1"]}}, {"name": "adminConnection", "in": "query", "schema": {"type": "integer", "enum": ["1", "2", "3"]}}, {"name": "adminPhone", "in": "query", "schema": {"type": "string"}}], "responses": {"200": {"description": "Paginated set of `GroupResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/GroupResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para listar los grupos de una campaña con su estado, origen y conexión del administrador, por ejemplo para detectar grupos llenos o desconectados.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/pipelines": {"get": {"operationId": "campaign.indexPipelines", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `per_page` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `id`, `name`, `group_index`, `created_at`, `updated_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar los pipelines de una campaña", "tags": ["Grupos de una campaña y operaciones"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "per_page", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}, {"name": "status", "in": "query", "schema": {"type": "string", "enum": ["scheduled", "running", "completed", "failed", "cancelled"]}}, {"name": "action_type", "in": "query", "schema": {"type": "string", "enum": ["add", "new", "name", "description", "photo", "participants", "remove-participants", "administrators", "remove-administrators", "join_group", "members_can_send_messages", "remove_group"]}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["id", "name", "group_index", "created_at", "updated_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"campaign": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}}, "required": ["id", "name"]}, "data": {"type": "array", "items": {"type": "object", "properties": {"id": {"type": "integer"}, "pipeline_id": {"type": "string"}, "name": {"type": ["string", "null"]}, "group_index": {"type": ["integer", "null"]}, "created_at": {"type": ["string", "null"]}, "summary": {"type": "object", "properties": {"total_actions": {"type": "integer", "minimum": 0}, "total_action_groups": {"type": "string"}, "status_counts": {"type": "object", "properties": {"scheduled": {"type": "string"}, "running": {"type": "string"}, "completed": {"type": "string"}, "failed": {"type": "string"}, "cancelled": {"type": "string"}, "pending_confirmation": {"type": "string"}, "skipped": {"type": "string"}}, "required": ["scheduled", "running", "completed", "failed", "cancelled", "pending_confirmation", "skipped"]}, "progress_percentage": {"type": "number"}}, "required": ["total_actions", "total_action_groups", "status_counts", "progress_percentage"]}}, "required": ["id", "pipeline_id", "name", "group_index", "created_at", "summary"]}}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "last_page": {"type": "integer"}, "per_page": {"type": "integer"}, "total": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "to": {"type": ["integer", "null"]}}, "required": ["current_page", "last_page", "per_page", "total", "from", "to"]}}, "required": ["campaign", "data", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para seguir las operaciones masivas sobre los grupos de una campaña (cambios de nombre, foto, descripción, administradores o participantes), filtrando por estado o tipo de acción.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/pipelines/{pipeline}": {"get": {"operationId": "campaign.showPipeline", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Ver un pipeline", "tags": ["Grupos de una campaña y operaciones"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "pipeline", "in": "path", "required": true, "description": "The pipeline ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": ["object", "null"], "properties": {"pipeline_id": {"type": "string"}, "id": {"type": "integer"}, "campaign": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}}, "required": ["id", "name"]}, "summary": {"type": "object", "properties": {"name": {"type": ["string", "null"]}, "total_actions": {"type": "integer", "minimum": 0}, "total_action_groups": {"type": "string"}, "status_counts": {"type": "object", "properties": {"scheduled": {"type": "string"}, "running": {"type": "string"}, "completed": {"type": "string"}, "failed": {"type": "string"}, "cancelled": {"type": "string"}, "pending_confirmation": {"type": "string"}, "skipped": {"type": "string"}}, "required": ["scheduled", "running", "completed", "failed", "cancelled", "pending_confirmation", "skipped"]}, "progress_percentage": {"type": "number"}, "created_at": {"type": ["string", "null"]}, "scheduled_at": {"type": ["string", "null"]}, "completed_at": {"type": ["string", "null"]}}, "required": ["name", "total_actions", "total_action_groups", "status_counts", "progress_percentage", "created_at", "scheduled_at", "completed_at"]}, "actions": {"type": "array", "items": {"type": "string"}}}, "required": ["pipeline_id", "id", "campaign", "summary", "actions"]}}}}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para ver el avance de una operación masiva: cuántas acciones están programadas, en curso, completadas o fallidas, y el porcentaje de progreso.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/operation-plans/config": {"get": {"operationId": "operationPlan.config", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Ver la configuración de los planes de operación", "tags": ["Grupos de una campaña y operaciones"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"nudge_group_threshold": {"type": "integer"}, "default_daily_cap": {"type": "integer"}, "min_spacing_minutes": {"type": "integer"}, "confirmation_spacing_minutes": {"type": "integer"}, "default_window_start": {"type": "string"}, "default_window_end": {"type": "string"}, "daily_cap_max": {"type": "integer"}, "weekends_enabled": {"type": "boolean", "description": "Change `scheduled-bulk-admin-operations`, increment 8 (design.md\n§D10.2): lets the client show/hide the \"include weekends\" toggle."}}, "required": ["nudge_group_threshold", "default_daily_cap", "min_spacing_minutes", "confirmation_spacing_minutes", "default_window_start", "default_window_end", "daily_cap_max", "weekends_enabled"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para conocer los límites con los que se programan los planes de operación de una campaña: tope diario, espaciado mínimo, franja horaria y si se permiten fines de semana.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/operation-plans": {"get": {"operationId": "operationPlan.index", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `per_page` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar los planes de operación de una campaña", "tags": ["Grupos de una campaña y operaciones"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "per_page", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"campaign": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}}, "required": ["id", "name"]}, "data": {"type": "array", "items": {"$ref": "#/components/schemas/OperationPlanSummaryResource"}}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "last_page": {"type": "integer"}, "per_page": {"type": "integer"}, "total": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "to": {"type": ["integer", "null"]}}, "required": ["current_page", "last_page", "per_page", "total", "from", "to"]}}, "required": ["campaign", "data", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}}, "x-fc-proposito": "Usalo para listar los planes de operación programados de una campaña, con su estado, rango de fechas, tope diario y cantidad de operaciones.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/operation-plans/{plan}": {"get": {"operationId": "operationPlan.show", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Ver un plan de operación", "tags": ["Grupos de una campaña y operaciones"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "plan", "in": "path", "required": true, "description": "The plan ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "`OperationPlanResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/OperationPlanResource"}}}}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para ver el detalle de un plan de operación: estado, zona horaria, ventana horaria, rango de fechas, tope diario y espaciado entre operaciones.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/actions": {"get": {"operationId": "campaign.actions", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** página fija de 15 elementos; se recorre con `page`.", "summary": "Listar las acciones de sesión de una campaña", "tags": ["Grupos de una campaña y operaciones"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "Paginated set of `ActionResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ActionResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para auditar las acciones ejecutadas sobre los grupos de una campaña a través de la sesión de WhatsApp: qué cambió (valor anterior y nuevo), estado, intentos y fechas.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/actions/{action}/groups": {"get": {"operationId": "groupManagement.actionGroup", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `name`, `status`, `created_at`, `updated_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar los grupos alcanzados por una acción", "tags": ["Grupos de una campaña y operaciones"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "action", "in": "path", "required": true, "description": "The action ID", "schema": {"type": "integer"}}, {"name": "name", "in": "query", "schema": {"type": "string", "maxLength": 255}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["name", "status", "created_at", "updated_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "Paginated set of `ActionGroupResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ActionGroupResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para ver en qué grupos se aplicó una acción y con qué resultado en cada uno, útil para identificar los que fallaron.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/groups": {"get": {"operationId": "group.index", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `created_at`, `id`, `name`, `updated_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar grupos", "tags": ["Grupos"], "parameters": [{"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "orderBy", "in": "query", "schema": {"type": "string"}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string"}}, {"name": "type", "in": "query", "schema": {"type": "string", "enum": ["1", "2", "3", "4"]}}, {"name": "perPage", "in": "query", "schema": {"type": "integer"}}, {"name": "status", "in": "query", "schema": {"type": "string", "enum": ["0", "1", "2", "3", "4", "5", "6", "7", "8"]}}, {"name": "deviceId", "in": "query", "schema": {"type": "integer"}}, {"name": "noCampaign", "in": "query", "schema": {"type": "boolean"}}], "responses": {"200": {"description": "Paginated set of `GroupResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/GroupResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}, "cloud_api_groups_enabled": {"type": ["string", "boolean"]}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total", "cloud_api_groups_enabled"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para listar los grupos de tu cuenta con filtros por tipo, estado, número o sin campaña asignada, por ejemplo para sincronizarlos con tu sistema.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/devices/{device}/groups": {"get": {"operationId": "group.groups", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `pageSize` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar los grupos que administra un número", "tags": ["Grupos"], "parameters": [{"name": "device", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "pageSize", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 250}}, {"name": "name", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 255}}], "responses": {"200": {"description": "Array of `GroupMinResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/GroupMinResource"}}, "meta": {"type": "object", "properties": {"current_page": {}, "last_page": {}, "per_page": {}, "total": {"type": "integer"}}, "required": ["current_page", "last_page", "per_page", "total"]}}, "required": ["data", "meta"]}}}}, "400": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"error": {"anyOf": [{"type": "string"}, {"type": "string", "enum": ["An error has occurred"]}]}}, "required": ["error"]}, {"type": "object", "properties": {"error": {"type": "string", "const": "An error has occurred"}}, "required": ["error"]}]}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "const": "Device not found"}}, "required": ["error"]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para ver los grupos que administra un número según lo que informa el proveedor de WhatsApp, por ejemplo para encontrar grupos que todavía no sumaste a una campaña.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/devices/{device}/ad-groups": {"get": {"operationId": "group.adGroups", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `pageSize` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar los grupos de anuncios de un número", "tags": ["Grupos"], "parameters": [{"name": "device", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "pageSize", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 250}}, {"name": "name", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 255}}], "responses": {"200": {"description": "Array of `GroupMinResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/GroupMinResource"}}, "meta": {"type": "object", "properties": {"current_page": {}, "last_page": {}, "per_page": {}, "total": {"type": "integer"}}, "required": ["current_page", "last_page", "per_page", "total"]}}, "required": ["data", "meta"]}}}}, "400": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"error": {"anyOf": [{"type": "string"}, {"type": "string", "enum": ["An error has occurred"]}]}}, "required": ["error"]}, {"type": "object", "properties": {"error": {"type": "string", "const": "An error has occurred"}}, "required": ["error"]}]}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "const": "Device not found"}}, "required": ["error"]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para listar los grupos de anuncios que administra un número (la respuesta indica la comunidad a la que pertenecen), por ejemplo para elegir cuáles sumar a una campaña.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/devices/{device}/channels": {"get": {"operationId": "group.channels", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `pageSize` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar los canales de un número", "tags": ["Grupos"], "parameters": [{"name": "device", "in": "path", "required": true, "schema": {"type": "integer"}}, {"name": "name", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 255}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "pageSize", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 250}}], "responses": {"200": {"description": "Array of `ChannelMinResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ChannelMinResource"}}, "meta": {"type": "object", "properties": {"current_page": {}, "last_page": {}, "per_page": {}, "total": {"type": "integer"}}, "required": ["current_page", "last_page", "per_page", "total"]}}, "required": ["data", "meta"]}}}}, "400": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"error": {"type": "string", "const": "Invalid response format from WAPI"}}, "required": ["error"]}, {"type": "object", "properties": {"error": {"anyOf": [{"type": "string"}, {"type": "string", "enum": ["An error has occurred"]}]}}, "required": ["error"]}, {"type": "object", "properties": {"error": {"type": "string", "const": "An error has occurred"}}, "required": ["error"]}]}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "const": "Device not found"}}, "required": ["error"]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para listar los canales de WhatsApp de un número y obtener sus identificadores.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/groups/{groupUid}/metadata": {"get": {"operationId": "groupMetadata.show", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Ver los datos actuales de un grupo en WhatsApp", "tags": ["Grupos"], "parameters": [{"name": "groupUid", "in": "path", "required": true, "description": "Plain string segment — not a model binding.", "schema": {"type": "string"}}, {"name": "device_id", "in": "query", "required": true, "schema": {"type": "integer", "minimum": 1}}, {"name": "group_uid", "in": "query", "required": true, "schema": {"type": "string", "pattern": "^[0-9]{10,25}(-[0-9]+)?(@g\\.us|@newsletter)?$"}}, {"name": "type", "in": "query", "schema": {"type": "string", "enum": ["groups", "ad-groups", "channels"]}}], "responses": {"200": {"description": "`GroupMetadataResource`\n\n`ChannelMetadataResource`", "content": {"application/json": {"schema": {"anyOf": [{"$ref": "#/components/schemas/GroupMetadataResource"}, {"$ref": "#/components/schemas/ChannelMetadataResource"}]}}}}, "502": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Gateway error. Please try again later."}}, "required": ["message"]}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string", "const": "Group not found or not accessible."}}, "required": ["message"]}, {"type": "object", "properties": {"message": {"type": "string", "const": "Device not found or access denied."}}, "required": ["message"]}]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para leer en vivo desde WhatsApp el nombre, la foto, la descripción, el link y los administradores actuales de un grupo o canal, por ejemplo para verificar que coincidan con lo esperado.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/groups/{group}": {"get": {"operationId": "group.show", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Ver un grupo", "tags": ["Grupos"], "parameters": [{"name": "group", "in": "path", "required": true, "description": "The group uid", "schema": {"type": "string"}}], "responses": {"200": {"description": "`GroupResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/GroupResource"}}}}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para ver el detalle de un grupo: administradores, si el dueño está conectado, clics, link de invitación y de campaña, y si está lleno.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/groups/{group}/actions": {"get": {"operationId": "group.actions", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `status`, `created_at`, `updated_at`, `completed_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar las acciones de sesión sobre un grupo", "tags": ["Grupos"], "parameters": [{"name": "group", "in": "path", "required": true, "description": "The group uid", "schema": {"type": "string"}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["status", "created_at", "updated_at", "completed_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "Paginated set of `ActionGroupResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ActionGroupResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para ver el historial de acciones ejecutadas sobre un grupo puntual y su resultado, por ejemplo para entender por qué cambió su nombre o sus administradores.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/groups/{group}/messages": {"get": {"operationId": "group.messages", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** página fija de 3 elementos; se recorre con `page`.", "summary": "Listar los mensajes enviados a un grupo", "tags": ["Grupos"], "parameters": [{"name": "group", "in": "path", "required": true, "description": "The group uid", "schema": {"type": "string"}}], "responses": {"200": {"description": "Paginated set of `MessageResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/CommunitiesMessageResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para ver los mensajes que se enviaron a un grupo y de qué envío programado salió cada uno.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/groups/{group}/scheduled-messages": {"get": {"operationId": "group.scheduledMessages", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `send_at`, `name`, `status`, `id`, `created_at`, `updated_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar los envíos programados que incluyen un grupo", "tags": ["Grupos"], "parameters": [{"name": "group", "in": "path", "required": true, "description": "The group uid", "schema": {"type": "string"}}, {"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["send_at", "name", "status", "id", "created_at", "updated_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["ASC", "DESC"]}}, {"name": "perPage", "in": "query", "schema": {"type": "integer"}}], "responses": {"200": {"description": "Paginated set of `ScheduledMessageResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ScheduledMessageResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}}, "x-fc-proposito": "Usalo para saber qué envíos programados incluyen a un grupo, por ejemplo antes de hacerle cambios.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/participants": {"get": {"operationId": "campaign.participants", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `updated_at`, `id`, `created_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar los participantes de una campaña", "tags": ["Participantes"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "phone", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "orderBy", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": ["string", "null"], "enum": ["asc", "desc", null]}}, {"name": "perPage", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1}}], "responses": {"200": {"description": "Paginated set of `ParticipantResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ParticipantResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para listar los participantes de una campaña, con búsqueda por teléfono y si son administradores.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/campaign-participants": {"get": {"operationId": "campaign.campaignParticipants", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `joined_at`, `left_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar los participantes con su grupo actual", "tags": ["Participantes"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "search", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "status", "in": "query", "schema": {"type": ["string", "null"], "enum": ["active", "exited", null]}}, {"name": "orderBy", "in": "query", "schema": {"type": ["string", "null"], "enum": ["joined_at", "left_at", null]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": ["string", "null"], "enum": ["asc", "desc", "ASC", "DESC", null]}}, {"name": "perPage", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1}}, {"name": "start_date", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "end_date", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "timezone", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "date_rule", "in": "query", "schema": {"type": ["integer", "null"], "enum": ["1", "2", "3", null]}}, {"name": "selection_type", "in": "query", "schema": {"type": ["string", "null"], "enum": ["all", "none", null]}}, {"name": "groups_uids[]", "in": "query", "schema": {"type": "array", "items": {"type": "string"}}}], "responses": {"200": {"description": "Paginated set of `CampaignParticipantResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/CampaignParticipantResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para saber en qué grupo está cada participante de una campaña, cuándo entró o salió y si sigue activo. Ideal para sincronizar miembros con tu CRM.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/participant-events": {"get": {"operationId": "campaign.participantEvents", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar entradas y salidas de una campaña", "tags": ["Participantes"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "search", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 50}}, {"name": "event_type", "in": "query", "schema": {"type": ["string", "null"], "enum": ["all", "join", "leave", null]}}, {"name": "page", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1}}, {"name": "perPage", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1, "maximum": 100}}, {"name": "start_date", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "end_date", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "timezone", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 64}}, {"name": "date_mode", "in": "query", "schema": {"type": ["string", "null"], "enum": ["both", "joins_only", "leaves_only", null]}}, {"name": "selection_type", "in": "query", "schema": {"type": ["string", "null"], "enum": ["all", "none", null]}}, {"name": "groups_uids[]", "in": "query", "schema": {"type": "array", "items": {"type": "string", "maxLength": 50}}}, {"name": "participant_filter", "in": "query", "schema": {"type": ["string", "null"], "enum": ["all", "active", null]}}], "responses": {"200": {"description": "Paginated set of `ParticipantEventResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ParticipantEventResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}}, "x-fc-proposito": "Sirve para obtener el registro de entradas y salidas de toda una campaña, con fecha, grupo y país, filtrando por tipo de evento, fechas o grupos.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/groups/participants": {"get": {"operationId": "group.participants", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `created_at`, `id`, `updated_at`; dirección `orderDirection`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar los participantes de un grupo", "tags": ["Participantes"], "parameters": [{"name": "groupUid", "in": "query", "required": true, "schema": {"type": "string"}}, {"name": "phone", "in": "query", "schema": {"type": "string"}}, {"name": "isAdmin", "in": "query", "schema": {"type": "string", "enum": ["true", "false", "1", "0"]}}, {"name": "eligibleForAdmin", "in": "query", "schema": {"type": "string", "enum": ["true", "false", "1", "0"]}}, {"name": "orderBy", "in": "query", "schema": {"type": "string"}}, {"name": "orderDirection", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}, {"name": "perPage", "in": "query", "schema": {"type": "integer"}}], "responses": {"200": {"description": "Paginated set of `ParticipantResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ParticipantResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para listar los participantes de un grupo, filtrando por teléfono, si son administradores o si se los puede promover a administrador.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/groups/{group}/participant-events": {"get": {"operationId": "group.participantEvents", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar entradas y salidas de un grupo", "tags": ["Participantes"], "parameters": [{"name": "group", "in": "path", "required": true, "description": "The group uid", "schema": {"type": "string"}}, {"name": "search", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 50}}, {"name": "event_type", "in": "query", "schema": {"type": ["string", "null"], "enum": ["all", "join", "leave", null]}}, {"name": "page", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1}}, {"name": "perPage", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1, "maximum": 100}}, {"name": "start_date", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "end_date", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "timezone", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 64}}, {"name": "date_mode", "in": "query", "schema": {"type": ["string", "null"], "enum": ["both", "joins_only", "leaves_only", null]}}, {"name": "selection_type", "in": "query", "schema": {"type": ["string", "null"], "enum": ["all", "none", null]}}, {"name": "groups_uids[]", "in": "query", "schema": {"type": "array", "items": {"type": "string", "maxLength": 50}}}, {"name": "participant_filter", "in": "query", "schema": {"type": ["string", "null"], "enum": ["all", "active", null]}}], "responses": {"200": {"description": "Paginated set of `ParticipantEventResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ParticipantEventResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}}, "x-fc-proposito": "Sirve para ver quién entró y salió de un grupo puntual y cuándo, con país y filtros por fecha o tipo de evento.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/scheduled-messages": {"get": {"operationId": "scheduledMessage.index", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `created_at`, `id`, `name`, `updated_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar envíos programados", "tags": ["Mensajes programados"], "parameters": [{"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "orderBy", "in": "query", "schema": {"type": "string"}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer"}}, {"name": "status", "in": "query", "schema": {"type": "string", "enum": ["0", "1", "2", "3", "4", "5", "6", "7", "8"]}}, {"name": "only_needs_connection", "in": "query", "description": "The `boolean` rule only accepts PHP-native booleans/0/1/\"0\"/\"1\" — a\nquery-string boolean travels as the literal string \"true\"/\"false\" and\nwas rejected with a 422 before ever reaching the filter_var() coercion\nbelow. Whitelist the common wire forms explicitly instead.", "schema": {"type": "string", "enum": ["true", "false", "1", "0"]}}, {"name": "folder_id", "in": "query", "schema": {"type": ["integer", "null"]}}, {"name": "unclassified", "in": "query", "schema": {"type": "string", "enum": ["true", "false", "1", "0"]}}], "responses": {"200": {"description": "Paginated set of `ScheduledMessageIndexResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ScheduledMessageIndexResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para listar tus envíos programados a grupos con su estado, fecha, recurrencia y campañas, filtrando por carpeta o por los que necesitan reconectar un número.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/scheduled-messages/{scheduledMessage}": {"get": {"operationId": "scheduledMessage.show", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Ver un envío programado", "tags": ["Mensajes programados"], "parameters": [{"name": "scheduledMessage", "in": "path", "required": true, "description": "The scheduled message ID", "schema": {"type": "string", "format": "uuid"}}], "responses": {"200": {"description": "`App.Modules.ScheduledMessages.Resources.ScheduledMessageResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/App.Modules.ScheduledMessages.Resources.ScheduledMessageResource"}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para ver un envío programado completo: fecha, estado, audiencia, lecturas, reproducciones, clics en links y velocidad de envío.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/scheduled-messages/{scheduledMessage}/groups": {"get": {"operationId": "scheduledMessage.groups", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** página fija de 3 elementos; se recorre con `page`. Orden: `orderBy` acepta `created_at`, `id`, `name`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Listar los grupos destino de un envío", "tags": ["Mensajes programados"], "parameters": [{"name": "scheduledMessage", "in": "path", "required": true, "description": "The scheduled message ID", "schema": {"type": "string", "format": "uuid"}}, {"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "orderBy", "in": "query", "schema": {"type": "string"}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer"}}, {"name": "status", "in": "query", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/LengthAwarePaginator"}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para saber a qué grupos va dirigido un envío programado, filtrando por nombre o estado.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/scheduled-messages/{scheduledMessage}/group-messages": {"get": {"operationId": "scheduledMessage.groupMessages", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`). Orden: `orderBy` acepta `group_message_template.id`, `group_message_template.status`, `groups.name`, `group_message_template.created_at`; dirección `orderByMethod`: `asc` o `desc`. Otro valor responde `422`.", "summary": "Ver el resultado por grupo de un envío", "tags": ["Mensajes programados"], "parameters": [{"name": "scheduledMessage", "in": "path", "required": true, "description": "The scheduled message ID", "schema": {"type": "string", "format": "uuid"}}, {"name": "name", "in": "query", "schema": {"type": "string"}}, {"name": "orderBy", "in": "query", "schema": {"type": "string", "enum": ["group_message_template.id", "group_message_template.status", "groups.name", "group_message_template.created_at"]}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string", "enum": ["asc", "desc"]}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}, {"name": "status", "in": "query", "schema": {"type": "integer"}}, {"name": "edit_status", "in": "query", "description": "Outcome of the latest edit of the sent text (edit-sent), kept\napart from `status`, which stays the delivery state.\n| |\n|---|\n| `1` <br/> Confirmed delivered, inside the window: the provider accepted the edit. |\n| `2` <br/> Confirmed delivered, but 15 minutes had already passed since it was sent to that group. |\n| `3` <br/> Cloud API device, a channel, or a provider that cannot edit. |\n| `4` <br/> The provider returned an error, or the message never reached the group. |", "schema": {"$ref": "#/components/schemas/ScheduledMessageEditStatusEnum"}}, {"name": "run", "in": "query", "description": "Optional occurrence filter (D3): local calendar date (Y-m-d) of a run,\nresolved into a UTC window by ScheduledMessageRunService. Additive —\nomitting it preserves the exact previous behaviour (MAX(id) globally).", "schema": {"type": "string", "format": "date"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object", "properties": {"template_id": {"type": "string"}, "group_id": {"type": "string"}, "group_name": {"type": "string"}, "campaign_name": {"type": "string"}, "audience": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "schedule_message_status": {"type": "string"}, "error_info": {}, "totaL_readed": {}, "total_played": {}, "reactions_count": {}, "clicks": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "sent_by": {"type": "array", "items": {"type": "string"}}, "attempts": {"type": "integer"}, "last_attempt_at": {"type": "string"}, "edit_status": {"type": ["string", "null"], "description": "Latest edit of the sent text (ScheduledMessageEditStatusEnum)\nand its reason code; the provider's raw answer stays in\n`message_deliveries` for support and never reaches the response."}, "edit_reason": {"type": ["string", "null"]}}, "required": ["template_id", "group_id", "group_name", "campaign_name", "audience", "schedule_message_status", "error_info", "totaL_readed", "total_played", "reactions_count", "clicks", "sent_by", "attempts", "last_attempt_at", "edit_status", "edit_reason"]}}, "links": {"type": "object", "properties": {"first": {"type": "string"}, "last": {"type": "string"}, "prev": {"type": ["string", "null"]}, "next": {"type": "string"}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "string"}, "per_page": {"type": "integer"}, "to": {"type": ["integer", "null"]}, "total": {"type": "string"}}, "required": ["current_page", "from", "last_page", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para ver el resultado del envío en cada grupo: estado, error, lecturas, reproducciones, reacciones y clics, opcionalmente para una ocurrencia puntual de un envío recurrente.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/scheduled-messages/{scheduledMessage}/readed-played-rates": {"get": {"operationId": "scheduledMessage.readedAndPlayedRates", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Ver las tasas de lectura y reproducción de un envío", "tags": ["Mensajes programados"], "parameters": [{"name": "scheduledMessage", "in": "path", "required": true, "description": "The scheduled message ID", "schema": {"type": "string", "format": "uuid"}}, {"name": "run", "in": "query", "schema": {"type": "string", "format": "date"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"audience": {"type": ["integer", "null"]}, "total_readed": {"type": ["integer", "null"]}, "total_played": {"type": ["integer", "null"]}}, "required": ["audience", "total_readed", "total_played"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para medir el alcance de un envío: audiencia total, cuántos lo leyeron y cuántos reprodujeron el contenido, en total o por ocurrencia.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/scheduled-messages/{scheduledMessage}/runs": {"get": {"operationId": "scheduledMessage.runs", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar las ocurrencias de un envío recurrente", "tags": ["Mensajes programados"], "parameters": [{"name": "scheduledMessage", "in": "path", "required": true, "description": "The scheduled message ID", "schema": {"type": "string", "format": "uuid"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}], "responses": {"200": {"description": "Array of `ScheduledMessageRunsResource`", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/ScheduledMessageRunsResource"}}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para ver cada ocurrencia ya ejecutada de un envío recurrente, con cuántos grupos salieron bien y cuántos con error.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/scheduled-messages/folders": {"get": {"operationId": "scheduledMessageFolder.index", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Listar las carpetas de envíos", "tags": ["Respuestas y carpetas"], "responses": {"200": {"description": "Array of `ScheduledMessageFolderResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/ScheduledMessageFolderResource"}}, "meta": {"type": "object", "properties": {"all_count": {"type": "integer", "minimum": 0}, "unclassified_count": {"type": "integer", "minimum": 0}}, "required": ["all_count", "unclassified_count"]}}, "required": ["data", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para obtener las carpetas en las que se organizan los envíos programados y filtrar el listado por carpeta.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/scheduled-messages/{scheduledMessage}/responses": {"get": {"operationId": "scheduledMessage.responses", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `perPage` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar las respuestas a un envío", "tags": ["Respuestas y carpetas"], "parameters": [{"name": "scheduledMessage", "in": "path", "required": true, "description": "The scheduled message ID", "schema": {"type": "string", "format": "uuid"}}, {"name": "page", "in": "query", "description": "Coordinator finding (LOW): an unbounded `page` combined with\n`perPage` lets `($page-1)*$perPage` grow arbitrarily, which\n`MessageResponseReader::paginate()`'s out-of-range\nshort-circuit already makes cheap (no SELECT runs), but the\ncap here still guards against a pathological/overflowing\noffset value reaching that arithmetic at all. 100,000 is\ngenerous headroom above the measured production worst case\n(39,002 rows for a single poll/event, i.e. ≤39,002 pages even\nat perPage=1).", "schema": {"type": "integer", "minimum": 1, "maximum": 100000}}, {"name": "perPage", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 1000}}, {"name": "type", "in": "query", "schema": {"type": "string", "enum": ["poll", "event"]}}], "responses": {"200": {"description": "Array of `MessageResponseResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/MessageResponseResource"}}, "meta": {"type": "object", "properties": {"total": {"type": "string"}, "per_page": {"type": "integer"}, "current_page": {"type": "integer"}, "last_page": {"type": "integer"}}, "required": ["total", "per_page", "current_page", "last_page"]}}, "required": ["data", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}}, "x-fc-proposito": "Sirve para recolectar las respuestas a encuestas o eventos de un envío: quién respondió, en qué grupo, qué eligió y cuándo.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/scheduled-messages/{scheduledMessage}/response-tags": {"get": {"operationId": "scheduledMessage.responseTags", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Listar las etiquetas que se aplican a quien responde", "tags": ["Respuestas y carpetas"], "parameters": [{"name": "scheduledMessage", "in": "path", "required": true, "description": "The scheduled message ID", "schema": {"type": "string", "format": "uuid"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object", "properties": {"message_template_id": {"type": "string"}, "block_type": {"type": "string"}, "options": {"type": "string"}, "responses_total": {"type": "string"}, "untagged_total": {"type": "string"}}, "required": ["message_template_id", "block_type", "options", "responses_total", "untagged_total"]}}}, "required": ["data"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para ver, por cada encuesta o evento del envío, qué etiquetas se aplican a quien responde según la opción elegida y cuántas respuestas quedaron sin etiquetar.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/stats/participants": {"get": {"operationId": "stats.participantStats", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** `start_date` y `end_date` son obligatorios, con formato `AAAA-MM-DD`, y el rango entre los dos, ambos incluidos, admite como máximo 366 días (un año completo, bisiesto incluido). Si falta una de las dos fechas, si una es inválida, si `end_date` es anterior a `start_date` o si el rango es mayor, responde `422`.", "summary": "Ver participantes por período", "tags": ["Estadísticas"], "parameters": [{"name": "start_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "end_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "unit", "in": "query", "schema": {"type": "string", "enum": ["day", "month", "hour"]}}, {"name": "type", "in": "query", "schema": {"type": "string", "enum": ["in", "out"]}}, {"name": "target", "in": "query", "schema": {"type": "string", "enum": ["groups", "campaigns"]}}, {"name": "ids[]", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}, {"name": "fill_missing", "in": "query", "schema": {"type": "boolean"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"in": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}}, "total_participants": {"type": "string"}}, "required": ["data", "total_participants"]}, "out": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}}, "total_participants": {"type": "string"}}, "required": ["data", "total_participants"]}}, "required": ["in", "out"]}, {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}}, "total_participants": {"type": "string"}}, "required": ["data", "total_participants"]}, {"type": "object", "properties": {"in": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "total_participants": {"type": "integer", "const": 0}}, "required": ["data", "total_participants"]}, "out": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "total_participants": {"type": "integer", "const": 0}}, "required": ["data", "total_participants"]}}, "required": ["in", "out"]}, {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "total_participants": {"type": "integer", "const": 0}}, "required": ["data", "total_participants"]}]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para graficar entradas y salidas de participantes por hora, día o mes en tus grupos o campañas, en un rango de hasta 366 días.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/stats/active-participants": {"get": {"operationId": "stats.activeParticipants", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** `start_date` y `end_date` son obligatorios, con formato `AAAA-MM-DD`, y el rango entre los dos, ambos incluidos, admite como máximo 366 días (un año completo, bisiesto incluido). Si falta una de las dos fechas, si una es inválida, si `end_date` es anterior a `start_date` o si el rango es mayor, responde `422`.", "summary": "Ver participantes activos", "tags": ["Estadísticas"], "parameters": [{"name": "start_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "end_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "unit", "in": "query", "schema": {"type": "string", "enum": ["day", "month", "hour"]}}, {"name": "target", "in": "query", "schema": {"type": "string", "enum": ["groups", "campaigns"]}}, {"name": "ids[]", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}, {"name": "status", "in": "query", "schema": {"type": "string", "enum": ["active", "inactive"]}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"active": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object", "properties": {"date": {"type": "string"}, "count": {"type": "integer"}}, "required": ["date", "count"]}}, "total_participants": {"type": "string"}}, "required": ["data", "total_participants"]}, "inactive": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object", "properties": {"date": {"type": "string"}, "count": {"type": "integer"}}, "required": ["date", "count"]}}, "total_participants": {"type": "string"}}, "required": ["data", "total_participants"]}}, "required": ["active", "inactive"]}, {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object", "properties": {"date": {"type": "string"}, "count": {"type": "integer"}}, "required": ["date", "count"]}}, "total_participants": {"type": "string"}}, "required": ["data", "total_participants"]}, {"type": "object", "properties": {"active": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "total_participants": {"type": "integer", "const": 0}}, "required": ["data", "total_participants"]}, "inactive": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "total_participants": {"type": "integer", "const": 0}}, "required": ["data", "total_participants"]}}, "required": ["active", "inactive"]}, {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "total_participants": {"type": "integer", "const": 0}}, "required": ["data", "total_participants"]}]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para ver cómo evolucionan los participantes activos e inactivos por período en tus grupos o campañas, por ejemplo para seguir el nivel de actividad de tu comunidad.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/stats/groups": {"get": {"operationId": "stats.groupStats", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** `start_date` y `end_date` son obligatorios, con formato `AAAA-MM-DD`, y el rango entre los dos, ambos incluidos, admite como máximo 366 días (un año completo, bisiesto incluido). Si falta una de las dos fechas, si una es inválida, si `end_date` es anterior a `start_date` o si el rango es mayor, responde `422`.", "summary": "Ver grupos por estado", "tags": ["Estadísticas"], "parameters": [{"name": "start_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "end_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "unit", "in": "query", "required": true, "schema": {"type": "string", "enum": ["day", "month", "hour"]}}, {"name": "fill_missing", "in": "query", "schema": {"type": "boolean"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}}, "total_groups": {"type": "string"}}, "required": ["data", "total_groups"]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para seguir cuántos grupos tenés en cada estado a lo largo del tiempo, por hora, día o mes.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/stats/clicks": {"get": {"operationId": "stats.clickStats", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** `start_date` y `end_date` son obligatorios, con formato `AAAA-MM-DD`, y el rango entre los dos, ambos incluidos, admite como máximo 366 días (un año completo, bisiesto incluido). Si falta una de las dos fechas, si una es inválida, si `end_date` es anterior a `start_date` o si el rango es mayor, responde `422`.", "summary": "Ver clics en links", "tags": ["Estadísticas"], "parameters": [{"name": "start_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "end_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "unit", "in": "query", "schema": {"type": "string", "enum": ["day", "month", "hour"]}}, {"name": "target", "in": "query", "schema": {"type": "string", "enum": ["groups", "campaigns"]}}, {"name": "ids[]", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}, {"name": "fill_missing", "in": "query", "schema": {"type": "boolean"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}}, "total_clicks": {"type": "string"}}, "required": ["data", "total_clicks"]}, {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "total_clicks": {"type": "integer", "const": 0}}, "required": ["data", "total_clicks"]}]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para medir los clics en los links de tus grupos o campañas por período y comparar cuáles atraen más tráfico.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/stats/records": {"get": {"operationId": "stats.statsRecords", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `per_page` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar registros de clics", "tags": ["Estadísticas"], "parameters": [{"name": "start_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "end_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "target", "in": "query", "schema": {"type": "string", "enum": ["groups", "campaigns"]}}, {"name": "ids[]", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "per_page", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}, {"name": "devices[]", "in": "query", "schema": {"type": "array", "items": {"type": "string", "maxLength": 64}}}, {"name": "countries[]", "in": "query", "schema": {"type": "array", "items": {"type": "string", "maxLength": 10}}}, {"name": "search", "in": "query", "schema": {"type": "string", "maxLength": 255}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object", "properties": {"id": {"type": "string"}, "address": {"type": "string"}, "browser": {"type": "string"}, "country": {"type": "string"}, "device": {"type": "string"}, "language": {"type": "string"}, "platform": {"type": "string"}, "referrer": {"type": "string"}, "group": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}}, "required": ["id", "name"]}, "phone": {"type": "string"}, "email": {"type": "string"}, "name": {"type": "string"}, "metadata": {}, "created_at": {"type": "string"}, "updated_at": {"type": "string"}}, "required": ["id", "address", "browser", "country", "device", "language", "platform", "referrer", "group", "phone", "email", "name", "metadata", "created_at", "updated_at"]}}, "total": {"type": "integer", "minimum": 0}, "per_page": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [50]}]}, "current_page": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [1]}]}, "last_page": {"type": "number"}}, "required": ["data", "total", "per_page", "current_page", "last_page"]}, {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "total": {"type": "integer", "const": 0}, "per_page": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [50]}]}, "current_page": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [1]}]}}, "required": ["data", "total", "per_page", "current_page"]}]}}}}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para bajar los registros individuales de tus grupos o campañas (país, dispositivo, navegador, referencia, grupo y datos de contacto si los hay), filtrando por fecha, país o dispositivo.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/stats/country-metrics": {"get": {"operationId": "stats.countryMetrics", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Ver métricas por país", "tags": ["Estadísticas"], "parameters": [{"name": "start_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "end_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "target", "in": "query", "schema": {"type": "string", "enum": ["groups", "campaigns"]}}, {"name": "ids[]", "in": "query", "schema": {"type": "array", "items": {"type": "integer"}}}, {"name": "countries[]", "in": "query", "schema": {"type": "array", "items": {"type": "string", "maxLength": 10}}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"anyOf": [{"type": "object", "properties": {"summary": {"type": "object", "properties": {"total_contacts": {"type": "string"}, "total_countries": {"type": "integer", "minimum": 0}, "total_clicks": {"type": "string"}, "total_entries": {"type": "string"}, "total_exits": {"type": "string"}}, "required": ["total_contacts", "total_countries", "total_clicks", "total_entries", "total_exits"]}, "countries": {"type": "array", "items": {"type": "object", "properties": {"country_code": {"type": "string"}, "country_name": {"type": "string"}, "contacts": {"type": "integer"}, "percentage": {"type": "number"}, "clicks": {"type": "integer"}, "entries": {"type": "integer"}, "entry_rate": {"type": "number"}, "exits": {"type": "integer"}}, "required": ["country_code", "country_name", "contacts", "percentage", "clicks", "entries", "entry_rate", "exits"]}}}, "required": ["summary", "countries"]}, {"type": "object", "properties": {"summary": {"type": "object", "properties": {"total_contacts": {"type": "integer", "const": 0}, "total_countries": {"type": "integer", "const": 0}, "total_clicks": {"type": "integer", "const": 0}, "total_entries": {"type": "integer", "const": 0}, "total_exits": {"type": "integer", "const": 0}}, "required": ["total_contacts", "total_countries", "total_clicks", "total_entries", "total_exits"]}, "countries": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}}, "required": ["summary", "countries"]}]}}, "required": ["data"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}}, "x-fc-proposito": "Usalo para analizar tu audiencia por país: contactos, clics, entradas, tasa de entrada y salidas en tus grupos o campañas.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/stats/countries": {"get": {"operationId": "stats.countryList", "description": "**Permiso requerido:** `communities:read` (Leer)", "summary": "Listar países presentes", "tags": ["Estadísticas"], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"type": "object", "properties": {"country_code": {"type": ["string", "null"]}, "country_name": {"type": ["string", "null"]}}, "required": ["country_code", "country_name"]}}}, "required": ["data"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Sirve para obtener la lista de países presentes en tus datos, por ejemplo para armar el filtro de países de tus reportes.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/webhooks": {"get": {"operationId": "campaignWebhook.index", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Nota:** Webhooks legados de campaña, con otro formato; no reciben los eventos de plataforma (ver `/webhooks`).", "summary": "Listar los webhooks de una campaña", "tags": ["Webhooks de campaña (legado)"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "Array of `CampaignWebhookResource`", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/CampaignWebhookResource"}}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}}, "x-fc-proposito": "Usalo para ver los webhooks legados configurados en una campaña (tipo, URL y si están activos). Para integraciones nuevas preferí las suscripciones de `/v1/webhooks`.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/campaigns/{campaign}/webhooks/logs": {"get": {"operationId": "campaignWebhook.logs", "description": "**Permiso requerido:** `communities:read` (Leer)\n\n**Con API key:** la respuesta siempre viene paginada. Tamaño de página: `per_page` (por defecto 25, máximo 100; un valor mayor, menor que 1 o no entero responde `422`).", "summary": "Listar las entregas de los webhooks de una campaña", "tags": ["Webhooks de campaña (legado)"], "parameters": [{"name": "campaign", "in": "path", "required": true, "description": "The campaign ID", "schema": {"type": "integer"}}, {"name": "start_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "end_date", "in": "query", "schema": {"type": "string", "format": "date-time"}}, {"name": "status", "in": "query", "schema": {"type": "integer", "enum": ["0", "1", "2"]}}, {"name": "type", "in": "query", "schema": {"type": "integer", "enum": ["0", "1", "2"]}}, {"name": "per_page", "in": "query", "schema": {"type": "integer", "minimum": 1, "maximum": 100}}], "responses": {"200": {"description": "Paginated set of `CampaignWebhookLogResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/CampaignWebhookLogResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/CommunitiesAuthorizationException"}, "404": {"$ref": "#/components/responses/CommunitiesModelNotFoundException"}, "401": {"$ref": "#/components/responses/CommunitiesAuthenticationException"}, "422": {"$ref": "#/components/responses/CommunitiesValidationException"}}, "x-fc-proposito": "Sirve para depurar los webhooks legados de una campaña: ves cada envío con su payload, la respuesta, el tiempo de ejecución y el error, filtrando por fecha, estado o tipo.", "x-fc-domain": "communities", "x-fc-permission": "communities:read"}}, "/api/v1/me": {"get": {"description": "**Lectura** · requiere el permiso `accounts:read`.", "responses": {"200": {"description": "`UserResource`", "content": {"application/json": {"schema": {"allOf": [{"$ref": "#/components/schemas/AccountsUserResource"}, {"type": "object", "required": ["feature_versions"]}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "tags": ["Account"], "summary": "Ver mi cuenta", "x-fc-proposito": "Usalo para identificar la cuenta detrás de tu clave y conocer su plan, los límites de números y agentes, el rol, la zona horaria y el idioma, por ejemplo al conectar tu integración.", "x-fc-domain": "accounts"}}, "/api/v1/alerts/campaigns": {"get": {"operationId": "alertCampaign.index", "description": "List campaigns visible to the authenticated account (owner + agents\nscope), reading from the read-only `groups` connection. Paginated,\nsearchable by `name`. Degrades to 503 when the `groups` connection is\nunavailable instead of leaking a 500 / stack trace\n\n**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25); también `per_page` · orden fijo: id ascendente · no acepta parámetros de orden.", "summary": "Listar las campañas para alertas", "tags": ["AlertCampaign"], "parameters": [{"name": "page", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1}}, {"name": "per_page", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1, "maximum": 100}}, {"name": "search", "in": "query", "schema": {"type": ["string", "null"], "maxLength": 100}}], "responses": {"200": {"description": "Paginated set of `AlertCampaignResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/AlertCampaignResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "503": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "No se pudieron cargar las campañas en este momento."}, "error": {"type": "string", "const": "groups_unavailable"}}, "required": ["message", "error"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage", "per_page"], "order": "id ascendente", "sortable": []}, "x-fc-proposito": "Sirve para listar, con búsqueda por nombre, las campañas de grupos que podés elegir al configurar alertas como el reporte diario.", "x-fc-domain": "accounts"}}, "/api/v1/alerts/catalog": {"get": {"operationId": "alertCatalog.index", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\nStripe-only alerts (concepts/events that only exist for accounts billed\nthrough Stripe, e.g. subscription reactivation, dormant period ending)\nare excluded when the owner's payment_provider is not Stripe (Hotmart\nor null).", "summary": "Ver el catálogo de alertas", "tags": ["AlertCatalog"], "responses": {"200": {"description": "Array of `AlertCatalogResource`", "content": {"application/json": {"schema": {"type": "array", "items": {"$ref": "#/components/schemas/AlertCatalogResource"}}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-proposito": "Usalo para conocer qué alertas existen (números desconectados, consumo de MACs, cobros, campañas, grupos y más), cuáles son críticas y qué significan, antes de activarlas en un destino.", "x-fc-domain": "accounts"}}, "/api/v1/alerts/daily-report": {"get": {"operationId": "alertDailyReport.show", "description": "Return the daily report configuration for the authenticated owner.\nReturns a default structure if no record exists yet\n\n**Lectura** · requiere el permiso `accounts:read`.", "summary": "Ver el reporte diario", "tags": ["AlertDailyReport"], "responses": {"200": {"description": "`AlertDailyReportResource`", "content": {"application/json": {"schema": {"anyOf": [{"$ref": "#/components/schemas/AlertDailyReportResource"}, {"type": "object", "properties": {"enabled": {"type": "boolean"}, "send_time": {"type": "string", "const": "08:00"}, "all_campaigns": {"type": "boolean"}, "campaign_ids": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}}, "required": ["enabled", "send_time", "all_campaigns", "campaign_ids"]}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-proposito": "Sirve para ver cómo está configurado el reporte diario: si está activo, a qué hora se envía y qué campañas incluye.", "x-fc-domain": "accounts"}, "put": {"operationId": "alertDailyReport.update", "description": "Upsert the daily report configuration for the authenticated owner.\nEnsures exactly one row per owner (unique(owner_id))\n\n**Escritura** · requiere el permiso `accounts:write`.\n\nWhen `all_campaigns` is false, the submitted `campaign_ids` are pruned\nto only those that still resolve to a visible campaign (owner + agents\nscope) in the `groups` database, dropping orphans silently. The purge\nis best-effort: if the `groups` connection is unavailable, the\nsubmitted ids are persisted unpruned rather than being wiped out.", "summary": "Configurar el reporte diario", "tags": ["AlertDailyReport"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateAlertDailyReportRequest"}}}}, "responses": {"200": {"description": "`AlertDailyReportResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertDailyReportResource"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Usalo para activar o ajustar el reporte diario: el horario de envío y si cubre todas las campañas o solo algunas.", "x-fc-domain": "accounts"}}, "/api/v1/alerts/destinations": {"get": {"operationId": "alertDestination.index", "description": "List destinations for the authenticated owner.\nSeed lazy: if the owner has no destinations and has a phone number,\ncreate an unverified phone destination from it\n\n**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25) · orden fijo: id ascendente · no acepta parámetros de orden.", "summary": "Listar los destinos de alertas", "tags": ["AlertDestination"], "responses": {"200": {"description": "Array of `AlertDestinationResource`\n\nPaginated set of `AlertDestinationResource`", "content": {"application/json": {"schema": {"anyOf": [{"type": "array", "items": {"$ref": "#/components/schemas/AlertDestinationResource"}}, {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/AlertDestinationResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage"], "order": "id ascendente", "sortable": []}, "x-fc-proposito": "Sirve para ver a dónde llegan tus alertas (teléfonos o webhooks), si están verificados y qué alertas tiene activas cada uno.", "x-fc-domain": "accounts"}, "post": {"operationId": "alertDestination.store", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\nEnforces, para `webhook`:\n  - tope de {@see self::WEBHOOK_DESTINATION_LIMIT} destinos por cuenta;\n  - `SsrfGuard::assertSafe()` ANTES de persistir. Hasta este incremento el\n    unico chequeo en el alta era `str_starts_with($destination, 'https://')`,\n    y el guard corria solo en el boton de prueba manual: se podia guardar\n    tranquilamente un destino apuntando a `https://127.0.0.1/...` o al\n    endpoint de metadata de la nube, y quedaba ahi esperando a que el\n    transporte de entrega lo usara;\n  - `verified_at` en null. Un webhook NO se auto-verifica: hay que pasar\n    por `POST alerts/destinations/{id}/verify`, que exige que el destino\n    devuelva un challenge firmado (ver AlertWebhookVerificationController);\n  - generacion del secreto de firma, devuelto en texto plano UNA sola vez\n    en esta respuesta 201 y nunca mas.", "summary": "Crear un destino de alertas", "tags": ["AlertDestination"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreAlertDestinationRequest"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Usalo para sumar un teléfono o un webhook HTTPS que reciba tus alertas operativas. Después hay que verificarlo; si es un webhook, guardá el secreto de firma, que se muestra una sola vez.", "x-fc-domain": "accounts"}}, "/api/v1/alerts/destinations/{id}": {"patch": {"operationId": "alertDestination.update", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\nLa URL de un `phone` sigue siendo de solo lectura post-creacion (se\nverifica por OTP contra ese numero; cambiarlo seria crear otro destino).\nPara un `webhook` se permite corregirla, y esa es justamente la razon por\nla que el guard anti-SSRF tiene que correr TAMBIEN aca: si solo corriera\nen el alta, editar seria la puerta de atras para dejar persistida una URL\nque apunta a la red interna.\n\nCambiar la URL resetea `verified_at`: lo que se verifico fue un endpoint\nconcreto, no el registro.\n\nReturns 404 for both non-existent and other-owner destinations to prevent\nresource-existence enumeration by unauthenticated/unauthorized callers.", "summary": "Actualizar un destino de alertas", "tags": ["AlertDestination"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateAlertDestinationRequest"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para renombrar un destino de alertas o, si es un webhook, corregir su URL; cambiar la URL obliga a verificarlo de nuevo.", "x-fc-domain": "accounts"}, "delete": {"operationId": "alertDestination.destroy", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\nReturns 404 for both non-existent and other-owner destinations to prevent\nresource-existence enumeration by unauthenticated/unauthorized callers.", "summary": "Eliminar un destino de alertas", "tags": ["AlertDestination"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "No content"}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Destino no encontrado."}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Usalo para dejar de enviar alertas a un teléfono o webhook; se borran también sus configuraciones de alertas.", "x-fc-domain": "accounts"}}, "/api/v1/alerts/destinations/{id}/otp": {"post": {"operationId": "alertOtp.send", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\nHTTP 202: OTP dispatched.\nHTTP 422: destination is not a phone or is already verified.\nHTTP 429: cooldown active or hourly limit reached.", "summary": "Enviar un código de verificación", "tags": ["AlertOtp"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"202": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Código enviado."}}, "required": ["message"]}}}}, "422": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string"}}, "required": ["message"]}}}}, "429": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string", "const": "Alcanzaste el límite de reenvíos por hora. Intenta más tarde."}}, "required": ["message"]}, {"type": "object", "properties": {"message": {"type": "string", "const": "Debes esperar antes de solicitar un nuevo código."}, "retry_after_seconds": {"type": "integer"}}, "required": ["message", "retry_after_seconds"]}]}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Destino no encontrado."}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para enviar (o reenviar) el código de verificación a un teléfono de alertas, paso previo para poder activarle alertas. Tiene espera entre envíos y tope por hora.", "x-fc-domain": "accounts"}}, "/api/v1/alerts/destinations/{id}/otp/verify": {"post": {"operationId": "alertOtp.verify", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\nHTTP 200: verified — returns updated destination resource.\nHTTP 422: destination not a phone, already verified, or wrong/expired code.", "summary": "Verificar el código", "tags": ["AlertOtp"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/VerifyAlertOtpRequest"}}}}, "responses": {"200": {"description": "`AlertDestinationResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertDestinationResource"}}}}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Destino no encontrado."}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Usalo para confirmar un teléfono de alertas con el código recibido; una vez verificado, ya le podés activar alertas.", "x-fc-domain": "accounts"}}, "/api/v1/alerts/quiet-hours": {"get": {"operationId": "alertQuietHour.show", "description": "Return the quiet hours configuration for the authenticated owner.\nReturns an empty/default structure if no record exists yet\n\n**Lectura** · requiere el permiso `accounts:read`.", "summary": "Ver el horario de silencio", "tags": ["AlertQuietHour"], "responses": {"200": {"description": "`AlertQuietHourResource`", "content": {"application/json": {"schema": {"anyOf": [{"$ref": "#/components/schemas/AlertQuietHourResource"}, {"type": "object", "properties": {"enabled": {"type": "boolean"}, "start_time": {"type": "null"}, "end_time": {"type": "null"}}, "required": ["enabled", "start_time", "end_time"]}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-proposito": "Sirve para ver si hay un horario de silencio configurado para las alertas y entre qué horas rige.", "x-fc-domain": "accounts"}, "put": {"operationId": "alertQuietHour.update", "description": "Upsert the quiet hours configuration for the authenticated owner.\nEnsures exactly one row per owner (unique(owner_id))\n\n**Escritura** · requiere el permiso `accounts:write`.", "summary": "Configurar el horario de silencio", "tags": ["AlertQuietHour"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateAlertQuietHourRequest"}}}}, "responses": {"200": {"description": "`AlertQuietHourResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertQuietHourResource"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Usalo para definir una franja horaria sin alertas, por ejemplo de noche, o para desactivarla.", "x-fc-domain": "accounts"}}, "/api/v1/alerts/destinations/{id}/settings": {"put": {"operationId": "alertSetting.upsert", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\n- Validates that the module/action pair exists in AlertCatalog.\n- Validates that the destination is verified (verified_at not null).\n- Enforces unique(alert_destination_id, module, action) via updateOrCreate.\n- Owner-scoped: returns 404 for both non-existent and other-owner destinations\n  to prevent resource-existence enumeration.", "summary": "Activar o desactivar una alerta", "tags": ["AlertSetting"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpsertAlertSettingRequest"}}}}, "responses": {"200": {"description": "`AlertSettingResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/AlertSettingResource"}}}}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Destino no encontrado."}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para elegir qué alertas recibe cada destino verificado, prendiéndolas o apagándolas una por una, por ejemplo la desconexión de un número o un pago fallido.", "x-fc-domain": "accounts"}}, "/api/v1/alerts/destinations/{id}/test": {"post": {"operationId": "alertWebhookTest.test", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\n- Owner-scoped: returns 404 for both non-existent and other-owner destinations\n  to prevent resource-existence enumeration.\n- Only webhook-type destinations are allowed.\n- SSRF guard is applied inside WebhookTestService before any HTTP call.", "summary": "Probar un destino webhook", "tags": ["AlertWebhookTest"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"status": {"type": ["integer", "null"]}, "ok": {"type": "boolean"}, "latency": {"type": "integer"}}, "required": ["status", "ok", "latency"]}}}}, "422": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"message": {"type": "string"}, "status": {"type": "null"}, "ok": {"type": "boolean"}, "latency": {"type": "integer", "const": 0}}, "required": ["message", "status", "ok", "latency"]}, {"type": "object", "properties": {"message": {"type": "string", "const": "Solo los destinos webhook pueden ser probados."}}, "required": ["message"]}]}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Destino no encontrado."}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Usalo para comprobar que tu webhook de alertas responde: envía un payload de prueba y te devuelve el estado y la latencia.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/capi/dataset": {"post": {"operationId": "capiConnection.dataset", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\nIdempotently create or recover the Meta Conversions API dataset for the\ndevice's WABA and persist it in `waba_capi_datasets`.\n\nAuthorized for the device owner only (DevicePolicy::update) — agents cannot\nprovision or manage CAPI datasets.", "summary": "Configurar el dataset de Conversions API", "tags": ["CapiConnection"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "`WabaCapiDatasetResource`", "content": {"application/json": {"schema": {"anyOf": [{"type": "object"}, {"$ref": "#/components/schemas/WabaCapiDatasetResource"}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para dejar listo el dataset de Meta Conversions API del WABA de un número, creándolo o recuperándolo si ya existía. Solo lo puede hacer el titular de la cuenta.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/capi/test": {"get": {"operationId": "capiConnection.test", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\n\"Probar conexión\": verifies the device token has the\n`whatsapp_business_manage_events` scope (`GET /debug_token`) and that the\nWABA has a dataset (`GET /{WABA_ID}/dataset`), persists the result, and\nreturns an aggregated status + a link to Meta's Events Manager.\n\nNEVER sends `POST /{DATASET_ID}/events` — Meta does not offer a\n`test_event_code` for business messaging and a test Purchase would inject\na fake sale (design.md D6).\n\nAuthorized for the device owner only (DevicePolicy::update) — agents cannot\ncheck CAPI connection status.", "summary": "Probar la conexión con Conversions API", "tags": ["CapiConnection"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-proposito": "Usalo para verificar la conexión con Conversions API: chequea los permisos del token y que el WABA tenga dataset, sin enviar eventos de prueba, y te da el link al Events Manager.", "x-fc-domain": "accounts"}}, "/api/v1/devices/capi/datasets": {"get": {"operationId": "capiConnection.datasets", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25) · orden fijo: id ascendente · no acepta parámetros de orden.\n\nRead-only status snapshot: returns the authenticated owner's already\npersisted `waba_capi_datasets` rows, one per WABA they've connected.\n\nUnlike `test()`, this endpoint NEVER calls Meta and NEVER writes — it only\nreads what a previous `dataset`/`test` call (or the Meta connection flow)\nalready persisted. Powers the connection card showing CAPI status as soon\nas it opens, without requiring the user to press \"Probar conexión\" first.\n\nNot device-scoped: not authorized via `DevicePolicy` against a single\n`Device` — it is scoped to `auth()->id()` directly, since\n`waba_capi_datasets.user_id` always holds the account owner's id (see\n`WabaCapiDataset`). Agents share the owner's account (`payment-valid` +\n`user-admin` gates on the route already require an owner-level token).", "summary": "Listar los datasets de Conversions API", "tags": ["CapiConnection"], "responses": {"200": {"description": "Array of items", "content": {"application/json": {"schema": {"anyOf": [{"type": "object"}, {"type": "array", "items": {"type": "string"}}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage"], "order": "id ascendente", "sortable": []}, "x-fc-proposito": "Sirve para mostrar el estado de Conversions API de todos tus WABA conectados sin llamar a Meta, a partir de lo que guardaron las verificaciones anteriores.", "x-fc-domain": "accounts"}}, "/api/v1/custom-domains": {"get": {"operationId": "customDomain.index", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25) · orden fijo: id ascendente · no acepta parámetros de orden.", "summary": "Listar dominios propios", "tags": ["CustomDomain"], "responses": {"200": {"description": "Array of `CustomDomainResource`\n\nPaginated set of `CustomDomainResource`", "content": {"application/json": {"schema": {"anyOf": [{"type": "array", "items": {"$ref": "#/components/schemas/CustomDomainResource"}}, {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/CustomDomainResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage"], "order": "id ascendente", "sortable": []}, "x-fc-proposito": "Usalo para ver tus dominios propios con su estado, el SSL y los registros DNS que tenés que cargar.", "x-fc-domain": "accounts"}, "post": {"operationId": "customDomain.store", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\n`StoreCustomDomainRequest` already sanitized `hostname` and validated\nshape + uniqueness (422, no Cloudflare call) before this method runs.\nCloudflare is called next; on failure NOTHING is persisted locally\n(spec scenario \"Cloudflare API failure on creation\").\n\nRequests `HTTP` DCV by default (design.md D9 — supersedes the original\nTXT-only MVP default): `CloudflareCustomHostnameClient::create()`\ndefaults to `SslValidationMethod::Http`, so no explicit method is passed\nhere. With HTTP DCV the certificate is issued once the CNAME resolves,\nso `ssl_validation_string` (TXT-specific) is expected to stay `null` for\nnew domains — `DnsRecordBuilder` renders a single routing record (no\nTXT) accordingly. A domain can later fall back to `TXT` via\n`switchValidationMethod()`.\n\nAlso autodetects the DNS provider from the hostname's nameservers\n(design.md D14-D17, Increment 6) and persists it as `detected_provider`\n— a best-effort UX hint, never blocking creation. `null` when detection\nfails or the domain was just registered and its NS do not resolve yet\n(the \"sticky\" re-detection on `verify()` covers that case).\n\nSince Increment 9 (design.md Addendum 5 D32/D34), a Custom Hostname\nalone gets a certificate but routes no traffic — a **Worker Route**\n(`{hostname}/*` -> the `custom-hostnames` Worker) is required for the\ndomain to actually work. It is created right after the Custom Hostname,\nusing the script name from `config('services.cloudflare.worker_script')`\n(D36), and its id is persisted as `cloudflare_route_id` (internal\nbookkeeping only — never exposed via `CustomDomainResource`). If the\nCustom Hostname succeeds but the Worker Route fails, the just-created\nCustom Hostname is rolled back (best-effort delete, logged on failure)\nand NOTHING is persisted locally — a domain with a hostname but no route\nwould be a silently dead domain (SSL may go ACTIVE but no traffic ever\nreaches the resolver), which is worse than failing the request outright.", "summary": "Registrar un dominio propio", "tags": ["CustomDomain"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreCustomDomainRequest"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para registrar un dominio propio, por ejemplo para los links de tus campañas. Después tenés que cargar los registros DNS que indica para que se valide.", "x-fc-domain": "accounts"}}, "/api/v1/custom-domains/{customDomain}/verify": {"post": {"operationId": "customDomain.verify", "description": "Actively re-trigger Cloudflare Domain Control Validation for a domain\nand persist the resolved canonical status (\"verify now\", design.md\nD4/D6/D10)\n\n**Escritura** · requiere el permiso `accounts:write`.\n\nScoped to the authenticated admin: a domain id belonging to another\naccount resolves to a `ModelNotFoundException`, which the app's global\nexception handler (`bootstrap/app.php`) renders as a 404 JSON response\nfor API requests — same behavior a mismatched implicit route-model\nbinding would produce.\n\nConfirmed against a live Cloudflare response: a plain `GET` never\nre-kicks validation, so a customer who just published DNS would see\n\"nothing happen\" until Cloudflare's own background re-check eventually\nran. This method issues a `PATCH` (via `patchSsl()`) with the domain's\nCURRENT `ssl_validation_method` — a re-trigger, not a method switch —\nwhich Cloudflare confirmed re-kicks the DCV check instantly. The\nresulting state is still asynchronous: it may not reflect `ACTIVE` in\nthis same response, but is captured here or on a subsequent\nverify/sweep.\n\nIf Cloudflare is unreachable or rejects the request, the domain's\npersisted `status` is left untouched and a 502 is returned (the\n`CloudflareApiException` thrown by the client already logs the raw\nerror before this method ever sees it).\n\n`ssl_validation_string` is TXT-specific (design.md D9): it is only\nre-captured from the response when the domain's method is `TXT`. For\nthe `HTTP` default it stays `null`, matching `DnsRecordBuilder`'s\nmethod-aware branching (design.md D12).\n\nAlso re-detects the DNS provider hint (design.md D15, Increment 6)\nfollowing a STICKY policy: `detected_provider` is only overwritten\nwhen the detector returns a non-null value. A transient `null` (e.g. a\nflaky lookup) never clobbers a previously good hint. This covers the\ncommon case where NS did not resolve yet at `store()` time (freshly\nregistered domain) but do resolve by the time the admin verifies.", "summary": "Verificar un dominio propio", "tags": ["CustomDomain"], "parameters": [{"name": "customDomain", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "`CustomDomainResource`", "content": {"application/json": {"schema": {"anyOf": [{"type": "object"}, {"$ref": "#/components/schemas/CustomDomainResource"}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Usalo después de publicar los registros DNS para forzar ya la validación del dominio, en lugar de esperar al chequeo automático.", "x-fc-domain": "accounts"}}, "/api/v1/custom-domains/{customDomain}/switch-validation": {"post": {"operationId": "customDomain.switchValidationMethod", "description": "Switch a domain's SSL Domain Control Validation (DCV) method between\n`HTTP` (default) and `TXT` (fallback, design.md D11)\n\n**Escritura** · requiere el permiso `accounts:write`.\n\nThis is the escape hatch for domains whose CNAME does not yet route to\nCloudflare: HTTP DCV cannot complete without traffic reaching the\norigin, but TXT DCV validates purely from a DNS record. Scoped to the\nauthenticated admin via `findOwned()` (404 if not owned). Calls\n`patchSsl()` with the NEW method (a \"switch\", as opposed to `verify()`'s\nsame-method \"re-trigger\") and persists it. When switching TO `TXT`, the\n`ssl_validation_string` is captured best-effort from the response —\nCloudflare frequently has not yet populated `ssl.validation_records` at\nPATCH time (confirmed against a live Cloudflare response: it is filled\nin asynchronously, a few seconds later). When the PATCH response comes\nback empty, a best-effort follow-up `get()` is issued immediately to try\nto capture it right away — this covers the common case where the value\nis already available a moment later, so the customer sees the TXT\nrecord without having to wait for a subsequent `verify()`/sweep. The\nfollow-up is best-effort: if it also comes back empty, or Cloudflare\nfails/is unreachable for it, the switch still succeeds and a later\n`verify()`/sweep captures the value once available (bug fix: the TXT\n`dns_record` was never shown to the user when the PATCH raced\nCloudflare's population of `ssl.validation_records`).", "summary": "Cambiar el método de validación SSL", "tags": ["CustomDomain"], "parameters": [{"name": "customDomain", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/SwitchValidationMethodRequest"}}}}, "responses": {"200": {"description": "`CustomDomainResource`", "content": {"application/json": {"schema": {"anyOf": [{"type": "object"}, {"$ref": "#/components/schemas/CustomDomainResource"}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para cambiar el método de validación SSL entre HTTP y TXT; usá TXT cuando el CNAME todavía no apunta y la validación por HTTP no puede completarse.", "x-fc-domain": "accounts"}}, "/api/v1/custom-domains/{customDomain}": {"delete": {"operationId": "customDomain.destroy", "description": "Unregister a custom domain: delete it from Cloudflare, then hard-delete\nthe local row (design.md D6)\n\n**Escritura** · requiere el permiso `accounts:operate`.\n\n> ⚠️ **Operación irreversible.** No hay papelera ni deshacer: resetear o limpiar un número mata su sesión de WhatsApp (hay que volver a escanear el código), purgar la cola descarta los mensajes encolados sin dejar registro de lo descartado, y los borrados eliminan el recurso.\n\nBy design the local row is removed even when the Cloudflare delete\ncall fails (`communities` must stop treating the hostname as\nresolvable immediately) — the failure is logged with the\n`cloudflare_id` for later manual cleanup of the orphaned Custom\nHostname (design.md Risks: \"Cloudflare falla el DELETE pero borramos\n     * local\"). `CloudflareCustomHostnameClient::delete()` already logs the\nraw Cloudflare error before throwing; this method logs the follow-up\ndecision to proceed with the local deletion anyway.\n\nSince Increment 9 (design.md Addendum 5 D35), the Worker Route created\nin `store()` is also torn down here, best-effort and INDEPENDENTLY from\nthe Custom Hostname delete (each in its own try/catch — one failing\nnever prevents attempting the other). Guarded by `cloudflare_route_id\n!== null` for any row without one (e.g. a theoretical pre-Increment-9\nrow). The local row is hard-deleted UNCONDITIONALLY, whether or not\neither Cloudflare call succeeded — orphaned Cloudflare resource ids are\nlogged for later manual cleanup.", "summary": "Eliminar un dominio propio", "tags": ["CustomDomain"], "parameters": [{"name": "customDomain", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"204": {"description": "No content"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:operate", "x-fc-destructive": true, "x-fc-proposito": "Usalo para dar de baja un dominio propio: deja de usarse de inmediato y no se puede deshacer. Requiere el permiso `accounts:operate`.", "x-fc-domain": "accounts"}}, "/api/v1/devices": {"get": {"operationId": "device.index", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25) · orden fijo: id ascendente · no acepta parámetros de orden.", "tags": ["Device"], "responses": {"200": {"description": "Array of `DeviceResource`\n\nPaginated set of `DeviceResource`", "content": {"application/json": {"schema": {"anyOf": [{"type": "array", "items": {"$ref": "#/components/schemas/DeviceResource"}}, {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/DeviceResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}]}}}}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage"], "order": "id ascendente", "sortable": []}, "summary": "Listar números", "x-fc-proposito": "Sirve para listar tus números de WhatsApp con su proveedor, si están conectados y activos y su estado de salud, y obtener los IDs que usan las demás operaciones.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/me": {"get": {"operationId": "device.me", "description": "**Lectura** · requiere el permiso `accounts:read`.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"500": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"anyOf": [{"type": "string"}, {"type": "string", "enum": ["An error has occurred"]}]}}, "required": ["message"]}}}}, "409": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "El número no está conectado a WhatsApp. Conectalo desde el panel de Funnelchat y volvé a intentar."}, "error": {"type": "string", "const": "device_not_connected"}}, "required": ["message", "error"]}}}}, "200": {"description": "", "content": {"application/json": {"schema": {"type": "null"}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "summary": "Ver el perfil de WhatsApp de un número", "x-fc-proposito": "Usalo para leer el perfil de WhatsApp de uno de tus números tal como lo informa el proveedor.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/check-phone": {"get": {"operationId": "device.checkPhone", "description": "**Lectura** · requiere el permiso `accounts:read`.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}, {"name": "phone", "in": "query", "required": true, "schema": {"type": "string"}}], "responses": {"500": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"anyOf": [{"type": "string"}, {"type": "string", "enum": ["An error has occurred"]}]}}, "required": ["message"]}}}}, "409": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "El número no está conectado a WhatsApp. Conectalo desde el panel de Funnelchat y volvé a intentar."}, "error": {"type": "string", "const": "device_not_connected"}}, "required": ["message", "error"]}}}}, "200": {"description": "", "content": {"application/json": {"schema": {"type": "null"}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "summary": "Verificar si un teléfono tiene WhatsApp", "x-fc-proposito": "Sirve para validar si un teléfono tiene WhatsApp antes de cargarlo como contacto o escribirle, y así evitar envíos fallidos.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}": {"get": {"operationId": "device.show", "description": "**Lectura** · requiere el permiso `accounts:read`.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "`DeviceResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DeviceResource"}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "summary": "Ver un número", "x-fc-proposito": "Usalo para ver los datos de un número: alias, proveedor, WABA, nombre de usuario, si está conectado y su estado de salud.", "x-fc-domain": "accounts"}, "put": {"operationId": "device.update", "description": "**Escritura** · requiere el permiso `accounts:write`.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"alias": {"type": ["string", "null"]}, "color": {"type": ["string", "null"]}, "image": {"type": ["string", "null"], "format": "uri"}, "number": {"type": ["string", "null"]}, "first_conecction_at": {"type": ["string", "null"], "format": "date-time"}, "is_business": {"type": "boolean"}, "phone_number_id": {"type": "string"}, "waba_id": {"type": "string"}, "code": {"type": "string"}, "whatsApp_business_app": {"type": "boolean"}, "capi_consent": {"type": "boolean", "description": "Meta Conversions API (CAPI) opt-in consent — capi-meta-connection Inc 3\n(design.md D3). Optional: only meaningful on the Meta branch, and even\nthere it is a no-op unless the `capi` feature flag is enabled for the\naccount (defense in depth, see updateMetaDevice below)."}}}}}}, "responses": {"400": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "const": "Unsupported provider"}}, "required": ["error"]}}}}, "200": {"description": "`DeviceResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DeviceResource"}}}}, "409": {"description": "", "content": {"application/json": {"schema": {"type": "array", "items": {}}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "summary": "Actualizar un número", "x-fc-proposito": "Sirve para actualizar los datos de un número (alias, color, imagen) o completar sus datos de WhatsApp Business, como WABA y phone number ID, y el consentimiento de Conversions API.", "x-fc-domain": "accounts"}, "delete": {"operationId": "device.destroy", "description": "`DELETE /api/devices/{device}` (whatsapp-preverified-number-assignment D24; spec\n`device-number-release`, \"Deleting a device is an explicit release of that row\"). Same\nauthorization and routes as before (owner; API keys with `operate`; `can:payment-valid`).\nBoth modes. Inside the account section: 409 `operation_in_progress` while a Meta signup\nholds a live number reservation on the row; otherwise a `deleted` audit event (actor\n`user:{id}`, `source: api_key` for an API key; it survives the deletion) and the physical\ndeletion. After the commit: best-effort provider teardown (QR `unsubscribe()`, Meta webhook\nunsubscription, never `/deregister`) and 1:1 back on for the QR sibling. A connected row may\nstill be deleted. A teardown failure is logged and does not fail the request (200)\n\n**Escritura** · requiere el permiso `accounts:operate`.\n\n> ⚠️ **Operación irreversible.** No hay papelera ni deshacer: resetear o limpiar un número mata su sesión de WhatsApp (hay que volver a escanear el código), purgar la cola descarta los mensajes encolados sin dejar registro de lo descartado, y los borrados eliminan el recurso.", "summary": "Eliminar un número", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": ""}, "409": {"description": "", "content": {"application/json": {"schema": {"type": "array", "items": {}}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:operate", "x-fc-destructive": true, "x-fc-proposito": "Usalo para dar de baja un número de la cuenta de forma definitiva. Es irreversible y requiere el permiso `accounts:operate`.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/reset": {"put": {"operationId": "device.reset", "description": "\"Borrar datos\" (whatsapp-preverified-number-assignment D60; spec `device-connection-actions`):\nerases every column of the number on this connection, QR and Meta, both modes, so the card\nconnects a DIFFERENT number (normal free-card rules: class A when the pool is full). Kept:\nalias (and color; Meta also image and `is_coexistence`), `active`, `is_additional`,\n`provider`. See `EraseDeviceNumberData`. 409 `device_retired` / `operation_in_progress`\n(nothing erased). 200 `{data: DeviceResource, numbers_capacity}`. The front asks for a red\nconfirmation before calling it\n\n**Escritura** · requiere el permiso `accounts:operate`.\n\n> ⚠️ **Operación irreversible.** No hay papelera ni deshacer: resetear o limpiar un número mata su sesión de WhatsApp (hay que volver a escanear el código), purgar la cola descarta los mensajes encolados sin dejar registro de lo descartado, y los borrados eliminan el recurso.", "summary": "Reiniciar la sesión de un número", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {}}, "numbers_capacity": {"type": ["string", "null"]}}, "required": ["data", "numbers_capacity"]}}}}, "409": {"description": "", "content": {"application/json": {"schema": {"type": "array", "items": {}}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:operate", "x-fc-destructive": true, "x-fc-proposito": "Sirve para reiniciar desde cero la sesión de WhatsApp de un número; después hay que volver a escanear el código QR. Es irreversible y requiere `accounts:operate`.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/update-webhook-received": {"put": {"operationId": "device.updateWebhookReceived", "description": "**Escritura** · requiere el permiso `accounts:write`.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "summary": "Configurar el webhook de mensajes recibidos", "x-fc-proposito": "Sirve para configurar en un número el aviso de mensajes recibidos que usa Funnelchat para recibirlos.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/update-webhook-received-and-delivery": {"put": {"operationId": "device.updateWebhookReceivedAndDelivery", "description": "**Escritura** · requiere el permiso `accounts:write`.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "summary": "Configurar el webhook de recibidos y entregas", "x-fc-proposito": "Sirve para configurar en un número los avisos de mensajes recibidos y de confirmaciones de entrega que usa Funnelchat.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/clean": {"post": {"operationId": "device.clean", "description": "\"Desconectar\" (whatsapp-preverified-number-assignment D60; spec `device-connection-actions`):\nnever erases the number information and never frees the place, QR and Meta, both modes.\n - QR: provider logout of the WhatsApp session; the instance (`uid`, `token`) stays so the\n   user scans again; alias, color, image, app name, identity and history are kept.\n - Meta: the QR sibling's one-to-one is turned back on FIRST (synchronously, while the row\n   still has its number, D60.7); then only the access token is cleared (no provider call,\n   `status()` answers `pending_login`, D60.10); `uid` (phone_number_id),\n   `waba_id`, identity and `is_coexistence` are kept, so the same number reconnects with\n   Embedded Signup on the same row (also at or over the pool: D59 S1 / D20 rule 4).\n`number` goes NULL as before (the card shows `number_key`). 409 `device_retired` on a\nretired row; 409 `operation_in_progress` while a Meta signup holds a number reservation.\n200 `{data: DeviceResource, numbers_capacity}` (`numbers_capacity` omitted when it cannot\nbe computed, D59.12)\n\n**Escritura** · requiere el permiso `accounts:operate`.\n\n> ⚠️ **Operación irreversible.** No hay papelera ni deshacer: resetear o limpiar un número mata su sesión de WhatsApp (hay que volver a escanear el código), purgar la cola descarta los mensajes encolados sin dejar registro de lo descartado, y los borrados eliminan el recurso.", "summary": "Limpiar un número", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {}}, "numbers_capacity": {"type": ["string", "null"]}}, "required": ["data", "numbers_capacity"]}}}}, "409": {"description": "", "content": {"application/json": {"schema": {"type": "array", "items": {}}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:operate", "x-fc-destructive": true, "x-fc-proposito": "Sirve para limpiar la sesión de WhatsApp de un número: después hay que volver a escanear el código QR. Es irreversible y requiere `accounts:operate`.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/chats": {"get": {"operationId": "device.chats", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25); también `pageSize` · orden fijo: el que devuelve el proveedor · no acepta parámetros de orden.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}, {"name": "phone", "in": "query", "schema": {"type": "string"}}, {"name": "page", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "pageSize", "in": "query", "schema": {"type": "integer", "minimum": 1}}, {"name": "type", "in": "query", "schema": {"type": "integer", "enum": ["1", "2"]}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"phones": {"type": "array", "items": {}}, "total": {"type": "integer"}, "page": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [1]}]}, "per_page": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [25]}]}, "last_page": {"type": "number"}}, "required": ["phones", "total", "page", "per_page", "last_page"]}, {"type": "null"}]}}}}, "500": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"anyOf": [{"type": "string"}, {"type": "string", "enum": ["An error has occurred"]}]}}, "required": ["message"]}}}}, "409": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "El número no está conectado a WhatsApp. Conectalo desde el panel de Funnelchat y volvé a intentar."}, "error": {"type": "string", "const": "device_not_connected"}}, "required": ["message", "error"]}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage", "pageSize"], "order": "el que devuelve el proveedor", "sortable": []}, "summary": "Listar los chats de un número", "x-fc-proposito": "Usalo para listar los chats que tiene un número en WhatsApp según el proveedor, con búsqueda por teléfono y filtro por tipo.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/open-conversations": {"get": {"operationId": "device.openConversations", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\n**Con API key** · Con una clave de API, `phones` admite como máximo 100 teléfonos por llamada. Más responde `422`.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}, {"name": "phones[]", "in": "query", "required": true, "schema": {"type": "array", "items": {"type": "string"}}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": ["object", "array"]}}}}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "summary": "Ver qué teléfonos tienen conversación abierta", "x-fc-proposito": "Sirve para chequear en lote, hasta 100 teléfonos por llamada, cuáles tienen una conversación abierta con un número, por ejemplo antes de decidir cómo contactarlos.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/message-queue": {"get": {"operationId": "device.messageQueue", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25); también `pageSize` · orden fijo: el que devuelve el proveedor · no acepta parámetros de orden.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}, {"name": "page", "in": "query", "schema": {"type": "integer"}}, {"name": "pageSize", "in": "query", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": ["array", "object"]}}}}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage", "pageSize"], "order": "el que devuelve el proveedor", "sortable": []}, "summary": "Ver la cola de mensajes", "x-fc-proposito": "Usalo para ver los mensajes que esperan en la cola de un número, por ejemplo para entender por qué algo todavía no salió.", "x-fc-domain": "accounts"}, "delete": {"operationId": "device.deleteMessageQueue", "description": "**Escritura** · requiere el permiso `accounts:write`.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}, {"name": "messageUids[]", "in": "query", "required": true, "schema": {"type": "array", "items": {"type": "string"}, "minItems": 1}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": ["object", "array"]}}}}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "summary": "Eliminar mensajes de la cola", "x-fc-proposito": "Sirve para sacar de la cola mensajes puntuales, por su UID, antes de que se envíen, por ejemplo uno que se encoló por error.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/message-queue/count": {"get": {"operationId": "device.getQueueCount", "description": "**Lectura** · requiere el permiso `accounts:read`.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": ["array", "object"]}}}}, "502": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Upstream gateway error"}}, "required": ["message"]}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "summary": "Contar los mensajes en la cola", "x-fc-proposito": "Usalo para monitorear cuántos mensajes tiene encolados un número, por ejemplo para detectar atascos.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/message-queue/all": {"delete": {"operationId": "device.clearQueue", "description": "**Escritura** · requiere el permiso `accounts:operate`.\n\n> ⚠️ **Operación irreversible.** No hay papelera ni deshacer: resetear o limpiar un número mata su sesión de WhatsApp (hay que volver a escanear el código), purgar la cola descarta los mensajes encolados sin dejar registro de lo descartado, y los borrados eliminan el recurso.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": ["array", "object"]}}}}, "204": {"description": "No content"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:operate", "x-fc-destructive": true, "summary": "Vaciar la cola de mensajes", "x-fc-proposito": "Sirve para vaciar toda la cola de un número cuando hay que frenar envíos acumulados. Descarta los mensajes sin dejar registro; es irreversible y requiere `accounts:operate`.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/status": {"get": {"operationId": "device.status", "description": "**Lectura** · requiere el permiso `accounts:read`.", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "summary": "Ver el estado de conexión", "x-fc-proposito": "Usalo para chequear el estado de conexión de un número, por ejemplo antes de lanzar envíos o para monitorear tus números.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/extension-token": {"post": {"operationId": "device.extensionToken", "description": "Request a short-lived extension token for transferring a WhatsApp Web session\nto this device instance via the Z-API Conector Chrome extension\n\n**Escritura** · requiere el permiso `accounts:write`.\n\nOnly available for ZApi and ZApiLite providers. Returns a one-time code with\nan expiry timestamp. The frontend uses this code to authenticate the extension\nwithout ever receiving the raw instance credentials.", "summary": "Pedir un token para la extensión de WhatsApp Web", "tags": ["Device"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "`ExtensionTokenResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/ExtensionTokenResource"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para obtener un código de un solo uso y corta duración con el que la extensión de Chrome Z-API Conector transfiere una sesión de WhatsApp Web a este número. Solo para números Z-API.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/connection-history": {"get": {"operationId": "deviceConnectionEvent.index", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25) · orden fijo: created_at descendente · no acepta parámetros de orden.", "tags": ["DeviceConnectionEvent"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}, {"name": "orderBy", "in": "query", "schema": {"type": "string"}}, {"name": "orderByMethod", "in": "query", "schema": {"type": "string"}}, {"name": "perPage", "in": "query", "schema": {"type": "integer"}}], "responses": {"200": {"description": "Paginated set of `DeviceConnectionEventResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/DeviceConnectionEventResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage"], "order": "created_at descendente", "sortable": []}, "summary": "Ver el historial de conexión", "x-fc-proposito": "Usalo para ver cuándo se conectó y desconectó un número, del evento más reciente al más antiguo, por ejemplo para investigar cortes.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/disconnect-survey": {"get": {"operationId": "deviceDisconnectSurvey.show", "description": "**Lectura** · requiere el permiso `accounts:read`.", "tags": ["DeviceDisconnectSurvey"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "`DisconnectSurveyResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DisconnectSurveyResource"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "summary": "Ver la encuesta de desconexión", "x-fc-proposito": "Sirve para saber si un número tiene pendiente la encuesta sobre el motivo de su desconexión.", "x-fc-domain": "accounts"}, "post": {"operationId": "deviceDisconnectSurvey.store", "description": "**Escritura** · requiere el permiso `accounts:write`.", "tags": ["DeviceDisconnectSurvey"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreDisconnectReasonRequest"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "summary": "Responder la encuesta de desconexión", "x-fc-proposito": "Usalo para registrar por qué se desconectó un número, respondiendo la encuesta de desconexión con uno de los motivos predefinidos.", "x-fc-domain": "accounts"}}, "/api/v1/devices/logs": {"get": {"operationId": "deviceFleetLogs.index", "description": "List `device_logs` rows across the account within a bounded window,\nnewest-first, keyset-paginated. Returns a `summary` only on the first\npage (no `cursor`)\n\n**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25); también `limit` · orden fijo: más reciente primero · no acepta parámetros de orden.", "summary": "Listar los registros de todos los números", "tags": ["DeviceFleetLogs"], "parameters": [{"name": "range", "in": "query", "schema": {"type": ["string", "null"], "enum": ["1h", "6h", "24h", "7d", "30d", null]}}, {"name": "from", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "to", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "only_errors", "in": "query", "schema": {"type": ["boolean", "null"]}}, {"name": "exclude_sends", "in": "query", "schema": {"type": ["boolean", "null"]}}, {"name": "methods[]", "in": "query", "schema": {"type": "array", "items": {"type": "string", "maxLength": 64}}}, {"name": "cursor", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "limit", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1, "maximum": 500}}, {"name": "per_device_cap", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1, "maximum": 150}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/DeviceFleetLogResource"}}, "devices": {"type": "array", "items": {"type": "object", "properties": {"device_id": {"type": "integer"}, "label": {"type": "string"}, "connected": {"type": "boolean"}, "activity_count": {"type": "integer"}, "truncated": {"type": ["string", "boolean"]}}, "required": ["device_id", "label", "connected", "activity_count", "truncated"]}}, "next_cursor": {"type": "string"}, "has_more": {"type": "string"}, "window": {"type": "object", "properties": {"from": {"type": "string"}, "to": {"type": "string"}}, "required": ["from", "to"]}, "summary": {"type": "string"}}, "required": ["data", "devices", "next_cursor", "has_more", "window", "summary"]}}}}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage", "limit"], "order": "más reciente primero", "sortable": []}, "x-fc-proposito": "Sirve para revisar en un solo lugar los registros de actividad de todos tus números (método, URL, código HTTP, duración, errores) en una ventana de tiempo. Ideal para diagnosticar fallas.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/health": {"get": {"operationId": "deviceHealth.show", "description": "`GET /devices/{device}/health` — serves `currentSnapshot()`\nnormalized through `DeviceHealthDetailResource`. NEVER calls Meta\n\n**Lectura** · requiere el permiso `accounts:read`.", "summary": "Ver la salud de un número", "tags": ["DeviceHealth"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "`DeviceHealthDetailResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/DeviceHealthDetailResource"}}}}, "422": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "const": "device_not_meta"}, "message": {"type": "string", "const": "Este dispositivo no usa la API de WhatsApp Cloud de Meta."}}, "required": ["error", "message"]}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-proposito": "Usalo para ver la salud de un número de WhatsApp Cloud (estado, identidad, nivel de throughput e historial de cambios) a partir del último dato guardado, sin consultar a Meta.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/health/refresh": {"post": {"operationId": "deviceHealth.refresh", "description": "`POST /devices/{device}/health/refresh` — triggers\n`MetaHealthStatusService::refreshAndPersist()` (at most ONE live Meta\ncall, subject to D7's 60s cooldown and per-device `Cache::lock`) and\nreturns the SAME shape as `show()`, plus `refresh_outcome`\n\n**Escritura** · requiere el permiso `accounts:write`.\n\n`refresh_outcome` (security-auditor finding — Increment 4 plan\nreview, \"the endpoint must tell the truth about what happened\"):\n`refreshAndPersist()` can return the pre-existing snapshot for THREE\ndifferent reasons — it genuinely refreshed, it bounced off the 60s\ncooldown, or it couldn't acquire the per-device lock (another refresh\n— the scheduled command, another tab, another agent — is already in\nflight for this exact device) — and from the HTTP response alone\nthose three are indistinguishable. A 200 with stale data and no\nsignal after the user clicks \"Actualizar ahora\" is a silent lie.\n\nResolution: compare `last_fetched_at` BEFORE and AFTER the call.\n`refreshAndPersist()` ALWAYS advances `last_fetched_at` when it\ngenuinely attempts Meta — success or failure alike (design.md D7:\nit's attempt metadata, not a health field) — and NEVER advances it on\neither bounce. So:\n  - unchanged (or still null) → `'not_refreshed_yet'` — covers BOTH\n    the cooldown bounce and the lock-contention bounce; the frontend\n    doesn't need to tell them apart to render \"esperá un momento\",\n    and collapsing them avoids inventing a distinction the service's\n    own public contract (`refreshAndPersist(): DeviceHealthStatus`,\n    Increment 1, already covered by 40+ tests) doesn't expose without\n    a breaking change.\n  - advanced → `'refreshed'` — a live call was actually dispatched,\n    regardless of whether Meta answered with success or an error\n    (that distinction is already visible via `health.state`/`stale`).", "summary": "Actualizar la salud de un número", "tags": ["DeviceHealth"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"health": {"type": "string"}, "identity": {"type": "string"}, "entities": {"type": "string"}, "throughput_level": {"type": "string", "description": "Detail-only (meta-number-health-status, Increment 6 gap-closing\nfollow-up) — deliberately NOT part of `health`, which the\nlisting shares verbatim; see\nDeviceHealthSummaryBuilder::buildThroughputLevel()."}, "history": {"type": "array", "items": {"type": "object", "properties": {"field": {"type": "string"}, "previous_value": {"type": "string"}, "new_value": {"type": "string"}, "detail": {"type": ["string", "null"]}, "source": {"type": "string"}, "occurred_at": {"type": "string"}}, "required": ["field", "previous_value", "new_value", "detail", "source", "occurred_at"]}}, "meta_billing": {"type": ["object", "null"], "properties": {"waba_id": {"type": ["string", "null"]}, "business_id": {"type": ["string", "null"]}}, "required": ["waba_id", "business_id"]}, "refresh_outcome": {"type": "string", "enum": ["refreshed", "not_refreshed_yet"]}}, "required": ["health", "identity", "entities", "throughput_level", "history", "meta_billing", "refresh_outcome"]}}}}, "422": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string", "const": "device_not_meta"}, "message": {"type": "string", "const": "Este dispositivo no usa la API de WhatsApp Cloud de Meta."}}, "required": ["error", "message"]}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para pedirle a Meta la salud actualizada de un número; la respuesta indica si se refrescó de verdad o si devolvió el dato previo por la espera de 60 segundos u otra actualización en curso.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/logs": {"get": {"operationId": "deviceLogs.index", "description": "List `device_logs` rows for the device within a bounded window,\nnewest-first, keyset-paginated. Returns a `summary` only on the first\npage (no `cursor`)\n\n**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25); también `limit` · orden fijo: más reciente primero · no acepta parámetros de orden.", "summary": "Listar los registros de un número", "tags": ["DeviceLogs"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}, {"name": "range", "in": "query", "schema": {"type": ["string", "null"], "enum": ["1h", "6h", "24h", "7d", "30d", null]}}, {"name": "from", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "to", "in": "query", "schema": {"type": ["string", "null"], "format": "date-time"}}, {"name": "only_errors", "in": "query", "schema": {"type": ["boolean", "null"]}}, {"name": "exclude_sends", "in": "query", "schema": {"type": ["boolean", "null"]}}, {"name": "methods[]", "in": "query", "schema": {"type": "array", "items": {"type": "string", "maxLength": 64}}}, {"name": "cursor", "in": "query", "schema": {"type": ["string", "null"]}}, {"name": "limit", "in": "query", "schema": {"type": ["integer", "null"], "minimum": 1, "maximum": 500}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/DeviceLogResource"}}, "next_cursor": {"type": "string"}, "has_more": {"type": "string"}, "window": {"type": "object", "properties": {"from": {"type": "string"}, "to": {"type": "string"}}, "required": ["from", "to"]}, "summary": {"anyOf": [{"type": "object", "properties": {"total": {"type": "integer"}, "errors": {"type": "integer"}}, "required": ["total", "errors"]}, {"type": "object", "properties": {"total": {"type": "integer", "const": 0}, "errors": {"type": "integer", "const": 0}}, "required": ["total", "errors"]}]}}, "required": ["data", "next_cursor", "has_more", "window", "summary"]}}}}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage", "limit"], "order": "más reciente primero", "sortable": []}, "x-fc-proposito": "Usalo para depurar un número puntual: ves sus registros de actividad con método, código HTTP, duración y errores en una ventana de tiempo, del más reciente al más antiguo.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/logs/health": {"get": {"operationId": "deviceLogs.health", "description": "Return the device's health signal: the last 5 non-send requests plus a\nverdict (`healthy`/`review`/`attention`)\n\n**Lectura** · requiere el permiso `accounts:read`.", "summary": "Ver la señal de salud de los registros", "tags": ["DeviceLogs"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"verdict": {"type": "string"}, "fail_count": {"type": "string"}, "sample_size": {"type": "string"}, "requests": {"type": "array", "items": {"$ref": "#/components/schemas/DeviceLogResource"}}}, "required": ["verdict", "fail_count", "sample_size", "requests"]}}}}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-proposito": "Sirve para hacer un chequeo rápido de un número: mira las últimas 5 peticiones que no son envíos y devuelve un veredicto (sano, revisar o atención).", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/username": {"get": {"operationId": "deviceUsername.show", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\nFetch the current reserved username from Meta and return it along with the locally stored value.", "summary": "Ver el nombre de usuario", "tags": ["DeviceUsername"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"username": {}}, "required": ["username"]}, {"type": "string"}]}}}}, "422": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"error": {"type": "string", "const": "El dispositivo no tiene credenciales Meta configuradas."}}, "required": ["error"]}, {"type": "object", "properties": {"error": {"type": "string", "const": "Solo disponible para dispositivos WhatsApp Cloud API."}}, "required": ["error"]}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-proposito": "Usalo para consultar el nombre de usuario de WhatsApp reservado para un número, tanto el que informa Meta como el guardado en Funnelchat.", "x-fc-domain": "accounts"}, "post": {"operationId": "deviceUsername.store", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\nReserve a username via Meta API and persist it locally.", "summary": "Configurar el nombre de usuario", "tags": ["DeviceUsername"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "properties": {"username": {"type": "string", "pattern": "^[a-z0-9._]+$", "minLength": 3, "maxLength": 35}}, "required": ["username"]}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"username": {"type": "string"}, "message": {"type": "string", "const": "Nombre de usuario reservado exitosamente."}}, "required": ["username", "message"]}, {"type": "string"}]}}}}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para reservar en Meta un nombre de usuario de WhatsApp para uno de tus números.", "x-fc-domain": "accounts"}}, "/api/v1/devices/{device}/username/suggestions": {"get": {"operationId": "deviceUsername.suggestions", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\nFetch username suggestions from Meta API.", "summary": "Sugerir nombres de usuario", "tags": ["DeviceUsername"], "parameters": [{"name": "device", "in": "path", "required": true, "description": "The device ID", "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"suggestions": {}}, "required": ["suggestions"]}, {"type": "string"}]}}}}, "422": {"description": "", "content": {"application/json": {"schema": {"anyOf": [{"type": "object", "properties": {"error": {"type": "string", "const": "El dispositivo no tiene credenciales Meta configuradas."}}, "required": ["error"]}, {"type": "object", "properties": {"error": {"type": "string", "const": "Solo disponible para dispositivos WhatsApp Cloud API."}}, "required": ["error"]}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "404": {"$ref": "#/components/responses/AccountsModelNotFoundException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-proposito": "Usalo para pedirle a Meta sugerencias de nombre de usuario antes de reservar uno.", "x-fc-domain": "accounts"}}, "/api/v1/webhooks/endpoints/{id}/deliveries": {"get": {"operationId": "webhookDelivery.index", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25); también `per_page` · orden fijo: attempted_at descendente, después id descendente · no acepta parámetros de orden.", "tags": ["WebhookDelivery"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "Paginated set of `WebhookDeliveryResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/WebhookDeliveryResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Endpoint de webhook no encontrado."}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage", "per_page"], "order": "attempted_at descendente, después id descendente", "sortable": []}, "summary": "Listar entregas de un webhook", "x-fc-proposito": "Sirve para depurar un endpoint de webhooks: ves cada intento de entrega, del más reciente al más antiguo.", "x-fc-domain": "accounts"}}, "/api/v1/webhooks/endpoints": {"get": {"operationId": "webhookEndpoint.index", "description": "**Lectura** · requiere el permiso `accounts:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25) · orden fijo: id ascendente · no acepta parámetros de orden.", "summary": "Listar webhooks", "tags": ["WebhookEndpoint"], "responses": {"200": {"description": "Array of `WebhookEndpointResource`\n\nPaginated set of `WebhookEndpointResource`", "content": {"application/json": {"schema": {"anyOf": [{"type": "array", "items": {"$ref": "#/components/schemas/WebhookEndpointResource"}}, {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/WebhookEndpointResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage"], "order": "id ascendente", "sortable": []}, "x-fc-proposito": "Usalo para ver qué endpoints de webhooks tiene la cuenta, a qué eventos está suscripto cada uno, su estado y su salud.", "x-fc-domain": "accounts"}, "post": {"operationId": "webhookEndpoint.store", "description": "**Escritura** · requiere el permiso `accounts:write`.", "summary": "Crear un webhook", "tags": ["WebhookEndpoint"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreWebhookEndpointRequest"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para que tu sistema se entere cuando un número se conecta o se desconecta, sin consultar la API. Registrá una URL HTTPS y guardá el secreto: se muestra una sola vez.", "x-fc-domain": "accounts"}}, "/api/v1/webhooks/endpoints/{id}": {"get": {"operationId": "webhookEndpoint.show", "description": "**Lectura** · requiere el permiso `accounts:read`.", "tags": ["WebhookEndpoint"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "`WebhookEndpointResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookEndpointResource"}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Endpoint de webhook no encontrado."}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "summary": "Ver un webhook", "x-fc-proposito": "Usalo para ver la configuración de un endpoint: URL, eventos, estado, motivo de desactivación y cuándo se rotó el secreto.", "x-fc-domain": "accounts"}, "patch": {"operationId": "webhookEndpoint.update", "description": "**Escritura** · requiere el permiso `accounts:write`.\n\nEl `SsrfGuard` corre TAMBIÉN acá y no sólo en el alta: si corriera sólo en\nel alta, editar sería la puerta de atrás para dejar persistida una URL que\napunta a la red interna.", "summary": "Actualizar un webhook", "tags": ["WebhookEndpoint"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateWebhookEndpointRequest"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para cambiar la URL o la descripción de un endpoint, o para prenderlo y apagarlo entero sin perder sus suscripciones.", "x-fc-domain": "accounts"}, "delete": {"operationId": "webhookEndpoint.destroy", "description": "Da de baja el endpoint. Las suscripciones y el historial de entregas caen\ncon él por `ON DELETE CASCADE`\n\n**Escritura** · requiere el permiso `accounts:write`.", "summary": "Eliminar un webhook", "tags": ["WebhookEndpoint"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Endpoint de webhook no encontrado."}}, "required": ["message"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "x-fc-proposito": "Usalo para dar de baja un endpoint; se eliminan también sus suscripciones y su historial de entregas.", "x-fc-domain": "accounts"}}, "/api/v1/webhooks/endpoints/{id}/events": {"put": {"operationId": "webhookEndpointEvent.update", "description": "**Escritura** · requiere el permiso `accounts:write`.", "tags": ["WebhookEndpointEvent"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "integer"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateWebhookEndpointEventsRequest"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "accounts:write", "x-fc-destructive": false, "summary": "Elegir los eventos de un webhook", "x-fc-proposito": "Sirve para definir a qué tipos de evento queda suscripto un endpoint.", "x-fc-domain": "accounts"}}, "/api/v1/webhooks/events": {"get": {"operationId": "webhookEventCatalog.index", "description": "**Lectura** · requiere el permiso `accounts:read`.", "tags": ["WebhookEventCatalog"], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {}}, "meta": {"type": "object", "properties": {"total": {"type": "integer"}, "active": {"type": "integer"}, "envelope_version": {"type": "string", "description": "La versión del formato estándar, no la de la API HTTP.", "const": "v1"}, "signature_header": {"type": "string", "const": "X-Funnelchat-Signature"}, "signature_format": {"type": "string", "const": "t=<unix>,v1=<hex hmac-sha256 de \"<t>.<cuerpo crudo>\" con el secreto del endpoint>"}}, "required": ["total", "active", "envelope_version", "signature_header", "signature_format"]}}, "required": ["data", "meta"]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "summary": "Listar tipos de evento", "x-fc-proposito": "Usalo para conocer el catálogo de eventos de la cuenta y sus números, cuáles ya se emiten y cuáles están planificados, con un ejemplo del cuerpo de cada uno.", "x-fc-domain": "accounts"}}, "/api/v1/webhooks": {"get": {"operationId": "webhookSubscription.index", "description": "**Lectura** · requiere el permiso `webhooks:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25) · orden fijo: id ascendente · no acepta parámetros de orden.\n\nCon una clave de API, solo las que la clave puede leer: se omiten las que\ntienen eventos de un área cuyo permiso de lectura (`accounts:read`,\n`conversations:read`, `communities:read`) le falta.", "summary": "Listar webhooks", "tags": ["WebhookSubscription"], "responses": {"200": {"description": "Array of `WebhookSubscriptionResource`\n\nPaginated set of `WebhookSubscriptionResource`", "content": {"application/json": {"schema": {"anyOf": [{"type": "array", "items": {"$ref": "#/components/schemas/WebhookSubscriptionResource"}}, {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/WebhookSubscriptionResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}]}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "webhooks:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage"], "order": "id ascendente", "sortable": []}, "x-fc-proposito": "Usalo para revisar a qué URLs le está mandando eventos tu cuenta y en qué estado está cada suscripción.", "x-fc-domain": "accounts"}, "post": {"operationId": "webhookSubscription.store", "description": "**Escritura** · requiere el permiso `webhooks:write`.\n\nCon una clave de API, cada tipo de `events` exige además el permiso de\nlectura de su área (`accounts:read`, `conversations:read`,\n`communities:read`); si falta, 422.", "summary": "Crear un webhook", "tags": ["WebhookSubscription"], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/StoreWebhookSubscriptionRequest"}}}}, "responses": {"403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}, "201": {"description": "`WebhookSubscriptionCreatedResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookSubscriptionCreatedResource"}}}}}, "x-fc-access": "write", "x-fc-permission": "webhooks:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para empezar a recibir eventos en tu sistema: elegís la URL y los eventos de cualquiera de las tres áreas, y guardás el secreto de firma, que se muestra solo esta vez.", "x-fc-domain": "accounts"}}, "/api/v1/webhooks/{id}": {"get": {"operationId": "webhookSubscription.show", "description": "**Lectura** · requiere el permiso `webhooks:read`.\n\nCon una clave de API, la clave necesita además el permiso de lectura del\nárea de cada tipo de evento de la suscripción; si falta alguno, 403\n`permission_denied`.", "summary": "Ver un webhook", "tags": ["WebhookSubscription"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"description": "`WebhookSubscriptionResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WebhookSubscriptionResource"}}}}, "403": {"description": "La clave de API no tiene el permiso de lectura de un área de los eventos de la suscripción.", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}, "message": {"type": "string"}}, "required": ["error", "message"]}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Suscripción de webhook no encontrada."}}, "required": ["message"]}}}}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "webhooks:read", "x-fc-destructive": false, "x-fc-proposito": "Usalo para ver la configuración de una suscripción: URL, eventos, filtros y estado.", "x-fc-domain": "accounts"}, "put": {"operationId": "webhookSubscription.update", "description": "Reemplaza `url`, `events`, `description` y `filters`; `status` opcional\n\n**Escritura** · requiere el permiso `webhooks:write`.\n\nCon una clave de API, cada tipo de `events` exige además el permiso de\nlectura de su área (`accounts:read`, `conversations:read`,\n`communities:read`); si falta, 422.\n\nCon una clave de API, la clave necesita además el permiso de lectura del\nárea de cada tipo de evento que la suscripción tiene ahora; si falta alguno,\n403 `permission_denied` y no se cambia nada. Un cuerpo inválido responde 422\nantes de mirar la suscripción.\n\nSi una clave de API cambia `url`, la suscripción queda atada a esa clave:\ncuando se revoque, pasa a `disabled` / `key_revoked`.", "summary": "Actualizar un webhook", "tags": ["WebhookSubscription"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"$ref": "#/components/schemas/UpdateWebhookSubscriptionRequest"}}}}, "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object"}}}}, "403": {"description": "La clave de API no tiene el permiso de lectura de un área de los eventos de la suscripción.", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}, "message": {"type": "string"}}, "required": ["error", "message"]}}}}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}, "422": {"$ref": "#/components/responses/AccountsValidationException"}}, "x-fc-access": "write", "x-fc-permission": "webhooks:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para cambiar la URL, los eventos, la descripción o los filtros de una suscripción, o para pausarla y reactivarla.", "x-fc-domain": "accounts"}, "delete": {"operationId": "webhookSubscription.destroy", "description": "**Escritura** · requiere el permiso `webhooks:write`.\n\nCon una clave de API, la clave necesita además el permiso de lectura del\nárea de cada tipo de evento de la suscripción; si falta alguno, 403\n`permission_denied` y no se borra.", "summary": "Eliminar un webhook", "tags": ["WebhookSubscription"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"204": {"description": "No content", "content": {"application/json": {"schema": {"type": "null"}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Suscripción de webhook no encontrada."}}, "required": ["message"]}}}}, "409": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Esta suscripción se está activando; esperá a que termine."}}, "required": ["message"]}}}}, "403": {"description": "La clave de API no tiene el permiso de lectura de un área de los eventos de la suscripción.", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}, "message": {"type": "string"}}, "required": ["error", "message"]}}}}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "webhooks:write", "x-fc-destructive": false, "x-fc-proposito": "Usalo para dejar de recibir los eventos de una suscripción; se borran también su historial de entregas.", "x-fc-domain": "accounts"}}, "/api/v1/webhooks/{id}/deliveries": {"get": {"operationId": "webhookSubscriptionDelivery.index", "description": "**Lectura** · requiere el permiso `webhooks:read`.\n\n**Listado** · `perPage` de 1 a 100 (por defecto 25); también `per_page` · orden fijo: attempted_at descendente, después id descendente · no acepta parámetros de orden.\n\nCon una clave de API, la clave necesita además el permiso de lectura del\nárea de cada tipo de evento de la suscripción; si falta alguno, 403\n`permission_denied`.\n\nCon una clave de API, solo las entregas de tipos cuya área la clave puede\nleer; las de prueba, `webhook.test`, se muestran siempre.", "summary": "Listar entregas de un webhook", "tags": ["WebhookSubscriptionDelivery"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"description": "Paginated set of `WebhookSubscriptionDeliveryResource`", "content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "array", "items": {"$ref": "#/components/schemas/WebhookSubscriptionDeliveryResource"}}, "links": {"type": "object", "properties": {"first": {"type": ["string", "null"]}, "last": {"type": ["string", "null"]}, "prev": {"type": ["string", "null"]}, "next": {"type": ["string", "null"]}}, "required": ["first", "last", "prev", "next"]}, "meta": {"type": "object", "properties": {"current_page": {"type": "integer", "minimum": 1}, "from": {"type": ["integer", "null"], "minimum": 1}, "last_page": {"type": "integer", "minimum": 1}, "links": {"type": "array", "description": "Generated paginator links.", "items": {"type": "object", "properties": {"url": {"type": ["string", "null"]}, "label": {"type": "string"}, "active": {"type": "boolean"}}, "required": ["url", "label", "active"]}}, "path": {"type": ["string", "null"], "description": "Base path for paginator generated URLs."}, "per_page": {"type": "integer", "description": "Number of items shown per page.", "minimum": 0}, "to": {"type": ["integer", "null"], "description": "Number of the last item in the slice.", "minimum": 1}, "total": {"type": "integer", "description": "Total number of items being paginated.", "minimum": 0}}, "required": ["current_page", "from", "last_page", "links", "path", "per_page", "to", "total"]}}, "required": ["data", "links", "meta"]}}}}, "403": {"description": "La clave de API no tiene el permiso de lectura de un área de los eventos de la suscripción.", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}, "message": {"type": "string"}}, "required": ["error", "message"]}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Suscripción de webhook no encontrada."}}, "required": ["message"]}}}}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "webhooks:read", "x-fc-destructive": false, "x-fc-listing": {"per_page_default": 25, "per_page_max": 100, "size_params": ["perPage", "per_page"], "order": "attempted_at descendente, después id descendente", "sortable": []}, "x-fc-proposito": "Usalo para depurar tu receptor: ves cada intento de entrega de una suscripción, con su resultado.", "x-fc-domain": "accounts"}}, "/api/v1/webhooks/{id}/test": {"post": {"operationId": "webhookSubscriptionTest.test", "description": "Envía una entrega de prueba firmada a la URL de la suscripción y devuelve el resultado\n\n**Escritura** · requiere el permiso `webhooks:write`.\n\nSolo a suscripciones `active` o `paused`; una `disabled` responde 422 sin enviar nada.\n\nCon una clave de API, la clave necesita además el permiso de lectura del área\nde cada tipo de evento de la suscripción; si falta alguno, 403\n`permission_denied`, antes que el 422 de una `disabled`.", "summary": "Enviar un evento de prueba", "tags": ["WebhookSubscriptionTest"], "parameters": [{"name": "id", "in": "path", "required": true, "schema": {"type": "string"}}], "responses": {"200": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"succeeded": {"type": "boolean"}, "response_status": {"type": ["integer", "null"]}, "error": {"type": ["string", "null"], "enum": ["non_2xx", "internal_error", "timeout", "connection_failed", "tls_error", "destination_not_allowed", "secret_unreadable", null]}, "duration_ms": {"type": ["integer", "null"]}, "delivery_id": {"type": "integer"}}, "required": ["succeeded", "response_status", "error", "duration_ms", "delivery_id"]}}}}, "422": {"description": "La suscripción está deshabilitada: no se envía nada.", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string"}}, "required": ["message"]}}}}, "403": {"description": "La clave de API no tiene el permiso de lectura de un área de los eventos de la suscripción.", "content": {"application/json": {"schema": {"type": "object", "properties": {"error": {"type": "string"}, "message": {"type": "string"}}, "required": ["error", "message"]}}}}, "404": {"description": "", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "const": "Suscripción de webhook no encontrada."}}, "required": ["message"]}}}}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "write", "x-fc-permission": "webhooks:write", "x-fc-destructive": false, "x-fc-proposito": "Sirve para probar tu receptor y tu verificación de firma con una entrega firmada, sin esperar a que pase algo en tu cuenta.", "x-fc-domain": "accounts"}}, "/api/v1/me/whatsapp-costs": {"get": {"operationId": "whatsappCost.index", "description": "Returns the full `WhatsappCostSummaryResource` for the requested\n(or current, when omitted) calendar month — `?month=YYYY-MM`\n\n**Lectura** · requiere el permiso `accounts:read`.", "summary": "Ver los costos de WhatsApp", "tags": ["WhatsappCost"], "parameters": [{"name": "month", "in": "query", "schema": {"type": "string"}}], "responses": {"200": {"description": "`WhatsappCostSummaryResource`", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/WhatsappCostSummaryResource"}}}}, "403": {"$ref": "#/components/responses/AccountsAuthorizationException"}, "401": {"$ref": "#/components/responses/AccountsAuthenticationException"}}, "x-fc-access": "read", "x-fc-permission": "accounts:read", "x-fc-destructive": false, "x-fc-proposito": "Sirve para consultar cuánto gastaste en WhatsApp en un mes (el actual o el que indiques con `month`), por ejemplo para controlar costos o conciliar tu facturación.", "x-fc-domain": "accounts"}}}, "webhooks": {"message.received": {"post": {"summary": "message.received", "description": "Un contacto le escribió a un número de WhatsApp de la cuenta.\n\n`device_id` es el número de tu cuenta que recibió el mensaje: el mismo `device_id` que mandas para responder por ese número. Es `null` si el mensaje no tiene número asociado.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object", "properties": {"data": {"type": "object", "properties": {"message": {"type": "object", "properties": {"type": {"type": "string", "enum": ["text", "image", "video", "document", "sticker", "contact", "audio", "note", "action", "invalid", "interactive", "location", "voice_call", "video_call", "event", "button", "template", "notification", "option_list", "template_button_reply", "poll", "whatsapp_form", "call_permission_request", "call_permission_reply", "request_contact_info"]}}}}}}}, "example": {"id": "evt_000000000000000000000001", "type": "message.received", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"message": {"id": 1001, "type": "text", "text": "Hola, quiero más información", "media": null}, "contact": {"id": 2001, "name": "Contacto de ejemplo", "phone": "+5491100000000"}, "receiver": {"phone": "+5491100000001"}, "device_id": 4521}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "flow.completed": {"post": {"summary": "flow.completed", "description": "Un contacto terminó un flujo de automatización.\n\nSe envía una sola vez por cada ejecución de un flujo, cuando termina con éxito. No se envía si la ejecución se cortó, se canceló, quedó bloqueada o falló. `contact` es `null` si la ejecución no tiene un contacto asociado. Un flujo muy usado puede generar muchos eventos seguidos.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000003", "type": "flow.completed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"flow": {"id": 3001, "name": "Flujo de ejemplo"}, "contact": {"id": 2001, "name": "Contacto de ejemplo", "phone": "+5491100000000"}}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "broadcast.finished": {"post": {"summary": "broadcast.finished", "description": "Una campaña llegó a su estado final (enviada, fallida o con fallos parciales), con sus conteos.\n\n`status` es `success`, `failed` o `partially_failed`. `reason` dice por qué terminó: `completed` (se enviaron todas las páginas), `cancelled` (se cerró una campaña pausada), `template_rejected` (Meta rechazó la plantilla, o su categoría cambió, mientras la campaña esperaba), `approval_expired` (la plantilla no se aprobó a tiempo), `mac_lockout` (la cuenta alcanzó su límite de contactos activos), `no_recipients` (la audiencia quedó vacía) o `error` (el envío no pudo despacharse). `totals` son los conteos del envío: `processed` (destinatarios procesados), `failed` y `skipped_unreachable` (destinatarios que el canal no puede alcanzar); valen 0 si la campaña terminó antes de enviar. Se emite una sola vez por campaña.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000011", "type": "broadcast.finished", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"broadcast": {"id": 6001, "name": "Campaña de ejemplo"}, "status": "partially_failed", "reason": "completed", "totals": {"processed": 1200, "failed": 30, "skipped_unreachable": 12}}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "contact.stage_changed": {"post": {"summary": "contact.stage_changed", "description": "Un contacto cambió de etapa en un tablero.\n\nSe envía un evento por cada contacto que cambia de etapa en un tablero por etapas. `kind` es entry (entra al tablero, `from_stage` es null), move (pasa de una etapa a otra), exit (sale del tablero, `to_stage` es null) o stage_deleted (se reubica porque su etapa se borró; el nombre de la etapa borrada es null). No se envía si el contacto se coloca en la etapa en la que ya estaba.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000008", "type": "contact.stage_changed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"contact": {"id": 2001, "name": "Contacto de ejemplo", "phone": "+5491100000000"}, "board": {"id": 5001, "name": "Embudo de ejemplo"}, "kind": "move", "from_stage": {"id": 6001, "name": "Nuevo", "position": 0}, "to_stage": {"id": 6002, "name": "En seguimiento", "position": 1}}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "message.status_changed": {"post": {"summary": "message.status_changed", "description": "Cambió el estado de entrega de un mensaje enviado.\n\nSe emite cuando cambia el estado de entrega de un mensaje que enviaste a un contacto, ya sea desde el chat, una automatización o un asistente de IA (incluye los enviados con clave). No se emite para mensajes de difusiones ni para mensajes recibidos. `message_id` es el id que devuelve la API, `contact_id` el del contacto y `device_id` el número de tu cuenta por el que salió el mensaje, el mismo `device_id` que usas para enviar (`null` si no tiene). `status` es `sent`, `delivered`, `read` o `failed`. `previous_status` es el estado que el mensaje tenía justo antes de este cambio y es `null` en tres casos: es el primer aviso del mensaje, es el mismo estado que `status` (por ejemplo, el proveedor confirma el `sent` que ya registró el envío) o no se pudo leer. `error` solo viene cuando `status` es `failed`: `code` es el código numérico del proveedor y `title` su título, y cualquiera de los dos puede ser `null` si el proveedor no los informa. `error.reason` es `null` cuando el fallo lo informó el proveedor; cuando Funnelchat decidió que un mensaje enviado con clave no salía (por ejemplo, por saldo insuficiente o ventana de 24 horas cerrada) trae el motivo (`device_unavailable`, `insufficient_balance`, `window_closed`, `recipient_unreachable`, `template_not_found`, `template_unresolved_placeholder`, `not_queued` o `not_sent`), `code` es `null` y `title` es un texto llano en el idioma de la cuenta. `occurred_at` es el momento en que Funnelchat registró el estado, en UTC. `client_reference` es la referencia que mandaste al enviar el mensaje (por ejemplo `pedido-1001`) y `null` si el envío no la llevaba: sirve para relacionar cada aviso con tu envío también cuando la llamada de envío no respondió. Cada estado se informa una sola vez por mensaje: si llegan varios avisos juntos se informa el más avanzado y un aviso atrasado que no cambia el estado no genera evento. Un mensaje que no llegó a salir (por ejemplo, por ventana de 24 horas cerrada) no recibe avisos de estado, salvo uno enviado con clave: ese recibe un único aviso `failed` con `previous_status` en `null`, su `error.reason` y su `client_reference`. Un mensaje enviado con clave cuyo resultado no se conoce (el proveedor no respondió o el envío se interrumpió) no recibe aviso. Un mensaje cuyo envío no empezó dentro de la hora ya no sale: pasada la hora sin resultado, su estado es `null`, o `failed` con `error.reason` `not_sent` y su aviso si su envío llega tarde. Reintentar es un envío nuevo, que sólo puede duplicar si el envío había empezado y se cortó. Un mensaje `queued` no es enviado: su resultado llega por este evento o por la consulta del mensaje. En cuentas con mucho tráfico, los eventos que excedan el límite de entrega por minuto de la cuenta se descartan. Para recibirlo debes suscribirte a `message.status_changed` de forma explícita: una suscripción a `*` no lo incluye.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000014", "type": "message.status_changed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"message_id": 1001, "contact_id": 2001, "device_id": 4521, "status": "failed", "previous_status": "sent", "error": {"code": 131047, "title": "Re-engagement message", "reason": null}, "occurred_at": "2026-10-01T12:00:00Z", "client_reference": "pedido-1001"}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "conversation.closed": {"post": {"summary": "conversation.closed", "description": "Una conversación pasó a cerrada.\n\nSe envía cuando una conversación pasa de abierta a cerrada. No se envía si la conversación ya estaba cerrada ni cuando se reabre.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000004", "type": "conversation.closed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"contact": {"id": 2001, "name": "Contacto de ejemplo", "phone": "+5491100000000"}}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "conversation.assigned": {"post": {"summary": "conversation.assigned", "description": "Una conversación quedó asignada a una persona del equipo o a un agente de IA.\n\nSe envía cuando una conversación queda asignada a una persona del equipo (`assignee.type` = `human`) o a un agente de IA (`assignee.type` = `ai`), por cualquier camino. `assignee` trae el tipo y el nombre, nunca un identificador. No se envía al desasignar, ni cuando se vuelve a enviar el mismo asignado.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000005", "type": "conversation.assigned", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"contact": {"id": 2001, "name": "Contacto de ejemplo", "phone": "+5491100000000"}, "assignee": {"type": "human", "name": "Agente de ejemplo"}}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "contact.tag_added": {"post": {"summary": "contact.tag_added", "description": "Un contacto recibió una etiqueta.\n\nSe envía un evento por cada contacto y etiqueta agregados, también en operaciones en bloque; no se envía si el contacto ya tenía la etiqueta. `source` indica el origen: manual, flow, bulk, import, sync, walink o ai. En operaciones en bloque grandes el límite de volumen de la cuenta puede descartar parte de los eventos.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000006", "type": "contact.tag_added", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"contact": {"id": 2001, "name": "Contacto de ejemplo", "phone": "+5491100000000"}, "tag": {"id": 4001, "name": "Etiqueta de ejemplo"}, "source": "manual"}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "broadcast.paused": {"post": {"summary": "broadcast.paused", "description": "Una campaña se pausó sola, por ejemplo porque bajó la calidad del número.\n\n`reason` es `quality_degraded` (la calidad del número bajó respecto de la del inicio, o llegó a RED) o `quality_unreadable` (no se pudo leer la calidad en tres controles seguidos). `quality` y `quality_baseline` son la calidad de Meta leída en el control y la del inicio del envío (`GREEN`, `YELLOW`, `RED`), o `null` si no se pudo leer. Sólo las campañas con envío inteligente se pausan solas. Reanudar la campaña y que se pause otra vez emite otro evento.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000007", "type": "broadcast.paused", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"broadcast": {"id": 6001, "name": "Campaña de ejemplo"}, "reason": "quality_degraded", "quality": "YELLOW", "quality_baseline": "GREEN"}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "contact.tag_removed": {"post": {"summary": "contact.tag_removed", "description": "Un contacto perdió una etiqueta.\n\nSe envía un evento por cada contacto y etiqueta quitados, también en operaciones en bloque; no se envía si el contacto no tenía la etiqueta. No se envía al borrar la etiqueta, al borrar o fusionar contactos ni al desvincular etiquetas de un tablero. `source` indica el origen: manual, flow, bulk o ai. En operaciones en bloque grandes el límite de volumen de la cuenta puede descartar parte de los eventos.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000009", "type": "contact.tag_removed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"contact": {"id": 2001, "name": "Contacto de ejemplo", "phone": "+5491100000000"}, "tag": {"id": 4001, "name": "Etiqueta de ejemplo"}, "source": "manual"}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "contact.created": {"post": {"summary": "contact.created", "description": "Se creó un contacto nuevo en la cuenta.\n\nSe envía un evento por cada contacto nuevo, sea cual sea el canal: un mensaje entrante, la creación manual, una importación o la API. No se envía cuando el contacto ya existía y sólo se actualiza, ni para los contactos que la plataforma crea para sus propias notificaciones. En importaciones grandes el límite de volumen de la cuenta puede descartar parte de los eventos.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000010", "type": "contact.created", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"contact": {"id": 2001, "name": "Contacto de ejemplo", "phone": "+5491100000000", "email": "contacto@example.com"}}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "template.status_changed": {"post": {"summary": "template.status_changed", "description": "Una plantilla cambió de estado de aprobación en Meta.\n\n`template` identifica la plantilla igual que `GET /templates` (`id`, `uid`, `name`, `category`). `previous_status` y `status` son los estados tal como los devuelve la API (por ejemplo `PENDING`, `APPROVED`, `REJECTED`, `PAUSED`); `previous_status` es `null` si la plantilla no tenía estado. `reason` trae la observación de rechazo (traducida como en la API y limitada a 500 caracteres) cuando el cambio es un rechazo informado al crear la plantilla, y es `null` en el resto. No se emite si Meta reenvía el estado que la plantilla ya tenía. Si una misma plantilla de Meta figura en varias cuentas, cada cuenta recibe su propio evento.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000012", "type": "template.status_changed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"template": {"id": 7001, "uid": "1234567890", "name": "plantilla_ejemplo", "category": "MARKETING"}, "previous_status": "PENDING", "status": "APPROVED", "reason": null}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "flow.failed": {"post": {"summary": "flow.failed", "description": "La ejecución de un flujo para un contacto terminó por un error.\n\nSe envía una sola vez por cada ejecución de un flujo que termina por un error, no porque llegó al final. `reason` indica el motivo y toma uno de estos valores: `node_missing` (el paso a ejecutar ya no existe), `unknown_node_type` (el paso es de un tipo no reconocido), `missing_subflow` (el flujo que debía ejecutarse dentro de otro ya no existe), `malformed_node` (el paso tiene una configuración dañada), `loop_detected` (la ejecución dio vueltas sin esperar nada) y `execution_error` (un error inesperado detuvo la ejecución). No se envía si la ejecución se canceló, quedó bloqueada o se cortó porque se editó el flujo o se unieron contactos. `contact` es `null` si la ejecución no tiene un contacto asociado.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000013", "type": "flow.failed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"flow": {"id": 3001, "name": "Flujo de ejemplo"}, "contact": {"id": 2001, "name": "Contacto de ejemplo", "phone": "+5491100000000"}, "reason": "execution_error"}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "whatsapp_form.completed": {"post": {"summary": "whatsapp_form.completed", "description": "Un contacto completó un formulario de WhatsApp.\n\nSe envía cuando un contacto completa un formulario, aunque la respuesta llegue tarde. Puede llegar más de una vez con el mismo `id`: deduplica por `id`. Si el contacto vuelve a enviar el mismo formulario, no se genera otro aviso: queda la primera respuesta. Cada respuesta de texto llega con a lo sumo 600 caracteres. `form.id` es el formulario (el mismo `family_id` de la lectura de formularios) y `form.version_id` la versión que recibió el contacto. `answers` es una lista con una entrada por pregunta, en el orden del formulario: `key` es el nombre de la pregunta, `label` su etiqueta y `value` la respuesta (texto, número, `true` o `false`, una lista de textos en opción múltiple, o `null` si no la contestó). En las preguntas de lista y de opción múltiple, `value` es el id de la opción. `message_id` es el id del mensaje del formulario y es `null` si el envío no tuvo un mensaje público; `device_id` es el número de WhatsApp por el que salió, o `null` si ya no es de la cuenta. `contact_id` es `null` si el contacto se eliminó antes de que saliera el aviso (las respuestas llegan igual); si se fusionó con otro, es el id del contacto que quedó. Mientras se fusionan dos contactos, el id puede ser el del contacto que se está fusionando.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000015", "type": "whatsapp_form.completed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"form": {"id": 4001, "name": "Captar un lead", "version_id": 4002}, "message_id": 1001, "contact_id": 2001, "device_id": 5001, "answers": [{"key": "nombre", "label": "Nombre", "value": "Contacto de ejemplo"}, {"key": "intereses", "label": "Intereses", "value": ["op_a", "op_b"]}, {"key": "comentarios", "label": "Comentarios", "value": null}], "occurred_at": "2026-10-01T12:00:00Z"}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "webhook.test (conversations)": {"post": {"summary": "webhook.test", "description": "Evento de prueba enviado desde la ruta de prueba de una suscripción.", "x-fc-status": "live", "parameters": [{"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma de la entrega: `t=<timestamp>,v1=<firma>`. Se calcula sobre `t + \".\" + cuerpo crudo`.", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo de evento (igual que `type` en el cuerpo).", "schema": {"type": "string"}}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "Identificador de esta entrega; cambia en cada intento.", "schema": {"type": "string"}}], "requestBody": {"content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_000000000000000000000002", "type": "webhook.test", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"subscription": "00000000-0000-4000-8000-000000000001", "sent_at": "2026-10-01T12:00:00Z"}}}}}, "responses": {"2XX": {"description": "Cualquier respuesta 2xx en 10 segundos o menos confirma la entrega."}}, "x-fc-domain": "conversations"}}, "group.created": {"post": {"operationId": "webhook.group_created", "summary": "group.created", "description": "Se creó o se importó un grupo dentro de una campaña de la cuenta.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "group.created"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_00000000000000000000000000000001", "type": "group.created", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"group": {"uid": "120363000000000001@g.us", "name": "Grupo de ejemplo", "type": "group"}, "campaign": {"uuid": "00000000-0000-4000-8000-000000000001", "name": "Campaña de ejemplo"}, "source": "created"}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "group_participant.joined": {"post": {"operationId": "webhook.group_participant_joined", "summary": "group_participant.joined", "description": "Una persona entró a un grupo de la cuenta; `campaign` es null si el grupo no es de una campaña.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "group_participant.joined"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_00000000000000000000000000000002", "type": "group_participant.joined", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"group": {"uid": "120363000000000001@g.us", "name": "Grupo de ejemplo"}, "campaign": {"uuid": "00000000-0000-4000-8000-000000000001"}, "participant": {"identity": "+5491100000000", "identity_type": "phone", "name": "Ana Pérez"}}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "group_participant.left": {"post": {"operationId": "webhook.group_participant_left", "summary": "group_participant.left", "description": "Una persona salió de un grupo de la cuenta; `campaign` es null si el grupo no es de una campaña.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "group_participant.left"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_00000000000000000000000000000003", "type": "group_participant.left", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"group": {"uid": "120363000000000001@g.us", "name": "Grupo de ejemplo"}, "campaign": {"uuid": "00000000-0000-4000-8000-000000000001"}, "participant": {"identity": "+5491100000000", "identity_type": "phone", "name": "Ana Pérez"}}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "scheduled_message.finished": {"post": {"operationId": "webhook.scheduled_message_finished", "summary": "scheduled_message.finished", "description": "Un envío programado terminó, con su resultado agregado.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "scheduled_message.finished"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_00000000000000000000000000000004", "type": "scheduled_message.finished", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"scheduled_message": {"uuid": "00000000-0000-4000-8000-000000000004", "name": "Envío de ejemplo"}, "outcome": "partially_failed", "groups": {"total": 10, "failed": 1, "partially_failed": 2}, "occurrence": 1}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "group.blocked": {"post": {"operationId": "webhook.group_blocked", "summary": "group.blocked", "description": "Un grupo quedó inoperable: sin administrador conectado, con el link de invitación inválido o suspendido por WhatsApp.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "group.blocked"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_00000000000000000000000000000005", "type": "group.blocked", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"group": {"uid": "120363000000000001@g.us", "name": "Grupo de ejemplo"}, "campaign": {"uuid": "00000000-0000-4000-8000-000000000001"}, "reason": "admin_access_lost"}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "group.recovered": {"post": {"operationId": "webhook.group_recovered", "summary": "group.recovered", "description": "Un grupo que estaba bloqueado volvió a operar.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "group.recovered"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_00000000000000000000000000000006", "type": "group.recovered", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"group": {"uid": "120363000000000001@g.us", "name": "Grupo de ejemplo"}, "campaign": {"uuid": "00000000-0000-4000-8000-000000000001"}, "reason": "access_restored"}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "scheduled_message.group_delivery_failed": {"post": {"operationId": "webhook.scheduled_message_group_delivery_failed", "summary": "scheduled_message.group_delivery_failed", "description": "Un envío programado falló en un grupo concreto, con el motivo desagregado.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "scheduled_message.group_delivery_failed"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_00000000000000000000000000000007", "type": "scheduled_message.group_delivery_failed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"scheduled_message": {"uuid": "00000000-0000-4000-8000-000000000007"}, "group": {"uid": "120363000000000001@g.us", "name": "Grupo de ejemplo"}, "reason": "device_disconnected", "partial": false, "occurrence": 1}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "scheduled_message.response_received": {"post": {"operationId": "webhook.scheduled_message_response_received", "summary": "scheduled_message.response_received", "description": "Alguien respondió a un envío programado votando una encuesta o respondiendo a un evento.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "scheduled_message.response_received"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_00000000000000000000000000000008", "type": "scheduled_message.response_received", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"scheduled_message": {"uuid": "00000000-0000-4000-8000-000000000008"}, "group": {"uid": "120363000000000001@g.us"}, "participant": {"identity": "+5491100000000", "identity_type": "phone", "name": "Ana Pérez"}, "response": {"type": "poll", "options": ["Opción A"], "rsvp": null}}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "campaign.status_changed": {"post": {"operationId": "webhook.campaign_status_changed", "summary": "campaign.status_changed", "description": "Una campaña se activó o se pausó.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "campaign.status_changed"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_00000000000000000000000000000009", "type": "campaign.status_changed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"campaign": {"uuid": "00000000-0000-4000-8000-000000000009", "name": "Campaña de ejemplo"}, "from": "active", "to": "paused"}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "pipeline.finished": {"post": {"operationId": "webhook.pipeline_finished", "summary": "pipeline.finished", "description": "Terminó un pipeline de operaciones sobre grupos, con su resultado agregado y la clave de idempotencia del pedido que lo originó, cuando la hay.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "pipeline.finished"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_0000000000000000000000000000000a", "type": "pipeline.finished", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"campaign": {"uuid": "00000000-0000-4000-8000-00000000000a"}, "pipeline": {"uuid": "00000000-0000-4000-8000-00000000000b", "name": "Pipeline de ejemplo"}, "outcome": "completed", "actions": {"total": 5, "succeeded": 4, "failed": 0, "skipped": 1, "cancelled": 0}, "idempotency_key": "00000000-0000-4000-8000-00000000000d"}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "poll.voted": {"post": {"operationId": "webhook.poll_voted", "summary": "poll.voted", "description": "Un participante votó en una encuesta enviada a un grupo, o cambió su voto.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "poll.voted"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_0000000000000000000000000000000b", "type": "poll.voted", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"poll": {"uid": "3EB0000000000000000001"}, "group": {"uid": "120363000000000001@g.us"}, "scheduled_message": {"uuid": "00000000-0000-4000-8000-000000000004"}, "participant": {"identity": "+5491100000000", "identity_type": "phone", "name": null}, "options": ["Opción A"]}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "group.invite_link_renewed": {"post": {"operationId": "webhook.group_invite_link_renewed", "summary": "group.invite_link_renewed", "description": "El enlace de invitación de un grupo cambió por una renovación, automática al detectarse inválido o pedida por el usuario; en v0 no se publican los cambios que detecta la sincronización ni el enlace inicial al crear el grupo.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "group.invite_link_renewed"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_0000000000000000000000000000000c", "type": "group.invite_link_renewed", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"group": {"uid": "120363000000000001@g.us", "name": "Grupo de ejemplo", "type": "group"}, "campaign": {"uuid": "00000000-0000-4000-8000-000000000001", "name": "Campaña de ejemplo"}, "invite_link": "https://chat.whatsapp.com/EjemploNuevo0000000000001", "previous_invite_link": "https://chat.whatsapp.com/EjemploAnterior000000000001", "trigger": "automatic", "recovered": false}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "group.invite_link_down": {"post": {"operationId": "webhook.group_invite_link_down", "summary": "group.invite_link_down", "description": "El enlace de invitación de un grupo quedó confirmado caído (WhatsApp respondió que no existe y la renovación no consiguió otro); no se publica por errores transitorios ni por grupos prohibidos o suspendidos, y se publica una sola vez por episodio de bloqueo.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "group.invite_link_down"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_0000000000000000000000000000000d", "type": "group.invite_link_down", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"group": {"uid": "120363000000000001@g.us", "name": "Grupo de ejemplo", "type": "group"}, "campaign": {"uuid": "00000000-0000-4000-8000-000000000001", "name": "Campaña de ejemplo"}, "reason": "http_404", "renewal_attempted": true}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}, "webhook.test (communities)": {"post": {"operationId": "webhook.webhook_test", "summary": "webhook.test", "description": "Evento de prueba que se envía al pedir la prueba de una suscripción; no se puede suscribir a él.", "tags": ["Webhooks"], "parameters": [{"name": "X-Funnelchat-Event", "in": "header", "required": true, "description": "Tipo del evento; igual al `type` del cuerpo.", "schema": {"type": "string"}, "example": "webhook.test"}, {"name": "X-Funnelchat-Delivery", "in": "header", "required": true, "description": "UUID de este intento de entrega; cambia en cada reintento. Para deduplicar usá el `id` del cuerpo.", "schema": {"type": "string"}, "example": "00000000-0000-4000-8000-000000000001"}, {"name": "X-Funnelchat-Signature", "in": "header", "required": true, "description": "Firma `t=<unix>,v1=<hex>`: HMAC-SHA256 de `t + \".\" + cuerpo` con el secreto completo `whsec_…`.", "schema": {"type": "string"}, "example": "t=1790856000,v1=0000000000000000000000000000000000000000000000000000000000000000"}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object"}, "example": {"id": "evt_0000000000000000000000000000000e", "type": "webhook.test", "version": 1, "occurred_at": "2026-10-01T12:00:00Z", "data": {"subscription": "00000000-0000-4000-8000-00000000000c", "sent_at": "2026-10-01T12:00:00Z"}}}}}, "responses": {"2XX": {"description": "Respondé con cualquier 2xx dentro de 10 segundos para confirmar la recepción."}}, "x-fc-domain": "communities"}}}, "components": {"schemas": {"AccountsUserResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "email": {"type": "string"}, "admin": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "role": {"type": "string"}, "token": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "first_login_at": {"type": "string"}, "last_login_at": {"type": "string"}, "first_login_form": {"type": ["boolean", "null"]}, "without_business_type": {"type": "boolean"}, "image": {"type": "string"}, "phone": {"type": "string"}, "timezone": {"type": "string"}, "locale": {"anyOf": [{"type": "string"}, {"type": "string", "enum": ["es"]}]}, "owner_id": {"type": "string"}, "owner": {"type": ["object", "null"], "description": "Owner (principal) account exposure — backoffice-legacy-parity Increment 4.\nGuard: only populated when the `owner` relation is explicitly eager-loaded\n(UserController::index/getUserDetails via `->with('owner:id,name,email')`).\nConsumers that don't eager-load it (e.g. /me, front/account) always get\n`null` here — the resource NEVER lazy-loads the relation itself, so no\nN+1 is possible regardless of caller.", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "email": {"type": "string"}}, "required": ["id", "name", "email"]}, "plan": {"anyOf": [{"type": "array"}, {"type": "string", "enum": [""]}]}, "effective_business_numbers_limit": {"type": ["integer", "null"]}, "effective_agents_limit": {"type": ["integer", "null"]}, "advisor_scope": {"type": "string"}, "show_advisor_name": {"type": "string"}, "contacts": {"type": ["string", "integer"]}, "groups": {"type": ["string", "integer"]}, "devices": {"type": "string"}, "additional_devices": {"type": "string"}, "agents": {"type": "string"}, "payment_till": {"type": "string"}, "next_billing_at": {"type": "string"}, "last_payment_at": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "feature_versions": {"type": "string"}, "effective_feature_versions": {"type": "object", "description": "Effective feature versions — beta-program-catalog-membership.\nMap of feature_key => bool (true = effective version is beta),\nresolved via VersionCheckService::isUserMigratedBatch()\nprecedence (own row -> owner row -> catalog default_version,\nsame as isUserMigrated() per feature, batched — see that\nmethod's docblock for the query-budget rationale). Unlike\n`feature_versions` above, this is not gated by whenLoaded —\nthe service performs its own cached lookup rather than\nrelying on an eager-loaded relation. Lets agents (read-only)\nsee the version they inherit from their owner without\nexposing the admin-gated catalog endpoints. Scoped to the GET /me and GET /user(+/v1 alias) endpoints —\nthe two self-serialization routes that need it per design.md\nD11 — rather than computed unconditionally on every\nUserResource serialization. Admin/agent LIST endpoints\n(GET /users, GET /users/agents) also render via UserResource;\ncomputing this per row there would still cost extra queries\nwith no caching benefit across distinct users, confirmed\nempirically to blow the pre-existing /users N+1 smoke-test\nbudget (35 queries) even for a single self-referencing admin\nrow inside the list. Path-gating avoids paying that cost\nanywhere it isn't needed.", "additionalProperties": {"type": "boolean"}}, "workspaces_enabled": {"type": "string", "description": "workspaces-and-roles Inc 2 (design.md D2, tasks.md 2.8). Same\npath-gating as effective_feature_versions above (GET /me,\nGET /user only) to avoid an extra per-row cache lookup on\nlist endpoints (GET /users, GET /users/agents) that also\nserialize via UserResource."}, "panel_usage": {"type": ["object", "null"], "description": "Progreso de uso del panel. Va con `whenLoaded` para que la lista del\nbackoffice no dispare una consulta por fila: sin el eager load la\nclave no aparece, en vez de aparecer con un cero inventado.", "properties": {"percentage": {"type": "string"}, "ceiling": {"type": "string"}, "used_count": {"type": "string"}, "reachable_count": {"type": "string"}, "total_count": {"type": "string"}, "tier": {"type": "string"}, "is_agent": {"type": "string"}, "blocks": {"type": "string"}, "computed_at": {"type": ["string", "null"]}}, "required": ["percentage", "ceiling", "used_count", "reachable_count", "total_count", "tier", "is_agent", "blocks", "computed_at"]}, "created_at": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "updated_at": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "is_dormant": {"type": "boolean", "description": "Dormant state — billing-dormant-account.\nResolution order:\n1. subscriptions relation is loaded (AuthController, /me route) → filter in PHP.\n2. dormant_row side-channel set by UserController::getUserDetails() → back-compat.\n3. Neither loaded → defaults to false / null."}, "dormant_expires_at": {"type": ["string", "null"]}, "dormant_provider": {"type": "string", "description": "Dormant provider + eligibility — dormant-hotmart-support D6.\ndormant_provider is always resolvable (plain column read); it mirrors\nthe same binary criterion BillingDormantService uses to branch dormant.", "enum": ["hotmart", "stripe"]}, "can_enter_dormant": {"type": ["boolean", "null"]}, "stripe_migration_incomplete": {"type": "boolean", "description": "Hotmart -> Stripe migration saved without a linked subscription\n(plan-change-full-mac-grant-and-stripe-migration-close, D6). The boolean is\nharmless everywhere; the reason code is only emitted to admins (backoffice)."}, "stripe_migration_incomplete_reason": {"type": "string"}, "mac_enforcement_enabled": {"type": "boolean", "description": "MAC enforcement fields — mac-rollover-and-admin Inc 5.\nmac_enforcement_enabled: cast to bool so null (pre-migration) renders as false.\navailable_mac_buckets_sum: single extra query; acceptable on /me (single user)."}, "available_mac_buckets_sum": {"type": "integer"}, "password_set": {"type": "boolean"}, "password_set_at": {"type": ["string", "null"]}}, "required": ["id", "name", "email", "admin", "role", "token", "first_login_at", "last_login_at", "first_login_form", "without_business_type", "image", "phone", "timezone", "locale", "owner_id", "owner", "plan", "advisor_scope", "show_advisor_name", "contacts", "groups", "devices", "additional_devices", "agents", "payment_till", "last_payment_at", "created_at", "updated_at", "is_dormant", "dormant_expires_at", "dormant_provider", "stripe_migration_incomplete", "mac_enforcement_enabled", "available_mac_buckets_sum"], "title": "UserResource"}, "ActionGroupResource": {"anyOf": [{"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "status": {"type": "string"}, "info": {}, "action_name": {"type": "string"}, "action_label": {"type": ["string", "null"]}, "executed_at": {"type": ["string", "null"]}, "executed_by": {"type": ["string", "null"]}, "groups": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}}, "required": ["id", "name", "status", "info", "action_name", "action_label", "executed_at", "executed_by", "groups"]}, {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": ["string", "null"]}, "status": {"type": "string"}, "info": {}, "action_name": {"type": ["string", "null"]}, "action_label": {"type": ["string", "null"]}, "executed_at": {"type": ["string", "null"]}, "executed_by": {"type": ["string", "null"]}, "groups": {"anyOf": [{"type": "array", "prefixItems": [{"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}}, "required": ["id", "name"]}], "minItems": 1, "maxItems": 1}, {"type": "array", "prefixItems": [{"type": "object", "properties": {"id": {"type": "null"}, "name": {"type": ["string", "null"]}}, "required": ["id", "name"]}], "minItems": 1, "maxItems": 1}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}}, "required": ["id", "name", "status", "info", "action_name", "action_label", "executed_at", "executed_by", "groups"]}], "title": "ActionGroupResource"}, "ActionResource": {"type": "object", "properties": {"id": {"type": "string"}, "action": {"type": "string"}, "old_value": {"type": "string"}, "new_value": {"type": "string"}, "field": {"type": "string"}, "status": {"$ref": "#/components/schemas/ActionStatusEnum"}, "tries": {"type": "string"}, "process_at": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "created_at": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "completed_at": {"type": ["string", "null"]}, "stats": {"type": "object", "properties": {"total_groups": {"type": "string"}, "pending": {"type": "string"}, "running": {"type": "string"}, "completed": {"type": "string"}, "failed": {"type": "string"}, "skipped": {"type": "string"}}, "required": ["total_groups", "pending", "running", "completed", "failed", "skipped"]}, "groups": {"type": "string"}, "subActions": {"type": "array", "items": {"$ref": "#/components/schemas/ActionResource"}}}, "required": ["id", "action", "old_value", "new_value", "field", "status", "tries", "process_at", "created_at", "completed_at", "stats", "groups", "subActions"], "title": "ActionResource"}, "ActionStatusEnum": {"type": "integer", "description": "| |\n|---|\n| `0` <br/>  |\n| `1` <br/>  |\n| `2` <br/>  |\n| `3` <br/>  |\n| `4` <br/>  |\n| `5` <br/>  |\n| `6` <br/>  |\n| `7` <br/>  |\n| `8` <br/>  |\n| `9` <br/>  |\n| `10` <br/>  |\n| `11` <br/> Intermediate state for new-flow add-admin actions: the provider ACKed the promotion request but real confirmation (promote webhook or group sync) has not yet arrived. Intentionally excluded from failedStatuses() so the pipeline retry endpoint does NOT reset it to PENDING. This state is set explicitly in ActionGroupProcessor — never derived from a provider string via fromString(). |\n| `12` <br/> Terminal state for an operation-plan action that is deliberately not executed: creator protection, would-leave-group-without-admin guard, confirmation timeout, or cascade from an upstream skipped/failed prerequisite in the same group's chain. Reason lives in action_group.info['skip_reason']. Deliberately excluded from failedStatuses() (a retry must NOT reset a SKIPPED row back to PENDING — the skip is definitive) and from isSuccessful() (a SKIPPED row does not represent a completed operation, so it must not be treated as \"the chain advanced\" for purposes that rely on isSuccessful()). Whether SKIPPED counts as \"chain can continue\" is a cascade decision made explicitly by PlanChainCascadeService, not by this enum. |", "enum": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], "title": "ActionStatusEnum"}, "AlertAction": {"type": "string", "enum": ["disconnected", "usage_80", "expiring", "usage_100", "upcoming_charge", "payment_failed", "plan_blocked", "reactivated", "trial_ending", "cancellation_confirmed", "dormant_ending", "blocked", "no_groups_available", "daily_report", "action_failed", "message_failed", "full", "invalid_link", "low_balance", "topup_succeeded", "topup_failed"], "title": "AlertAction"}, "AlertCampaignResource": {"type": "object", "properties": {"id": {"type": "string"}, "uuid": {"type": "string"}, "name": {"type": "string"}, "is_active": {"type": "boolean"}, "groups_count": {"type": "integer"}, "type": {"type": ["integer", "null"]}}, "required": ["id", "uuid", "name", "is_active", "groups_count", "type"], "title": "AlertCampaignResource"}, "AlertCatalogResource": {"type": "object", "properties": {"module": {"type": "string"}, "action": {"type": "string"}, "category": {"type": "string"}, "is_critical": {"type": "string"}, "label": {"type": "string"}, "tooltip": {"type": "string"}}, "required": ["module", "action", "category", "is_critical", "label", "tooltip"], "title": "AlertCatalogResource"}, "AlertDailyReportResource": {"type": "object", "properties": {"enabled": {"type": "boolean"}, "send_time": {"type": ["string", "null"]}, "all_campaigns": {"type": "boolean"}, "campaign_ids": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}}, "required": ["enabled", "send_time", "all_campaigns", "campaign_ids"], "title": "AlertDailyReportResource"}, "AlertDestinationResource": {"type": "object", "properties": {"id": {"type": "integer"}, "type": {"type": "string"}, "destination": {"type": "string"}, "name": {"type": ["string", "null"]}, "verified": {"type": "boolean"}, "has_secret": {"type": "string"}, "created_at": {"type": ["string", "null"]}, "settings": {"type": "array", "items": {"$ref": "#/components/schemas/AlertSettingResource"}}}, "required": ["id", "type", "destination", "name", "verified", "has_secret", "created_at"], "title": "AlertDestinationResource"}, "AlertDestinationType": {"type": "string", "enum": ["phone", "webhook"], "title": "AlertDestinationType"}, "AlertModule": {"type": "string", "enum": ["device", "mac", "billing", "subscription", "account", "campaign", "group", "ai_quota"], "title": "AlertModule"}, "AlertQuietHourResource": {"type": "object", "properties": {"enabled": {"type": "boolean"}, "start_time": {"type": ["string", "null"]}, "end_time": {"type": ["string", "null"]}}, "required": ["enabled", "start_time", "end_time"], "title": "AlertQuietHourResource"}, "AlertSettingResource": {"type": "object", "properties": {"module": {"type": "string"}, "action": {"type": "string"}, "enabled": {"type": "boolean"}}, "required": ["module", "action", "enabled"], "title": "AlertSettingResource"}, "App.Modules.ScheduledMessages.Resources.MessageTemplateResource": {"type": "object", "properties": {"id": {"type": "string"}, "type": {"type": "string"}, "value": {"type": "string"}, "mentioned": {"type": "boolean"}, "metrics": {"type": "string"}, "reactions": {"type": "string"}, "votes": {"type": "string"}, "event_responses": {"type": "string", "description": "openspec/changes/surface-event-rsvps-in-campaign-metrics, design D2:\nevent RSVP counts grouped by response value, deduped by the\nresponder's coalesced identity (phone, falling back to lid).\nValues with zero responders never appear (no group produced).\nNOTE: this class is a separate JsonResource from\nApp\\Modules\\GroupManagement\\Resources\\MessageTemplateResource\n(same shape, different namespace) — this is the one actually\ncomposed by ScheduledMessageController::show via\nScheduledMessages\\Resources\\ScheduledMessageResource, so the\nfield is added here too to reach the campaign detail read path."}, "shipping_order": {"type": "string"}, "variations": {"type": "array", "items": {"$ref": "#/components/schemas/MessageTemplateVariationResource"}}}, "required": ["id", "type", "value", "mentioned", "metrics", "reactions", "votes", "event_responses", "shipping_order", "variations"], "title": "App.Modules.ScheduledMessages.Resources.MessageTemplateResource"}, "App.Modules.ScheduledMessages.Resources.ScheduledMessageResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "send_at": {"type": ["string", "null"]}, "status": {"type": "string"}, "is_draft": {"type": ["string", "boolean"]}, "type": {"type": "string"}, "folder": {"type": ["object", "null"], "properties": {"id": {"type": "string"}, "name": {"type": "string"}}, "required": ["id", "name"]}, "audience": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "total_readed": {}, "total_played": {}, "link_clicks": {"type": "integer"}, "whalink_custom_domain": {"type": "string", "description": "track-scheduled-message-link-clicks D4/D7: read back by the\nfrontend's edit/clone hydration (`useEditGroupMessageForm`)."}, "shipping_speed": {"type": "string"}, "campaign_ids": {"type": "string"}, "group_ids": {"type": ["string", "null"]}, "excluded_group_ids": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}, "static_audience": {"type": "string"}, "campaigns": {"type": "string"}, "groups": {"type": "string"}, "total_groups": {"type": "string"}, "messages": {"type": "array", "items": {"$ref": "#/components/schemas/App.Modules.ScheduledMessages.Resources.MessageTemplateResource"}}, "total_left_after_send": {"type": "string"}, "can_be_edited": {"type": "string"}, "recurrence": {"anyOf": [{"type": "object", "properties": {"enabled": {"type": "boolean"}, "frequency": {"type": "string"}, "interval": {"type": "string"}, "week_days": {"type": "string"}, "end_type": {"type": "string"}, "end_date": {"type": "string"}, "occurrences": {"type": "string"}, "current_occurrence": {"type": "string"}, "only_new_groups": {"type": "string"}, "only_full_groups": {"type": "string"}}, "required": ["enabled", "frequency", "interval", "week_days", "end_type", "end_date", "occurrences", "current_occurrence", "only_new_groups", "only_full_groups"]}, {"type": "object", "properties": {"enabled": {"type": "boolean"}}, "required": ["enabled"]}]}}, "required": ["id", "name", "send_at", "status", "is_draft", "type", "folder", "audience", "total_readed", "total_played", "link_clicks", "whalink_custom_domain", "shipping_speed", "campaign_ids", "group_ids", "excluded_group_ids", "static_audience", "campaigns", "groups", "total_groups", "messages", "total_left_after_send", "can_be_edited", "recurrence"], "title": "App.Modules.ScheduledMessages.Resources.ScheduledMessageResource"}, "AvailableGroupResource": {"type": "object", "properties": {"uid": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "name": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "description": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "status": {"anyOf": [{"type": "string"}, {"type": "string", "enum": ["available"]}]}, "campaign_id": {"type": ["string", "null"]}}, "required": ["uid", "name", "description", "status", "campaign_id"], "title": "AvailableGroupResource"}, "BroadcastContactResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "phone": {"type": "string"}, "whatsapp_id": {"type": "string"}, "email": {"type": "string"}, "image": {"type": "string"}}, "required": ["id", "name", "phone", "whatsapp_id", "email", "image"], "title": "BroadcastContactResource"}, "BroadcastCostResource": {"type": "object", "properties": {"broadcast_id": {"type": "string"}, "currency": {"type": "string", "const": "USD"}, "is_partial": {"type": "boolean"}, "estimate": {"type": ["object", "null"], "properties": {"category": {"type": ["string", "null"]}, "currency": {"type": "string"}, "contacts": {"type": "integer"}, "unpriced_contacts": {"type": "integer"}, "total": {"type": "number"}, "per_market": {"type": "array", "items": {"type": "object", "properties": {"market": {"type": "string"}, "contacts": {"type": "integer"}, "rate": {"type": ["number", "null"]}, "cost": {"type": "number"}}, "required": ["market", "contacts", "rate", "cost"]}}, "rates_updated_at": {"type": ["string", "null"]}, "computed_at": {"type": ["string", "null"]}}, "required": ["category", "currency", "contacts", "unpriced_contacts", "total", "per_market", "rates_updated_at", "computed_at"]}, "computed": {"type": "object", "properties": {"total": {"type": "number"}, "sent": {"type": "integer", "const": 0}, "billed": {"type": "integer", "const": 0}, "free": {"type": "integer", "const": 0}, "pending_price": {"type": "integer", "const": 0}, "no_data": {"type": "integer", "const": 0}, "category": {"type": ["null", "string", "integer", "object"]}, "per_market": {"type": "array", "items": {"type": "object", "properties": {"market": {"type": "string"}, "contacts": {"type": "integer"}, "rate": {"type": ["number", "null"]}, "cost": {"type": "number"}}, "required": ["market", "contacts", "rate", "cost"]}}}, "required": ["total", "sent", "billed", "free", "pending_price", "no_data", "category", "per_market"]}, "rates_updated_at": {"type": ["string", "null"]}}, "required": ["broadcast_id", "currency", "is_partial", "estimate", "computed", "rates_updated_at"], "title": "BroadcastCostResource"}, "BroadcastCostTiersResource": {"type": "object", "properties": {"tiers": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}, "rates_updated_at": {"type": ["string", "null"]}}, "required": ["tiers", "rates_updated_at"], "title": "BroadcastCostTiersResource"}, "BroadcastFolderResource": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}, "broadcasts_count": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}}, "required": ["id", "name", "broadcasts_count"], "title": "BroadcastFolderResource"}, "BroadcastMessageResource": {"type": "object", "properties": {"contact_id": {"type": "string"}, "contact_name": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "contact_phone": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "contact_image": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "message_status": {"type": ["string", "null"]}, "message_info": {"type": "object", "properties": {"message": {"anyOf": [{"type": "string"}, {}]}, "error_code": {"anyOf": [{"type": "string"}, {}]}, "error": {"anyOf": [{"type": "string"}, {}]}, "error_category": {"anyOf": [{"type": ["string", "null"]}, {}]}}, "required": ["message", "error_code", "error", "error_category"], "additionalProperties": {}}, "skip_reason": {"type": ["string", "null"]}, "skipped_at": {"type": ["string", "null"]}, "recipient_state": {"type": "string"}, "sent_at": {"type": ["string", "null"]}}, "required": ["contact_id", "contact_name", "contact_phone", "contact_image", "message_status", "message_info", "skip_reason", "skipped_at", "recipient_state", "sent_at"], "title": "BroadcastMessageResource"}, "BroadcastResource": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}, "device_id": {"type": ["integer", "null"]}, "template_id": {"anyOf": [{"type": "integer"}, {"type": "string", "enum": [""]}]}, "template": {"type": ["string", "null"]}, "folder": {"type": ["object", "null"], "properties": {"id": {"type": "integer"}, "name": {"type": "string"}}, "required": ["id", "name"]}, "value": {"type": ["array", "null"], "items": {}}, "template_button_flows": {"type": ["array", "null"], "items": {}}, "included_tags": {"type": "array", "items": {"$ref": "#/components/schemas/TagResource"}}, "excluded_tags": {"type": "array", "items": {"$ref": "#/components/schemas/TagResource"}}, "some_tags": {"type": "array", "items": {"$ref": "#/components/schemas/TagResource"}}, "countries": {"type": ["array", "null"], "items": {}}, "kanban_setting_id": {"type": ["integer", "null"]}, "kanban_stage_ids": {"type": ["array", "null"], "items": {}}, "only_stagnant": {"type": "boolean"}, "lead_score_categories": {"type": ["array", "null"], "items": {}}, "lead_score_min": {"type": ["integer", "null"]}, "lead_score_max": {"type": ["integer", "null"]}, "exclude_never_replied": {"type": "boolean"}, "date_from": {"type": "string"}, "date_to": {"type": "string"}, "send_at": {"type": "string"}, "contacts_count": {"type": ["string", "integer", "null"]}, "delivered_messages_count": {"type": "null"}, "readed_messages_count": {"type": "null"}, "created_at": {"type": "string"}, "updated_at": {"type": "string"}, "status": {"$ref": "#/components/schemas/BroadcastStatusEnum"}, "shipping_speed": {"$ref": "#/components/schemas/BroadcastShippingSpeedEnum"}, "risk_acceptance": {"type": ["object", "null"], "properties": {"accepted_by_name": {"type": ["string", "null"]}, "accepted_at": {"type": "string"}, "audience_count": {"type": ["integer", "null"]}, "provider": {"type": ["string", "null"]}, "risk_codes": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}}, "required": ["accepted_by_name", "accepted_at", "audience_count", "provider", "risk_codes"]}, "missing_data_audit": {"type": ["object", "null"], "properties": {"audience_count": {"type": "integer"}, "affected_count": {"type": "integer"}, "items": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}, "unknown_custom_fields": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}}, "required": ["audience_count", "affected_count", "items", "unknown_custom_fields"]}, "smart_send": {"type": "boolean"}, "smart_send_state": {"type": ["object", "null"], "properties": {"batch_size": {"type": "string"}, "pages_per_batch": {"type": "string"}, "current_batch": {"type": "string"}, "total_batches": {"type": "string"}, "quality": {"type": "string"}, "quality_baseline": {"type": "string"}, "checked_at": {"type": "string"}, "resume_page": {"type": "string"}, "page_size": {"type": "string"}, "total_pages": {"type": "string"}, "paused_at": {"type": "string"}, "pause_reason": {"type": "string"}, "quality_read_failures": {"type": "string"}, "cancelled_at": {"type": "string"}}, "required": ["batch_size", "pages_per_batch", "current_batch", "total_batches", "quality", "quality_baseline", "checked_at", "resume_page", "page_size", "total_pages", "paused_at", "pause_reason", "quality_read_failures", "cancelled_at"]}, "failure_reason": {"type": ["object", "null"], "properties": {"reason": {"type": ["string", "null"]}}, "required": ["reason"]}, "send_mode": {"$ref": "#/components/schemas/BroadcastSendModeEnum"}, "protect_template_category": {"type": "boolean"}}, "required": ["id", "name", "device_id", "template_id", "template", "folder", "value", "template_button_flows", "included_tags", "excluded_tags", "some_tags", "countries", "kanban_setting_id", "kanban_stage_ids", "only_stagnant", "lead_score_categories", "lead_score_min", "lead_score_max", "exclude_never_replied", "date_from", "date_to", "send_at", "contacts_count", "delivered_messages_count", "readed_messages_count", "created_at", "updated_at", "status", "shipping_speed", "risk_acceptance", "missing_data_audit", "smart_send", "smart_send_state", "failure_reason", "send_mode", "protect_template_category"], "title": "BroadcastResource"}, "BroadcastSendModeEnum": {"type": "string", "enum": ["now", "schedule", "after_approval"], "title": "BroadcastSendModeEnum"}, "BroadcastShippingSpeedEnum": {"type": "integer", "enum": [0, 1, 2], "title": "BroadcastShippingSpeedEnum"}, "BroadcastStatusEnum": {"type": "integer", "enum": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "title": "BroadcastStatusEnum"}, "BroadcastsResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "device_id": {"type": "string"}, "template_id": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "folder_id": {"type": "string"}, "value": {"type": "string"}, "date_from": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "date_to": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "send_at": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "contacts_count": {"type": "string"}, "created_at": {"type": "string"}, "updated_at": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "status": {"type": "string"}, "shipping_speed": {"type": "string"}, "delivered_messages_count": {"type": ["string", "null"]}, "readed_messages_count": {"type": ["string", "null"]}, "replied_messages_count": {"type": ["string", "null"]}, "failure_reason": {"type": ["object", "null"], "properties": {"reason": {"type": ["string", "null"]}}, "required": ["reason"]}, "send_mode": {"type": "string"}, "protect_template_category": {"type": "boolean"}}, "required": ["id", "name", "device_id", "template_id", "folder_id", "value", "date_from", "date_to", "send_at", "contacts_count", "created_at", "updated_at", "status", "shipping_speed", "delivered_messages_count", "readed_messages_count", "replied_messages_count", "failure_reason", "send_mode", "protect_template_category"], "title": "BroadcastsResource"}, "BulkMoveStageRequest": {"type": "object", "properties": {"contact_ids": {"type": "array", "items": {"type": "integer"}, "minItems": 1, "maxItems": 200}, "stage_id": {"type": "integer"}}, "required": ["contact_ids", "stage_id"], "title": "BulkMoveStageRequest"}, "BulkRemoveStageRequest": {"type": "object", "properties": {"contact_ids": {"type": "array", "items": {"type": "integer"}, "minItems": 1, "maxItems": 200}}, "required": ["contact_ids"], "title": "BulkRemoveStageRequest"}, "CampaignConditionalResource": {"type": "object", "properties": {"id": {"type": "string"}, "type": {"type": "string"}, "conditions": {"type": "string"}, "action_type": {"type": "string"}, "redirect_url": {"type": "string"}, "redirect_group_id": {"type": "string"}, "redirect_group_name": {"type": ["string", "null"]}, "is_group_exclusive": {"type": "string"}, "is_active": {"type": "string"}, "order": {"type": "string"}, "created_at": {"type": "string"}, "updated_at": {"type": "string"}}, "required": ["id", "type", "conditions", "action_type", "redirect_url", "redirect_group_id", "is_group_exclusive", "is_active", "order", "created_at", "updated_at"], "title": "CampaignConditionalResource"}, "CampaignParticipantResource": {"type": "object", "properties": {"id": {"type": "string"}, "phone": {"type": "string"}, "name": {"type": "string"}, "lid": {"type": "string"}, "group_id": {"type": "string"}, "group_name": {"type": ["string", "null"]}, "joined_at": {"type": ["string", "null"]}, "left_at": {"type": ["string", "null"]}, "status": {"type": "string"}, "source": {"type": "string"}}, "required": ["id", "phone", "name", "lid", "group_id", "joined_at", "left_at", "status", "source"], "title": "CampaignParticipantResource"}, "CampaignResource": {"type": "object", "properties": {"id": {"type": "string"}, "uuid": {"type": "string"}, "name": {"type": "string"}, "slug": {"type": "string"}, "photo": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "description": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "pixel": {"type": "string"}, "admins": {"type": "string"}, "group_admins": {"type": "string"}, "disconnected_groups": {"type": "string"}, "connection_error_groups": {"type": "string"}, "deviceId": {"type": "string"}, "userId": {"type": "string"}, "type": {"type": "string"}, "groups": {"type": "string"}, "is_active": {"type": "string"}, "redirect_url": {"type": "string"}, "traffic_distribution_type": {"type": ["string", "null"]}, "fill_groups_enabled": {"type": "string"}, "group_numbering_position": {"type": ["string", "null"]}, "anti_bot_verification_enabled": {"type": "string"}, "custom_domain": {"type": "string"}, "captcha_enabled": {"type": "string"}, "virtual_queue_enabled": {"type": "boolean"}, "virtual_queue_rate_per_minute": {"type": "integer"}, "group_name_variations": {"type": "string"}, "moderation": {"type": "object", "properties": {"type": {"type": "string"}, "insults_and_profanity": {"type": "string"}, "negative_comments": {"type": "string"}, "repetitive_spam": {"type": "string"}, "external_links": {"type": "string"}, "promotions_and_sales": {"type": "string"}, "off_topic": {"type": "string"}}, "required": ["type", "insults_and_profanity", "negative_comments", "repetitive_spam", "external_links", "promotions_and_sales", "off_topic"]}, "members_can_send_messages": {"type": "string"}, "clicks_limit": {"type": "string"}, "participants_limit": {"type": "string"}, "backup_groups_count": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "admins_per_group": {"type": "string"}, "input_tag": {"type": "string"}, "output_tag": {"type": "string"}, "send_always_same_group": {"type": "string"}, "total_participants": {"type": "string"}, "total_clicks": {"type": "string"}, "total_in": {"type": "string"}, "total_out": {"type": "string"}, "total_in_counter": {"type": "integer", "description": "Additive fields backed by the maintained conversion counters (SUM over\ngroups.total_joins/total_leaves), populated by the group sync (Inc.1) and\nthe amortized `groups:backfill-conversion-counters` command (Inc.2) — no\nlonger backfilled on read. These coexist with the calculated `total_in`/\n`total_out` above during the rollout; a later, separate change will drop\nthe calculated fields once the counters are fully populated. `(int)` casts\nthe SUM's possible NULL (all-NULL groups) to 0 for a consistent `number` type.\nScoped by groups.user_id = campaign.user_id — same defensive scoping already\napplied to `groups`/`total_clicks` in CampaignsResource (listing)."}, "total_out_counter": {"type": "integer"}, "auto_create_groups": {"type": "boolean"}, "group_creation_suspended": {"type": ["string", "boolean"]}, "suspended_creator_phones": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}, "group_creation_has_available_creator": {"type": ["string", "boolean"]}, "api_policy": {"type": "object", "description": "`api-group-campaigns` change, design D8: ONLY for the Grupo API\ntype — spec `api-group-campaign-policy` § \"Campaña de otro tipo\n no tiene política API\" (\"la respuesta no incluye política de\n grupos API, y el consumidor tolera su ausencia\"). `$this->when()`\nomits the key entirely for the other three types, rather than\nemitting it as `null`.", "additionalProperties": {}}}, "required": ["id", "uuid", "name", "slug", "photo", "description", "pixel", "admins", "group_admins", "disconnected_groups", "connection_error_groups", "deviceId", "userId", "type", "groups", "is_active", "redirect_url", "traffic_distribution_type", "fill_groups_enabled", "group_numbering_position", "anti_bot_verification_enabled", "custom_domain", "captcha_enabled", "virtual_queue_enabled", "virtual_queue_rate_per_minute", "group_name_variations", "members_can_send_messages", "clicks_limit", "participants_limit", "backup_groups_count", "admins_per_group", "input_tag", "output_tag", "send_always_same_group", "total_participants", "total_clicks", "total_in", "total_out", "total_in_counter", "total_out_counter", "auto_create_groups", "group_creation_suspended", "suspended_creator_phones", "group_creation_has_available_creator"], "title": "CampaignResource"}, "CampaignWebhookLogResource": {"type": "object", "properties": {"id": {"type": "string"}, "campaign_id": {"type": "string"}, "webhook_id": {"type": "string"}, "webhook_type": {"type": "string"}, "url": {"type": "string"}, "payload": {"type": "string"}, "response_status": {"type": "string"}, "response_body": {"type": "string"}, "execution_time_ms": {"type": "string"}, "success": {"type": "string"}, "error_message": {"type": "string"}, "created_at": {"type": "string"}}, "required": ["id", "campaign_id", "webhook_id", "webhook_type", "url", "payload", "response_status", "response_body", "execution_time_ms", "success", "error_message", "created_at"], "title": "CampaignWebhookLogResource"}, "CampaignWebhookResource": {"type": "object", "properties": {"id": {"type": "string"}, "type": {"type": "string"}, "url": {"type": "string"}, "is_active": {"type": "string"}, "created_at": {"type": "string"}, "updated_at": {"type": "string"}}, "required": ["id", "type", "url", "is_active", "created_at", "updated_at"], "title": "CampaignWebhookResource"}, "CampaignsResource": {"type": "object", "properties": {"id": {"type": "string"}, "uuid": {"type": "string"}, "name": {"type": "string"}, "custom_domain": {"type": "string", "description": "Needed so the list can render the branded link (custom domain → /c)\nconsistently with the detail view."}, "photo": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "deviceId": {"type": "string"}, "userId": {"type": "string"}, "type": {"type": "string"}, "is_active": {"type": "string", "description": "Needed by `conversations`' Flows campaign-picker node so it can\nfilter out dead campaigns — same column, same cast as the\nsingular `CampaignResource` (`CampaignResource.php:30`)."}, "groups": {"type": "string"}, "total_clicks": {"type": "string"}, "total_in": {"type": "string"}, "total_in_counter": {"type": "integer", "description": "Additive field backed by the maintained conversion counter (SUM over\ngroups.total_joins), populated by the group sync (Inc.1) and the amortized\n`groups:backfill-conversion-counters` command (Inc.2) — no longer backfilled\non read. Coexists with the calculated `total_in` above during the rollout; a\nlater, separate change will drop the calculated field once the counter is\nfully populated. `(int)` casts the SUM's possible NULL to 0. Note: this\nResource's shape has no `total_out` field — only `total_in_counter` is added\nhere, paralleling the calculated side."}, "admins": {"type": "string"}, "group_admins": {"type": "string"}, "disconnected_groups": {"type": "string"}, "connection_error_groups": {"type": "string"}, "running_actions_count": {"type": "string"}, "pending_actions_count": {"type": "string"}, "pending_sync_groups": {"type": "string"}, "group_creation_suspended": {"type": "boolean", "description": "Suspension state fields — always serialized; the list endpoint does NOT\neager-load administrators, so these are always their default (false / []).\nFull values are only populated in the show/detail endpoint (CampaignResource)."}, "suspended_creator_phones": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "group_creation_has_available_creator": {"type": "boolean"}, "api_open_groups": {"type": "integer", "description": "`api-group-campaigns`. Sólo para el cuarto tipo; para los otros\ntres las claves no viajan y el consumidor tolera su ausencia\n(`$this->when()` omite la clave entera, no la emite en null). El listado las pedía desde el primer día y nadie las emitía: la\ncolumna de administradores quedaba vacía para las campañas API.\nUn gate por incremento no lo atrapa porque cada mitad, por\nseparado, está bien.\n\nHubo además `api_seats_used`/`api_seats_total` para una columna\n\"Ocupación\" que se quitó: medía cuán llenos están los grupos,\nque es una métrica del modelo de LLENAR grupos. En atención\ndedicada un grupo en 8/8 no es un éxito, es uno sin lugar."}, "api_policy": {"type": "object", "properties": {"officialNumber": {"type": ["string", "null"]}, "officialLid": {"type": ["string", "null"]}, "companionNumbers": {"type": "string"}}, "required": ["officialNumber", "officialLid", "companionNumbers"]}}, "required": ["id", "uuid", "name", "custom_domain", "photo", "deviceId", "userId", "type", "is_active", "groups", "total_clicks", "total_in", "total_in_counter", "admins", "group_admins", "disconnected_groups", "connection_error_groups", "running_actions_count", "pending_actions_count", "pending_sync_groups", "group_creation_suspended", "suspended_creator_phones", "group_creation_has_available_creator"], "title": "CampaignsResource"}, "ChannelMetadataResource": {"type": "object", "properties": {"name": {"type": ["string", "null"]}, "description": {"type": ["string", "null"]}, "image": {"type": ["string", "null"]}, "link": {"type": ["string", "null"]}, "admins_numbers": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "participants_numbers": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}, "verified_admin": {"type": "boolean"}, "subscribers_count": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "participants": {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}}, "required": ["name", "description", "image", "link", "admins_numbers", "participants_numbers", "verified_admin", "subscribers_count", "participants"], "title": "ChannelMetadataResource"}, "ChannelMinResource": {"type": "object", "properties": {"id": {"type": ["string", "null"]}, "uid": {"type": ["string", "null"]}, "name": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "description": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "state": {"type": ["string", "null"]}, "subscribers_count": {"type": "integer"}, "invite_link": {"type": ["string", "null"]}, "verification": {"type": ["string", "null"]}, "picture": {"type": ["string", "null"]}, "preview": {"type": ["string", "null"]}, "creation_time": {"type": ["integer", "null"]}, "mute": {"type": ["string", "null"]}, "role": {"type": ["string", "null"]}, "group_id": {"type": ["string", "null"]}}, "required": ["id", "uid", "name", "description", "state", "subscribers_count", "invite_link", "verification", "picture", "preview", "creation_time", "mute", "role", "group_id"], "title": "ChannelMinResource"}, "CommunitiesMessageResource": {"type": "object", "properties": {"id": {"type": "string"}, "uid": {"type": "string"}, "type": {"type": "string"}, "value": {}, "scheduledMessage": {"$ref": "#/components/schemas/ScheduledMessageResource"}}, "required": ["id", "uid", "type", "value", "scheduledMessage"], "title": "MessageResource"}, "ContactCustomFieldResource": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}, "description": {"type": "string"}, "type": {"type": "string"}, "value": {"type": "string"}, "created_at": {"type": "string"}, "updated_at": {"type": "string"}, "contact_count": {"type": "string"}}, "required": ["id", "name", "description", "type", "value", "created_at", "updated_at", "contact_count"], "title": "ContactCustomFieldResource"}, "ContactFlowListResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "running": {"type": "boolean"}, "created_at": {"type": ["string", "null"]}, "last_run_at": {"type": ["string", "null"]}}, "required": ["id", "name", "running", "created_at", "last_run_at"], "title": "ContactFlowListResource"}, "ContactKanbanStageBoardResource": {"type": "object", "properties": {"kanban_setting_id": {"type": "string"}, "kanban_name": {"type": "string"}, "current_stage_id": {"type": "string"}, "value_custom_field_id": {"type": "string"}, "profit_custom_field_id": {"type": "string"}, "stages": {"type": "string"}}, "required": ["kanban_setting_id", "kanban_name", "current_stage_id", "value_custom_field_id", "profit_custom_field_id", "stages"], "title": "ContactKanbanStageBoardResource"}, "ContactLeadScoreResource": {"type": "object", "properties": {"score": {"type": "string"}, "signals_count": {"type": "string"}, "category": {"type": "string"}, "confidence": {"type": "string"}, "disqualified": {"type": "string"}, "disqualified_at": {"type": "string"}, "disqualification_reasons": {"type": "string"}, "updated_at": {"type": ["string", "null"]}, "events": {"type": "array", "items": {"$ref": "#/components/schemas/LeadScoreEventResource"}}, "next_cursor": {"type": "string"}, "has_more": {"type": "boolean"}}, "required": ["score", "signals_count", "category", "confidence", "disqualified", "disqualified_at", "disqualification_reasons", "updated_at", "events", "next_cursor", "has_more"], "title": "ContactLeadScoreResource"}, "ContactNoteResource": {"type": "object", "properties": {"id": {"type": "integer"}, "contact_id": {"type": "integer"}, "author_id": {"type": ["integer", "null"]}, "author_name": {}, "body": {"type": "string"}, "created_at": {"type": ["string", "null"]}, "updated_at": {"type": ["string", "null"]}}, "required": ["id", "contact_id", "author_id", "author_name", "body", "created_at", "updated_at"], "title": "ContactNoteResource"}, "ContactResource": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}, "email": {"type": "string"}, "phone": {"type": "string"}, "whatsapp_id": {"type": "string"}, "whatsapp_username": {"type": ["string", "null"]}, "has_phone": {"type": "boolean"}, "country_code": {"type": "string"}, "status": {"type": "integer"}, "image": {"type": "string"}, "notes": {"type": ["string", "null"], "deprecated": true}, "custom_fields": {"type": "array", "items": {"$ref": "#/components/schemas/ContactCustomFieldResource"}}, "created_at": {"type": ["string", "null"], "format": "date-time"}, "active_conversation": {"type": "integer"}, "favorite": {"type": "integer"}, "unread_messages": {"type": "integer"}, "manually_unread": {"type": "boolean"}, "last_message": {"type": "object", "properties": {"text": {"type": "string"}, "file": {"type": "string"}, "date": {"type": "string"}, "type": {"type": "string"}, "own": {"type": "string"}, "deleted_at": {"type": "string"}, "deleted_scope": {"type": "string"}}, "required": ["text", "file", "date", "type", "own", "deleted_at", "deleted_scope"]}, "device_id": {"type": ["integer", "null"]}, "adviser_id": {"type": ["integer", "null"]}, "paused_at": {"type": ["string", "null"]}, "mac_date": {"type": ["string", "null"]}, "is_mac_inactive": {"type": "boolean", "description": "`true` si el contacto es inactivo por el límite de contactos activos en el momento de la consulta: tu cuenta está en bloqueo (`state` = `enforcing` en `GET /mac/status`) y el contacto todavía no es un contacto activo del ciclo actual. Viene en `GET /contacts`, `GET /contacts/{contact}` y `GET /conversations`."}, "ai_assistant_name": {"type": ["string", "null"]}, "stage_since": {"type": "string"}, "is_stagnant": {"type": "boolean"}, "lead_score": {"type": "object", "properties": {"score": {"type": "integer"}, "signals_count": {"type": "integer"}, "category": {"type": "string"}, "confidence": {"type": "string"}, "disqualified": {"type": "boolean"}, "disqualified_at": {"type": ["string", "null"]}, "updated_at": {"type": ["string", "null"]}}, "required": ["score", "signals_count", "category", "confidence", "disqualified", "disqualified_at", "updated_at"]}, "name_mismatch": {"type": ["array", "null"], "items": {}}}, "required": ["id", "name", "email", "phone", "whatsapp_id", "has_phone", "country_code", "status", "image", "notes", "created_at", "active_conversation", "favorite", "unread_messages", "device_id", "adviser_id", "paused_at", "mac_date", "ai_assistant_name"], "title": "ContactResource"}, "ContactTagResource": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}, "description": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "color": {"type": "string"}, "created_at": {"type": ["string", "null"]}, "updated_at": {"type": ["string", "null"]}, "contact_count": {"type": "string"}, "assigned_at": {"type": ["string", "null"]}, "last_updated": {"type": ["string", "null"]}}, "required": ["id", "name", "description", "color", "created_at", "updated_at", "contact_count", "assigned_at", "last_updated"], "title": "ContactTagResource"}, "ConversationsMessageResource": {"type": "object", "properties": {"chat_id": {"type": "string"}, "contact_id": {"anyOf": [{"type": "integer"}, {"type": "string", "enum": [""]}]}, "content": {"anyOf": [{"type": "array"}, {"type": "string", "enum": [""]}]}, "device_id": {"anyOf": [{"type": "integer"}, {"type": "string", "enum": [""]}]}, "error_code": {"type": "string"}, "file": {"type": "string"}, "forwarded": {"type": "integer"}, "group_id": {"anyOf": [{"type": "integer"}, {"type": "string", "enum": [""]}]}, "id": {"type": "integer"}, "origin": {"anyOf": [{"$ref": "#/components/schemas/MessageOriginEnum"}, {"type": "string", "enum": [""]}]}, "origin_id": {"anyOf": [{"type": "integer"}, {"type": "string", "enum": [""]}]}, "own": {"type": "integer"}, "reference_message": {"type": ["string", "null"]}, "reference_message_uid": {"type": ["string", "null"]}, "is_quotable": {"type": "boolean"}, "status": {"anyOf": [{"$ref": "#/components/schemas/MessageStatusEnum"}, {"type": "null"}]}, "temporal_uuid": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "text": {"type": "string"}, "time": {"type": ["string", "null"]}, "type": {"anyOf": [{"$ref": "#/components/schemas/MessageTypeEnum"}, {"type": "null"}]}, "uid": {"type": "string"}, "info": {"anyOf": [{"type": "array"}, {"type": "string", "enum": [""]}]}, "user_id": {"type": "integer"}, "deleted_at": {"type": ["string", "null"]}, "deleted_scope": {"type": ["string", "null"]}, "poll_results": {"type": "string"}, "event_rsvps": {"type": "string"}, "form_answers": {"type": "string"}, "reactions": {"type": "array", "items": {}}, "file_original_name": {"type": ["string", "null"]}, "client_reference": {"type": ["string", "null"]}}, "required": ["chat_id", "contact_id", "content", "device_id", "error_code", "file", "forwarded", "group_id", "id", "origin", "origin_id", "own", "reference_message", "reference_message_uid", "is_quotable", "status", "temporal_uuid", "text", "time", "type", "uid", "info", "user_id", "deleted_at", "deleted_scope", "reactions", "file_original_name", "client_reference"], "title": "MessageResource"}, "ConversationsUserResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "email": {"type": "string"}, "owner_id": {"type": "string"}, "advisor_scope": {"type": "string"}, "created_at": {"type": "string"}, "updated_at": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}}, "required": ["id", "name", "email", "owner_id", "advisor_scope", "created_at", "updated_at"], "title": "UserResource"}, "CustomDomainResource": {"type": "object", "properties": {"id": {"type": "integer"}, "hostname": {"type": "string"}, "status": {"type": "string"}, "ssl": {"type": "object", "properties": {"validation_method": {"type": "string"}, "validation_string": {"type": ["string", "null"]}}, "required": ["validation_method", "validation_string"]}, "dns_records": {"type": "array", "items": {"anyOf": [{"type": "object", "properties": {"type": {"type": "string", "const": "ALIAS"}, "host": {"type": "string", "const": "@"}, "value": {"type": "string"}, "purpose": {"type": "string"}}, "required": ["type", "host", "value", "purpose"]}, {"type": "object", "properties": {"type": {"type": "string", "const": "CNAME"}, "host": {"type": "string"}, "value": {"type": "string"}, "purpose": {"type": "string", "const": "Apunta el subdominio al servicio de redirección de Funnelchat."}}, "required": ["type", "host", "value", "purpose"]}, {"type": "object", "properties": {"type": {"type": "string", "const": "TXT"}, "host": {"type": "string"}, "value": {"type": ["string", "null"]}, "purpose": {"type": "string", "const": "Validación de control de dominio (DCV) requerida por Cloudflare para emitir el certificado SSL."}}, "required": ["type", "host", "value", "purpose"]}]}}, "detected_provider": {"type": ["string", "null"], "description": "Best-effort DNS provider hint (change `custom-domains`,\nIncrement 6 — design.md D15/D17): pre-selects the frontend's\nprovider dropdown. `null` when detection failed/found no known\nmatch — the frontend falls back to the generic provider. NOT\npart of the cross-domain read contract (`communities` never\nreads it)."}, "created_at": {"type": ["string", "null"]}, "updated_at": {"type": ["string", "null"]}}, "required": ["id", "hostname", "status", "ssl", "dns_records", "detected_provider", "created_at", "updated_at"], "title": "CustomDomainResource"}, "CustomFieldResource": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}, "type": {"$ref": "#/components/schemas/CustomFieldTypeEnum"}, "description": {"type": ["string", "null"]}, "contact_count": {"type": "integer"}, "created_at": {"type": ["string", "null"], "format": "date-time"}}, "required": ["id", "name", "type", "description", "created_at"], "title": "CustomFieldResource"}, "CustomFieldTypeEnum": {"type": "integer", "enum": [0, 1, 2, 3, 4], "title": "CustomFieldTypeEnum"}, "DeviceConnectionEventEnum": {"type": "integer", "enum": [0, 1], "title": "DeviceConnectionEventEnum"}, "DeviceConnectionEventResource": {"type": "object", "properties": {"id": {"type": "integer"}, "phone": {"type": ["string", "null"]}, "event": {"$ref": "#/components/schemas/DeviceConnectionEventEnum"}, "created_at": {"type": "string"}}, "required": ["id", "phone", "event", "created_at"], "title": "DeviceConnectionEventResource"}, "DeviceFleetLogResource": {"type": "object", "properties": {"id": {"type": "integer"}, "method": {"type": "string"}, "url": {"type": ["string", "null"]}, "status_code": {"type": ["integer", "null"]}, "is_error": {"type": "boolean"}, "duration_ms": {"type": ["integer", "null"]}, "created_at": {"type": ["string", "null"]}, "request_payload": {"type": ["array", "null"], "items": {}}, "response_body": {"type": ["array", "null"], "items": {}}, "device_id": {"type": ["string", "null"]}, "device_label": {"type": ["string", "null"]}}, "required": ["id", "method", "url", "status_code", "is_error", "duration_ms", "created_at", "request_payload", "response_body", "device_id", "device_label"], "title": "DeviceFleetLogResource"}, "DeviceHealthDetailResource": {"type": "object", "properties": {"health": {"type": "string"}, "identity": {"type": "string"}, "entities": {"type": "string"}, "throughput_level": {"type": "string", "description": "Detail-only (meta-number-health-status, Increment 6 gap-closing\nfollow-up) — deliberately NOT part of `health`, which the\nlisting shares verbatim; see\nDeviceHealthSummaryBuilder::buildThroughputLevel()."}, "history": {"type": "array", "items": {"type": "object", "properties": {"field": {"type": "string"}, "previous_value": {"type": "string"}, "new_value": {"type": "string"}, "detail": {"type": ["string", "null"]}, "source": {"type": "string"}, "occurred_at": {"type": "string"}}, "required": ["field", "previous_value", "new_value", "detail", "source", "occurred_at"]}}, "meta_billing": {"type": ["object", "null"], "properties": {"waba_id": {"type": ["string", "null"]}, "business_id": {"type": ["string", "null"]}}, "required": ["waba_id", "business_id"]}}, "required": ["health", "identity", "entities", "throughput_level", "history", "meta_billing"], "title": "DeviceHealthDetailResource"}, "DeviceLogResource": {"type": "object", "properties": {"id": {"type": "integer"}, "method": {"type": "string"}, "url": {"type": ["string", "null"]}, "status_code": {"type": ["integer", "null"]}, "is_error": {"type": "boolean"}, "duration_ms": {"type": ["integer", "null"]}, "created_at": {"type": ["string", "null"]}, "request_payload": {"type": ["array", "null"], "items": {}}, "response_body": {"type": ["array", "null"], "items": {}}}, "required": ["id", "method", "url", "status_code", "is_error", "duration_ms", "created_at", "request_payload", "response_body"], "title": "DeviceLogResource"}, "DeviceResource": {"type": "object", "properties": {"id": {"type": "integer"}, "alias": {"type": "string"}, "image": {"type": "string"}, "color": {"type": "string"}, "number": {"type": "string"}, "normalized_number": {"type": "string", "description": "coexistence-connection-and-devices-ux, Increment 1 (design.md D1,\ntasks.md 1.1): purely additive, read-time-computed field used by\n`front/dashboard` to group Meta + QR devices sharing the same\nnumber into one card (`groupDevicesByNumber`). Reuses the SAME\n`PhoneNumberNormalizer` the backend already uses at write time\n(DeviceController) — never a second, drifting implementation.\n`null` when `number` is empty/null (device not yet connected)."}, "first_conecction_at": {"type": "string"}, "user_id": {"type": "integer"}, "provider": {"$ref": "#/components/schemas/WhatsAppProviderEnum"}, "waba_id": {"type": "string"}, "whatsapp_username": {"type": ["string", "null"]}, "is_business": {"type": "integer"}, "connected": {"type": "integer"}, "active": {"type": ["integer", "null"]}, "health": {"type": "string", "description": "meta-number-health-status, Increment 2 (design.md D4/D5/D6):\n`null` for any non-Meta device, WITHOUT exception. NEVER\ntriggers a Meta call — reads only the eager-loaded\n`healthStatus` relation (see DeviceController::index()) via\nDeviceHealthSummaryBuilder, the single source of truth for\nthis shape (also reused by the Increment 4 detail endpoint)."}, "number_key": {"type": ["string", "null"], "description": "Bolsa unica de numeros (whatsapp-preverified-number-assignment, Inc 9, D15/D29):\naditivo. Identidad persistente del numero (sobrevive a la desconexion) y el ultimo\nnumero para mostrar cuando `number` esta vacio. Las columnas estan en\n`Device::$hidden`; aca se exponen explicitamente."}, "number_key_confirmed": {"type": "string", "description": "Identidad confirmada = clave + `number_key_confirmed_at` (una clave sin fecha es\nla \"esperada\" de una fila hermana todavia sin conectar)."}, "is_coexistence": {"type": "boolean"}, "slot_state": {"type": "string", "description": "Inc 10 (D29): `free` / `reserved` / `pending` / `held`, computed only from the row's\ncolumns (no event probe, no extra query). A torn-down row without key from before\nthe backfill may look `free` (see DeviceSlotService::slotState())."}, "pool_suspended": {"type": "boolean", "description": "Inc 25 (audit 2026-10-15, D29/D36/D41/D42): suspension by the plan, the customer's\nkeep choice, and whether a QR connection of this number is allowed (computed over the\ntitular's rows already loaded by the listing; a single resource reads its number's\nrows with one query)."}, "pool_suspension_reason": {"type": ["string", "null"]}, "pool_keep": {"type": "boolean"}, "qr_allowed": {"type": "boolean"}, "can_change_to_meta": {"type": "boolean", "description": "D60 (contract with `front/dashboard`): a QR row of the titular with a confirmed\nnumber identity may change to Meta in place (`PUT devices/{id}` with the Embedded\nSignup code of the same number, own SIM). Never an extra query."}, "capabilities": {"type": "object", "description": "armar-funnelchat-f1-f2 (contract 1014 C4, design.md D3): what the\npublic API can do with this number, per provider. Additive; shared\nby the web routes and the `/api/v1` alias.", "properties": {"messages": {"type": "object", "properties": {"text": {"type": "boolean"}, "media": {"type": "boolean"}, "buttons": {"type": "boolean"}, "lists": {"type": "boolean"}, "templates": {"type": "boolean"}, "location": {"type": "boolean"}, "voice_note": {"type": "boolean"}, "polls": {"type": "boolean"}, "events": {"type": "boolean"}, "contact_card": {"type": "boolean"}, "reactions": {"type": "boolean"}}, "required": ["text", "media", "buttons", "lists", "templates", "location", "voice_note", "polls", "events", "contact_card", "reactions"]}, "groups": {"type": "object", "properties": {"create": {"type": "boolean"}, "add_participants": {"anyOf": [{"type": "boolean"}, {"type": "string", "enum": ["direct", "invite"]}]}, "remove_participants": {"type": "boolean"}, "admins": {"type": "boolean"}, "photo": {"type": "boolean"}, "settings": {"type": "boolean"}, "invite_link": {"type": "boolean"}, "send": {"type": "boolean"}, "join_requests": {"anyOf": [{"type": "boolean"}, {"type": "string", "enum": ["list", "list_and_decide"]}]}}, "required": ["create", "add_participants", "remove_participants", "admins", "photo", "settings", "invite_link", "send", "join_requests"]}, "statuses": {"type": "array", "items": {"type": "string"}}}, "required": ["messages", "groups", "statuses"]}}, "required": ["id", "alias", "image", "color", "number", "normalized_number", "first_conecction_at", "user_id", "provider", "waba_id", "whatsapp_username", "is_business", "connected", "active", "health", "number_key", "number_key_confirmed", "is_coexistence", "slot_state", "pool_suspended", "pool_suspension_reason", "pool_keep", "qr_allowed", "can_change_to_meta", "capabilities"], "title": "DeviceResource"}, "DisconnectSurveyResource": {"type": "object", "properties": {"pending": {"type": "boolean"}, "phone": {"type": ["string", "null"]}}, "required": ["pending", "phone"], "title": "DisconnectSurveyResource"}, "ExtensionTokenResource": {"type": "object", "properties": {"token": {"type": "string"}, "expiresAt": {"type": "integer"}, "expiresInSeconds": {"type": "integer"}}, "required": ["token", "expiresAt", "expiresInSeconds"], "title": "ExtensionTokenResource"}, "FlowResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "uuid": {"type": "string"}, "first_node_uuid": {"type": "string"}, "enable": {"type": "string"}, "valid": {"type": "string"}, "executions": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "created_at": {"type": "string"}, "updated_at": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "nodes_count": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "trigger_uuid": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "trigger_device_ids": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}, "trigger_type": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "trigger_values": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}, "trigger_is_global": {"type": "boolean"}, "trigger_save_activation_phrase": {"type": "boolean"}, "trigger_activation_phrase_field_id": {"type": ["string", "null"]}, "payment_platform": {"type": ["string", "null"]}, "payment_external_product_id": {"type": ["string", "null"]}, "payment_event_types": {"type": ["string", "null"]}, "payment_event_entry_nodes": {"type": ["string", "null"]}, "default": {"type": "string"}, "once_per_contact": {"type": "string"}, "frequency_limit_value": {"type": "string"}, "frequency_limit_unit": {"type": ["string", "null"]}, "frequency_limit_minutes": {"type": "string"}, "folder": {"$ref": "#/components/schemas/FolderResource"}, "nodes": {"type": "array", "items": {"$ref": "#/components/schemas/NodeResource"}}, "inbound_origins": {"type": "string"}, "intention_prompt": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "match_location": {"type": ["string", "null"]}, "match_details": {"type": ["string", "null"]}, "risk_acceptance": {"type": ["object", "null"], "properties": {"accepted_by_name": {"type": ["string", "null"]}, "accepted_at": {"type": "string"}, "codes": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}}, "required": ["accepted_by_name", "accepted_at", "codes"]}}, "required": ["id", "name", "uuid", "first_node_uuid", "enable", "valid", "executions", "created_at", "updated_at", "nodes_count", "trigger_device_ids", "trigger_type", "trigger_is_global", "trigger_save_activation_phrase", "trigger_activation_phrase_field_id", "payment_platform", "payment_external_product_id", "payment_event_types", "payment_event_entry_nodes", "default", "once_per_contact", "frequency_limit_value", "frequency_limit_unit", "frequency_limit_minutes", "intention_prompt", "match_location", "match_details", "risk_acceptance"], "title": "FlowResource"}, "FolderResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "parent_id": {"type": "string"}, "children": {"type": "array", "items": {"$ref": "#/components/schemas/FolderResource"}}, "flows_count": {"type": "integer"}}, "required": ["id", "name", "parent_id"], "title": "FolderResource"}, "ForwardMessageRequest": {"type": "object", "properties": {"targets": {"type": "array", "items": {"type": "object", "properties": {"contact_id": {"type": "integer"}, "device_id": {"type": "integer"}}, "required": ["contact_id", "device_id"]}, "minItems": 1, "maxItems": 5, "uniqueItems": true}, "client_reference": {"type": ["string", "null"], "pattern": "^[\\x21-\\x7E]{1,64}$", "maxLength": 64}}, "required": ["targets"], "title": "ForwardMessageRequest"}, "GroupMetadataResource": {"type": "object", "properties": {"name": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "description": {"type": ["string", "null"]}, "image": {"type": ["string", "null"]}, "link": {"type": ["string", "null"]}, "admins_numbers": {"type": "array", "items": {"type": ["string", "null"]}}, "participants_numbers": {"type": "array", "items": {"type": ["string", "null"]}}, "verified_admin": {"type": "boolean"}, "subscribers_count": {"type": "integer"}, "participants": {"type": "array", "items": {}}, "owner": {"type": ["string", "null"]}}, "required": ["name", "description", "image", "link", "admins_numbers", "participants_numbers", "verified_admin", "subscribers_count", "participants", "owner"], "title": "GroupMetadataResource"}, "GroupMinResource": {"type": "object", "properties": {"uid": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "name": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "description": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "device_id": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "alias": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "number": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "communityUid": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}}, "required": ["uid", "name", "description", "device_id", "alias", "number", "communityUid"], "title": "GroupMinResource"}, "GroupResource": {"type": "object", "properties": {"id": {"anyOf": [{"type": "string"}, {}]}, "admins_numbers": {"anyOf": [{"type": "string"}, {}]}, "admins": {"anyOf": [{"type": "array", "items": {"type": "object", "properties": {"phone": {"type": ["string", "null"]}, "lid": {"type": ["string", "null"]}, "isConnected": {"type": "boolean"}, "displayPhone": {"type": ["string", "null"]}, "accessDenied": {"type": "boolean"}}, "required": ["phone", "lid", "isConnected", "displayPhone", "accessDenied"]}}, {}]}, "admins_count": {"anyOf": [{"type": "string"}, {}]}, "owner": {"anyOf": [{"type": "string"}, {}]}, "owner_display_phone": {"anyOf": [{"type": "string", "description": "Additive (issue #1159): `owner` may be the creator's lid; this is the\nphone to show, set page-wide by `GroupService::applyOwnerConnected()`.\nNot computed (events, other endpoints) → the raw `owner`, no query."}, {}]}, "owner_connected": {"anyOf": [{"type": "boolean"}, {}]}, "clicks": {"anyOf": [{"type": "string"}, {}]}, "communityUid": {"anyOf": [{"type": "string"}, {}]}, "newsletterUid": {"anyOf": [{"type": "string"}, {}]}, "description": {"anyOf": [{"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, {}]}, "deviceId": {"anyOf": [{"type": "string"}, {}]}, "image": {"anyOf": [{"type": "string"}, {}]}, "isFull": {"anyOf": [{"type": ["string", "boolean"]}, {}]}, "isApi": {"anyOf": [{"type": "boolean"}, {}]}, "link": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "campaign_link": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "campaign_custom_domain": {"type": ["string", "null"], "description": "Custom domain of the owning campaign (if any) so the list can render\nthe branded link (custom domain → /c) instead of the default one."}, "name": {"type": "string"}, "order": {"type": "string"}, "participants_numbers": {"type": "string"}, "participants_count": {"type": "string"}, "status": {"type": "string"}, "uid": {"type": "string"}, "updatedAt": {"type": "string"}, "createdAt": {"type": "string"}, "scheduled_message_count": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "type": {"type": "string"}, "manual_import": {"type": "boolean"}, "origin": {"type": "integer", "enum": [1, 0]}, "lastSyncAt": {"type": ["string", "null"]}}, "required": ["id", "admins_numbers", "admins", "admins_count", "owner", "owner_display_phone", "owner_connected", "clicks", "communityUid", "newsletterUid", "description", "deviceId", "image", "isFull", "isApi", "link", "campaign_link", "campaign_custom_domain", "name", "order", "participants_numbers", "participants_count", "status", "uid", "updatedAt", "createdAt", "scheduled_message_count", "type", "manual_import", "origin", "lastSyncAt"], "additionalProperties": {}, "title": "GroupResource"}, "IngestLeadScoreEventsRequest": {"type": "object", "properties": {"events": {"type": "array", "items": {"type": "object", "properties": {"signal": {"type": "string", "enum": ["group_join", "group_leave", "group_reaction", "group_poll_vote", "link_click"]}, "owner_user_id": {"type": "integer", "minimum": 1}, "contact_id": {"type": ["integer", "null"], "minimum": 1}, "phone": {"type": ["string", "null"], "pattern": "^[0-9+\\s()\\-]+$", "minLength": 8, "maxLength": 32}, "occurred_at": {"type": "string", "format": "date-time"}, "external_id": {"type": "string", "maxLength": 255}, "scope_key": {"type": ["string", "null"], "maxLength": 255}, "meta": {"type": ["object", "null"], "properties": {"campaign_message_uid": {"type": ["string", "null"], "maxLength": 255}}}}, "required": ["signal", "owner_user_id", "occurred_at", "external_id"]}, "minItems": 1, "maxItems": 100}}, "required": ["events"], "title": "IngestLeadScoreEventsRequest"}, "KanbanSettingResource": {"type": "object", "properties": {"id": {"type": "integer"}, "user_id": {"type": "integer"}, "name": {"type": "string"}, "mode": {"type": "string"}, "stagnant_days": {"type": ["integer", "null"]}, "value_custom_field_id": {"type": ["integer", "null"]}, "profit_custom_field_id": {"type": ["integer", "null"]}, "view_mode": {"type": "string"}, "icon": {"type": ["string", "null"]}, "description": {"type": ["string", "null"]}, "is_primary": {"type": "boolean"}, "color_by_lead_score": {"type": "boolean"}, "structure": {"type": "array", "items": {}}, "stages": {"type": "array", "items": {"$ref": "#/components/schemas/KanbanStageResource"}}, "created_at": {"type": ["string", "null"], "format": "date-time"}, "updated_at": {"type": ["string", "null"], "format": "date-time"}}, "required": ["id", "user_id", "name", "mode", "stagnant_days", "value_custom_field_id", "profit_custom_field_id", "view_mode", "icon", "description", "is_primary", "color_by_lead_score", "structure", "created_at", "updated_at"], "title": "KanbanSettingResource"}, "KanbanStageResource": {"type": "object", "properties": {"id": {"type": "integer"}, "kanban_setting_id": {"type": "integer"}, "name": {"type": "string"}, "position": {"type": "integer"}, "contacts_count": {"type": "integer"}, "created_at": {"type": ["string", "null"], "format": "date-time"}, "updated_at": {"type": ["string", "null"], "format": "date-time"}}, "required": ["id", "kanban_setting_id", "name", "position", "created_at", "updated_at"], "title": "KanbanStageResource"}, "LeadScoreDistributionResource": {"type": "object", "properties": {"total": {"type": "string"}, "counts": {"type": "object", "properties": {"hot": {"type": "string"}, "warm": {"type": "string"}, "cold": {"type": "string"}, "insufficient_data": {"type": "string"}}, "required": ["hot", "warm", "cold", "insufficient_data"]}, "thresholds": {"type": "object", "properties": {"hot_threshold": {"type": "string"}, "warm_threshold": {"type": "string"}, "min_signals": {"type": "string"}}, "required": ["hot_threshold", "warm_threshold", "min_signals"]}}, "required": ["total", "counts", "thresholds"], "title": "LeadScoreDistributionResource"}, "LeadScoreEventResource": {"type": "object", "properties": {"id": {"type": "string"}, "signal": {"type": "string"}, "source": {"type": "string"}, "raw_points": {"type": "string"}, "applied_points": {"type": "string"}, "cap_reason": {"type": ["string", "null"]}, "occurred_at": {"type": ["string", "null"]}, "actor_user_id": {"type": "string"}, "reason": {"type": "string"}}, "required": ["id", "signal", "source", "raw_points", "applied_points", "cap_reason", "occurred_at"], "title": "LeadScoreEventResource"}, "LeadScoreSettingsResource": {"type": "object", "properties": {"hot_threshold": {"type": "integer"}, "warm_threshold": {"type": "integer"}, "min_signals": {"type": "integer"}, "hysteresis": {"type": "integer"}, "decay_steps": {"type": "array", "items": {"type": "object", "properties": {"days": {"type": "integer"}, "points": {"type": "integer"}}, "required": ["days", "points"]}}, "decay_enabled": {"type": "boolean"}, "rules": {"type": "array", "items": {}}}, "required": ["hot_threshold", "warm_threshold", "min_signals", "hysteresis", "decay_steps", "decay_enabled", "rules"], "title": "LeadScoreSettingsResource"}, "LengthAwarePaginator": {"type": "object", "properties": {"current_page": {"type": "integer"}, "data": {"type": "string"}, "first_page_url": {"type": "string"}, "from": {"type": ["integer", "null"]}, "last_page": {"type": "integer"}, "last_page_url": {"type": "string"}, "links": {"type": "object", "additionalProperties": {}}, "next_page_url": {"type": "string"}, "path": {"type": ["string", "null"]}, "per_page": {"type": "integer"}, "prev_page_url": {"type": ["string", "null"]}, "to": {"type": ["integer", "null"]}, "total": {"type": "integer"}}, "required": ["current_page", "data", "first_page_url", "from", "last_page", "last_page_url", "links", "next_page_url", "path", "per_page", "prev_page_url", "to", "total"], "title": "LengthAwarePaginator"}, "MessageOriginEnum": {"type": "integer", "enum": [0, 1, 2, 3], "title": "MessageOriginEnum"}, "MessageResponseResource": {"type": "object", "properties": {"message_template_id": {"type": "string"}, "block_type": {"type": "string"}, "group_name": {"type": "string"}, "responder_name": {"type": "string"}, "identity": {"type": "string"}, "identity_type": {"type": "string"}, "response": {"type": "string"}, "response_label": {"type": "string"}, "responded_at": {"type": ["string", "null"]}}, "required": ["message_template_id", "block_type", "group_name", "responder_name", "identity", "identity_type", "response", "response_label", "responded_at"], "title": "MessageResponseResource"}, "MessageStatusEnum": {"type": "integer", "enum": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], "title": "MessageStatusEnum"}, "MessageTemplateResource": {"type": "object", "properties": {"id": {"type": "string"}, "type": {"type": "string"}, "value": {"type": "string"}, "metrics": {"type": "string"}, "reactions": {"type": "string"}, "votes": {"type": "string"}, "event_responses": {"type": "string", "description": "openspec/changes/surface-event-rsvps-in-campaign-metrics, design D2:\nevent RSVP counts grouped by response value, deduped by the\nresponder's coalesced identity (phone, falling back to lid).\nValues with zero responders never appear (no group produced)."}, "shipping_order": {"type": "string"}}, "required": ["id", "type", "value", "metrics", "reactions", "votes", "event_responses", "shipping_order"], "title": "MessageTemplateResource"}, "MessageTemplateStatusEnum": {"type": "string", "enum": ["PENDING", "APPROVED", "REJECTED", "PENDING_DELETION", "PAUSED", "DISABLED", "ARCHIVED"], "title": "MessageTemplateStatusEnum"}, "MessageTemplateTypeEnum": {"type": "string", "enum": ["text", "image", "document", "video", "location", "default"], "title": "MessageTemplateTypeEnum"}, "MessageTemplateVariationResource": {"type": "object", "properties": {"id": {"type": "string"}, "value": {"type": "string"}, "position": {"type": "string"}}, "required": ["id", "value", "position"], "title": "MessageTemplateVariationResource"}, "MessageTypeEnum": {"type": "integer", "enum": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24], "title": "MessageTypeEnum"}, "MoveBroadcastsToFolderRequest": {"type": "object", "properties": {"broadcast_ids": {"type": "array", "items": {"type": "integer"}, "minItems": 1, "maxItems": 100}, "folder_id": {"type": ["integer", "null"]}}, "required": ["broadcast_ids"], "title": "MoveBroadcastsToFolderRequest"}, "MoveContactStageRequest": {"type": "object", "properties": {"contact_id": {"type": "integer"}, "stage_id": {"type": "integer"}}, "required": ["contact_id", "stage_id"], "title": "MoveContactStageRequest"}, "NodeResource": {"type": "object", "properties": {"uuid": {"type": "string"}, "flow_id": {"type": "string"}, "type": {"type": "integer"}, "action": {"type": "string"}, "value": {"type": "string"}, "next_uuid": {"type": "string"}}, "required": ["uuid", "flow_id", "type", "action", "value", "next_uuid"], "title": "NodeResource"}, "OperationPlanResource": {"type": "object", "properties": {"id": {"type": "integer"}, "uuid": {"type": "string"}, "campaign_id": {"type": "integer"}, "pipeline_uuid": {"type": ["string", "null"]}, "status": {"type": "string", "description": "Live on-read aggregate (increment 6 — PlanStatusResolver), NOT the\nraw persisted column. Right after confirm this still resolves to\n\"scheduled\" (nothing has run yet), so existing confirm-response\nassertions are unaffected — see OperationPlanConfirmTest."}, "name": {"type": "string"}, "timezone": {"type": "string"}, "window_start": {"type": "string"}, "window_end": {"type": "string"}, "range_start_date": {"type": "string"}, "range_end_date": {"type": "string"}, "daily_cap": {"type": "integer"}, "min_spacing_minutes": {"type": "integer"}, "confirmation_spacing_minutes": {"type": "integer"}, "entry_point": {"type": "string"}, "seed": {"type": ["integer", "null"]}, "idempotency_key": {"type": ["string", "null"]}, "confirmed_at": {"type": ["string", "null"]}, "operations": {"type": "array", "items": {"type": "string"}}}, "required": ["id", "uuid", "campaign_id", "pipeline_uuid", "status", "name", "timezone", "window_start", "window_end", "range_start_date", "range_end_date", "daily_cap", "min_spacing_minutes", "confirmation_spacing_minutes", "entry_point", "seed", "idempotency_key", "confirmed_at", "operations"], "title": "OperationPlanResource"}, "OperationPlanSummaryResource": {"type": "object", "properties": {"id": {"type": "string"}, "uuid": {"type": "string"}, "campaign_id": {"type": "string"}, "pipeline_uuid": {"type": "string"}, "status": {"type": "string"}, "name": {"type": "string"}, "entry_point": {"type": "string"}, "timezone": {"type": "string"}, "range_start_date": {"type": ["string", "null"]}, "range_end_date": {"type": ["string", "null"]}, "daily_cap": {"type": "string"}, "confirmed_at": {"type": ["string", "null"]}, "operation_counts": {"type": "object", "properties": {"total": {"type": "number"}, "pending": {"type": ["number", "array"]}, "running": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "done": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "failed": {"type": "integer", "const": 0}, "skipped": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}, "cancelled": {"anyOf": [{"type": "string"}, {"type": "integer", "enum": [0]}]}}, "required": ["total", "pending", "running", "done", "failed", "skipped", "cancelled"]}}, "required": ["id", "uuid", "campaign_id", "pipeline_uuid", "status", "name", "entry_point", "timezone", "range_start_date", "range_end_date", "daily_cap", "confirmed_at", "operation_counts"], "title": "OperationPlanSummaryResource"}, "ParticipantEventResource": {"type": "object", "properties": {"phone": {"type": "string"}, "whatsapp_id": {"type": "string"}, "event_type": {"type": "string", "enum": ["join", "leave"]}, "event_at": {"type": ["string", "null"]}, "group_id": {"type": "integer"}, "group_name": {"type": "string"}, "country_code": {"type": "string"}, "country_name": {"type": "string"}}, "required": ["phone", "whatsapp_id", "event_type", "event_at", "group_id", "group_name", "country_code", "country_name"], "title": "ParticipantEventResource"}, "ParticipantResource": {"type": "object", "properties": {"id": {"type": "string"}, "phone": {"type": "string"}, "action": {"type": "string"}, "isAdmin": {"type": "string"}, "created_at": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}}, "required": ["id", "phone", "action", "isAdmin", "created_at"], "title": "ParticipantResource"}, "PaymentEventTypeEnum": {"type": "string", "enum": ["approved", "pending", "rejected", "cancelled", "refunded", "chargeback"], "title": "PaymentEventTypeEnum"}, "PaymentProductResource": {"type": "object", "properties": {"external_product_id": {"type": "string"}, "name": {"type": ["string", "null"]}}, "required": ["external_product_id", "name"], "title": "PaymentProductResource"}, "PublicMessageResource": {"type": "object", "properties": {"id": {"type": "integer"}, "contact_id": {"type": ["integer", "null"]}, "device_id": {"type": ["integer", "null"]}, "direction": {"type": "string", "enum": ["outbound", "inbound"]}, "type": {"type": "string", "enum": ["text", "image", "video", "document", "sticker", "contact", "audio", "note", "action", "invalid", "interactive", "location", "voice_call", "video_call", "event", "button", "template", "notification", "option_list", "template_button_reply", "poll", "whatsapp_form", "call_permission_request", "call_permission_reply", "request_contact_info"]}, "status": {"type": ["string", "null"], "enum": ["sent", "delivered", "read", "failed", "queued", null]}, "error": {"type": ["object", "null"], "properties": {"code": {"type": ["integer", "null"]}, "title": {"type": ["string", "null"]}, "reason": {"type": ["string", "null"]}}, "required": ["code", "title", "reason"]}, "client_reference": {"type": ["string", "null"]}, "created_at": {"type": ["string", "null"]}, "updated_at": {"type": ["string", "null"]}}, "required": ["id", "contact_id", "device_id", "direction", "type", "status", "error", "client_reference", "created_at", "updated_at"], "title": "PublicMessageResource"}, "QuickReplyResource": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}, "message": {"type": "string"}, "file": {"type": "string"}, "type": {"$ref": "#/components/schemas/MessageTypeEnum"}, "visibility": {"$ref": "#/components/schemas/QuickReplyVisibilityEnum"}, "agents": {"type": "object", "additionalProperties": {}}, "user_id": {"type": "integer"}, "created_at": {"type": "string"}, "updated_at": {"type": "string"}}, "required": ["id", "name", "message", "file", "type", "visibility", "agents", "user_id", "created_at", "updated_at"], "title": "QuickReplyResource"}, "QuickReplyVisibilityEnum": {"type": "integer", "enum": [1, 2, 3], "title": "QuickReplyVisibilityEnum"}, "RemoveContactStageRequest": {"type": "object", "properties": {"contact_id": {"type": "integer"}}, "required": ["contact_id"], "title": "RemoveContactStageRequest"}, "ReorderKanbanStagesRequest": {"type": "object", "properties": {"stage_ids": {"type": "array", "items": {"type": "integer"}, "minItems": 1, "uniqueItems": true}}, "required": ["stage_ids"], "title": "ReorderKanbanStagesRequest"}, "SaleResource": {"type": "object", "properties": {"id": {"type": "integer"}, "occurred_at": {"type": "string"}, "platform": {"type": "string"}, "product_name": {"type": ["string", "null"]}, "kind": {"type": "string"}, "event_type": {"type": ["string", "null"]}, "amount_minor": {"type": "integer"}, "currency": {"type": "string"}, "attribution": {"type": "string"}, "buyer": {"type": ["object", "null"], "properties": {"contact_id": {"type": ["integer", "null"]}, "name": {"type": ["string", "null"]}, "phone": {"type": ["string", "null"]}}, "required": ["contact_id", "name", "phone"]}}, "required": ["id", "occurred_at", "platform", "product_name", "kind", "event_type", "amount_minor", "currency", "attribution", "buyer"], "title": "SaleResource"}, "SalesMetricsResource": {"type": "object", "properties": {"window": {"type": "string"}, "currencies": {"type": "string"}, "distinct_buyers": {"type": "string"}, "repeat_purchase_rate": {"type": "string"}}, "required": ["window", "currencies", "distinct_buyers", "repeat_purchase_rate"], "title": "SalesMetricsResource"}, "ScheduledMessageEditStatusEnum": {"type": "integer", "description": "What happened to one group when the text of an already-sent scheduled message was edited (`POST scheduled-messages/{scheduledMessage}/edit-sent`). Every group of the last send ends in exactly one of these. Stored in the group's outcome row of the edit in `message_deliveries` (`additional_info.sent_edit_outcome.status`, see `ScheduledMessageSentEditService`), never in `group_message_template.status`: that one is the delivery state, still written by the provider webhooks.\n| |\n|---|\n| `1` <br/> Confirmed delivered, inside the window: the provider accepted the edit. |\n| `2` <br/> Confirmed delivered, but 15 minutes had already passed since it was sent to that group. |\n| `3` <br/> Cloud API device, a channel, or a provider that cannot edit. |\n| `4` <br/> The provider returned an error, or the message never reached the group. |", "enum": [1, 2, 3, 4], "title": "ScheduledMessageEditStatusEnum"}, "ScheduledMessageFolderResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "scheduled_messages_count": {"type": "integer"}}, "required": ["id", "name", "scheduled_messages_count"], "title": "ScheduledMessageFolderResource"}, "ScheduledMessageIndexResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"anyOf": [{"type": "string"}, {"type": "string", "enum": [""]}]}, "send_at": {"type": ["string", "null"]}, "status": {"type": "string"}, "is_draft": {"type": ["string", "boolean"]}, "type": {"type": "string"}, "folder_id": {"type": "string"}, "campaign_ids": {"type": "string"}, "excluded_group_ids": {"anyOf": [{"type": "string"}, {"type": "array", "items": {"type": "string"}, "minItems": 0, "maxItems": 0}]}, "can_be_edited": {"type": "string"}, "recurrence": {"anyOf": [{"type": "object", "properties": {"enabled": {"type": "boolean"}, "frequency": {"type": "string"}, "interval": {"type": "string"}, "week_days": {"type": "string"}, "end_type": {"type": "string"}, "end_date": {"type": "string"}, "occurrences": {"type": "string"}, "current_occurrence": {"type": "string"}, "only_new_groups": {"type": "string"}, "only_full_groups": {"type": "string"}}, "required": ["enabled", "frequency", "interval", "week_days", "end_type", "end_date", "occurrences", "current_occurrence", "only_new_groups", "only_full_groups"]}, {"type": "object", "properties": {"enabled": {"type": "boolean"}}, "required": ["enabled"]}]}}, "required": ["id", "name", "send_at", "status", "is_draft", "type", "folder_id", "campaign_ids", "excluded_group_ids", "can_be_edited", "recurrence"], "title": "ScheduledMessageIndexResource"}, "ScheduledMessageResource": {"type": "object", "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "send_at": {"type": "string"}, "status": {"type": "string"}, "type": {"type": "string"}, "audience": {"type": "string"}, "total_readed": {"type": "string"}, "total_played": {"type": "string"}, "campaigns": {"type": "string"}, "groups": {"type": "string"}, "total_groups": {"type": "string"}, "messages": {"type": "array", "items": {"$ref": "#/components/schemas/MessageTemplateResource"}}}, "required": ["id", "name", "send_at", "status", "type", "audience", "total_readed", "total_played", "campaigns", "groups", "total_groups", "messages"], "title": "ScheduledMessageResource"}, "ScheduledMessageRunsResource": {"type": "object", "properties": {"run": {"type": "string"}, "total_groups": {"type": "integer"}, "count_ok": {"type": "integer"}, "count_error": {"type": "integer"}}, "required": ["run", "total_groups", "count_ok", "count_error"], "title": "ScheduledMessageRunsResource"}, "SendDirectMessageRequest": {"type": "object", "properties": {"device_id": {"type": "integer"}, "text": {"type": "string"}, "contact_id": {"type": "integer"}, "file": {"type": "string", "description": "Archivo del mensaje: el nombre de un archivo ya subido a tu cuenta o la URL pública de un archivo (te recomendamos `https://`), que WhatsApp o el proveedor descarga; admite hasta 4096 caracteres. Si empieza con `http`, en mayúsculas o minúsculas, tiene que ser una URL `http://` o `https://`; si no, la respuesta es 422. Consulta la sección «Enviar un archivo por URL».", "pattern": "^(?:[Hh][Tt][Tt][Pp][Ss]?:\\/\\/.+|(?!\\.\\.?$)(?![Hh][Tt][Tt][Pp])[^\\/\\\\\\x00]+)$", "maxLength": 4096}, "file_type": {"type": ["string", "null"], "description": "Tipo del archivo. Es obligatorio cuando `file` es una URL que no termina en una extensión reconocida; si lo envías, manda sobre la extensión. Valores: `image`, `video`, `audio`, `document`, `sticker`. Consulta la sección «Enviar un archivo por URL».", "enum": ["image", "video", "audio", "document", "sticker", null]}, "file_name": {"type": ["string", "null"], "description": "Nombre con el que el contacto ve un documento, con su extensión (por ejemplo `factura.pdf`), de hasta 255 caracteres. Es obligatorio al enviar un documento por URL que no termina en una extensión de la lista. Consulta la sección «Enviar un archivo por URL».", "minLength": 1, "maxLength": 255}, "temporal_uuid": {"type": "string"}, "local_id": {"type": ["string", "null"], "maxLength": 255}, "note": {"type": "boolean"}, "client_reference": {"type": ["string", "null"], "pattern": "^[\\x21-\\x7E]{1,64}$", "maxLength": 64}}, "required": ["device_id", "contact_id"], "title": "SendDirectMessageRequest"}, "SendMessageRequest": {"type": "object", "properties": {"phone": {"type": "string", "minLength": 1}, "whatsapp_id": {"type": "string", "minLength": 1}, "device_id": {"type": "integer"}, "text": {"type": "string"}, "contact_id": {"type": "integer"}, "file": {"type": "string", "description": "Archivo del mensaje: el nombre de un archivo ya subido a tu cuenta o la URL pública de un archivo (te recomendamos `https://`), que WhatsApp o el proveedor descarga; admite hasta 4096 caracteres. Si empieza con `http`, en mayúsculas o minúsculas, tiene que ser una URL `http://` o `https://`; si no, la respuesta es 422. Consulta la sección «Enviar un archivo por URL».", "pattern": "^(?:[Hh][Tt][Tt][Pp][Ss]?:\\/\\/.+|(?!\\.\\.?$)(?![Hh][Tt][Tt][Pp])[^\\/\\\\\\x00]+)$", "maxLength": 4096}, "file_type": {"type": ["string", "null"], "description": "Tipo del archivo. Es obligatorio cuando `file` es una URL que no termina en una extensión reconocida; si lo envías, manda sobre la extensión. Valores: `image`, `video`, `audio`, `document`, `sticker`. Consulta la sección «Enviar un archivo por URL».", "enum": ["image", "video", "audio", "document", "sticker", null]}, "file_name": {"type": ["string", "null"], "description": "Nombre con el que el contacto ve un documento, con su extensión (por ejemplo `factura.pdf`), de hasta 255 caracteres. Es obligatorio al enviar un documento por URL que no termina en una extensión de la lista. Consulta la sección «Enviar un archivo por URL».", "minLength": 1, "maxLength": 255}, "temporal_uuid": {"type": "string"}, "note": {"type": "boolean"}, "options": {"type": "array", "items": {"type": "object", "properties": {"id": {"type": "string", "minLength": 1, "maxLength": 200}, "label": {"type": "string", "pattern": "\\S", "minLength": 1}}}, "minItems": 1, "maxItems": 10}, "reference_message_uid": {"type": ["string", "null"], "maxLength": 255}, "client_reference": {"type": ["string", "null"], "pattern": "^[\\x21-\\x7E]{1,64}$", "maxLength": 64}, "link_preview": {"type": "boolean"}, "link_preview_meta": {"type": "object", "properties": {"url": {"type": "string", "format": "uri", "maxLength": 2048}, "title": {"type": ["string", "null"], "maxLength": 200}, "description": {"type": ["string", "null"], "maxLength": 500}, "image_url": {"type": ["string", "null"], "format": "uri", "maxLength": 2048}, "domain": {"type": "string", "maxLength": 253}}}}, "required": ["device_id", "contact_id"], "title": "SendMessageRequest"}, "SendWhatsappFormRequest": {"type": "object", "properties": {"form_id": {"type": "integer", "minimum": 1}, "device_id": {"type": "integer", "minimum": 1}, "text": {"type": "string", "minLength": 1, "maxLength": 1024}, "button_text": {"type": "string", "minLength": 1, "maxLength": 30}, "client_reference": {"type": ["string", "null"], "pattern": "^[\\x21-\\x7E]{1,64}$", "maxLength": 64}}, "required": ["form_id", "device_id"], "title": "SendWhatsappFormRequest"}, "SslValidationMethod": {"type": "string", "description": "SSL Domain Control Validation (DCV) method requested from Cloudflare for SaaS when creating or re-validating a Custom Hostname. `HTTP` is the default requested by `CloudflareCustomHostnameClient::create()` (design.md D9 — supersedes the original TXT-only MVP default): the certificate is issued once the CNAME resolves and traffic reaches Cloudflare, with no DNS TXT record required for the common case. `TXT` remains available as an explicit fallback via the `switch-validation` endpoint (design.md D11) for domains whose CNAME does not yet route to Cloudflare.\n", "enum": ["TXT", "HTTP"], "title": "SslValidationMethod"}, "StoreAlertDestinationRequest": {"type": "object", "properties": {"type": {"$ref": "#/components/schemas/AlertDestinationType"}, "destination": {"type": "string", "maxLength": 255}, "name": {"type": ["string", "null"], "maxLength": 120}}, "required": ["type", "destination"], "title": "StoreAlertDestinationRequest"}, "StoreBroadcastFolderRequest": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 255}}, "required": ["name"], "title": "StoreBroadcastFolderRequest"}, "StoreContactCardRequest": {"type": "object", "properties": {"device_id": {"type": "integer"}, "contact_card_ids": {"type": "array", "items": {"type": "integer"}, "minItems": 1, "maxItems": 5, "uniqueItems": true}, "client_reference": {"type": ["string", "null"], "pattern": "^[\\x21-\\x7E]{1,64}$", "maxLength": 64}}, "required": ["device_id", "contact_card_ids"], "title": "StoreContactCardRequest"}, "StoreContactNoteRequest": {"type": "object", "properties": {"body": {"type": "string", "maxLength": 2000}}, "required": ["body"], "title": "StoreContactNoteRequest"}, "StoreContactRequest": {"type": "object", "properties": {"phone": {"type": "string"}, "whatsapp_id": {"type": "string"}, "name": {"type": ["string", "null"]}, "email": {"type": ["string", "null"], "format": "email"}, "image": {"type": ["string", "null"]}, "deviceId": {"type": ["integer", "null"]}}, "title": "StoreContactRequest"}, "StoreCustomDomainRequest": {"type": "object", "description": "Validates the payload for `POST /custom-domains`.\n\nSanitizes `hostname` in `prepareForValidation()` BEFORE the `unique`/`regex`\nrules run, so:\n  - the uniqueness check compares against the canonical (sanitized) form —\n    `https://Go.MiMarca.com/landing` and `go.mimarca.com` are the same\n    hostname for this check.\n  - a duplicate or malformed hostname fails validation (422) BEFORE the\n    controller method ever runs, guaranteeing Cloudflare is never called\n    for those cases (spec scenarios \"Duplicate hostname rejected\" /\n    \"Invalid hostname rejected\").\n\nAuthorization (admin-only, payment-valid) is enforced at the route-group\nlevel (`can:user-admin` + `can:payment-valid`), not here — same pattern as\n`StorePlanRequest`/`UpdatePlanRequest`.", "properties": {"hostname": {"type": "string", "maxLength": 255}}, "required": ["hostname"], "title": "StoreCustomDomainRequest"}, "StoreDisconnectReasonRequest": {"type": "object", "description": "Validates the payload for `POST /devices/{device}/disconnect-survey`.\n\nAuthorization is delegated to the controller (`authorize('update', $device)`),\nmatching the pattern used across the Device module's other write endpoints.\n\n`reason` accepts only `DeviceDisconnectReasonEnum::writableValues()`\n(`{1, 2, 4, 5}`) — change `disconnect-survey-block-type`: the legacy `3`\n(`whatsapp_blocked`, ambiguous permanent/temporary) is rejected with 422\non write, even though it remains a valid value for reads/metrics.", "properties": {"reason": {"type": "integer", "enum": ["1", "2", "4", "5"]}}, "required": ["reason"], "title": "StoreDisconnectReasonRequest"}, "StoreFolderRequest": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 255}, "parent_id": {"type": ["integer", "null"]}}, "required": ["name"], "title": "StoreFolderRequest"}, "StoreKanbanSettingRequest": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 50}, "mode": {"type": "string", "enum": ["tags", "stages"]}, "structure": {"type": ["array", "null"], "items": {"type": "object", "properties": {"tag_id": {"type": "integer"}, "position": {"type": "integer", "minimum": 0}}, "required": ["tag_id", "position"]}, "maxItems": 15}, "stages": {"type": ["array", "null"], "items": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 50}}}, "maxItems": 15}, "stagnant_days": {"type": ["integer", "null"], "minimum": 1, "maximum": 65535}, "view_mode": {"type": ["string", "null"], "enum": ["compact", "extended", null]}, "icon": {"type": ["string", "null"], "enum": ["target", "filter", "trending-up", "users", "briefcase", "shopping-cart", "heart", "star", "flag", "rocket", "layers", "zap", null]}, "description": {"type": ["string", "null"], "maxLength": 200}, "is_primary": {"type": "boolean"}, "color_by_lead_score": {"type": "boolean"}}, "required": ["name"], "title": "StoreKanbanSettingRequest"}, "StoreKanbanStageRequest": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 50}, "after_stage_id": {"type": ["integer", "null"]}}, "required": ["name"], "title": "StoreKanbanStageRequest"}, "StoreLeadScoreAdjustmentRequest": {"type": "object", "properties": {"delta": {"type": "integer", "minimum": -100, "maximum": 100}, "reason": {"type": "string", "minLength": 3, "maxLength": 255}}, "required": ["delta", "reason"], "title": "StoreLeadScoreAdjustmentRequest"}, "StoreMessageReactionRequest": {"type": "object", "properties": {"emoji": {"type": "string", "maxLength": 32}}, "required": ["emoji"], "title": "StoreMessageReactionRequest"}, "StoreMessageTemplateRequest": {"type": "object", "properties": {"name": {"type": "string"}, "language_code": {"type": "string", "enum": ["af", "sq", "ar", "ar_EG", "ar_AE", "ar_LB", "ar_MA", "ar_QA", "az", "be_BY", "bn", "bn_IN", "bg", "ca", "zh_CN", "zh_HK", "zh_TW", "hr", "cs", "da", "prs_AF", "nl", "nl_BE", "en", "en_GB", "en_US", "en_AE", "en_AU", "en_CA", "en_GH", "en_IE", "en_IN", "en_JM", "en_MY", "en_NZ", "en_QA", "en_SG", "en_UG", "en_ZA", "et", "fil", "fi", "fr", "fr_BE", "fr_CA", "fr_CH", "fr_CI", "fr_MA", "ka", "de", "de_AT", "de_CH", "el", "gu", "ha", "he", "hi", "hu", "id", "ga", "it", "ja", "kn", "kk", "rw_RW", "ko", "ky_KG", "lo", "lv", "lt", "mk", "ms", "ml", "mr", "nb", "ps_AF", "fa", "pl", "pt_BR", "pt_PT", "pa", "ro", "ru", "sr", "si_LK", "sk", "sl", "es", "es_AR", "es_CL", "es_CO", "es_CR", "es_DO", "es_EC", "es_HN", "es_MX", "es_PA", "es_PE", "es_ES", "es_UY", "sw", "sv", "ta", "te", "th", "tr", "uk", "ur", "uz", "vi", "zu"]}, "category": {"type": "string", "enum": ["UTILITY", "MARKETING", "AUTHENTICATION"]}, "type": {"type": "string", "enum": ["text", "image", "document", "video", "location", "default"]}, "header": {"type": ["string", "null"]}, "file": {"type": "string"}, "body": {"type": "string"}, "params": {"type": ["array", "null"], "items": {"type": "string"}}, "footer": {"type": ["string", "null"]}, "buttons": {"type": ["array", "null"], "items": {"type": "object", "properties": {"text": {"type": "string"}, "type": {"type": "string", "enum": ["QUICK_REPLY", "URL", "PHONE_NUMBER"]}, "url": {"type": "string", "format": "uri"}, "phone_number": {"type": "string"}}}, "maxItems": 3}}, "required": ["name", "language_code", "category", "type", "body"], "title": "StoreMessageTemplateRequest"}, "StoreWebhookEndpointRequest": {"type": "object", "properties": {"url": {"type": "string", "format": "uri", "description": "`https` obligatorio. La comprobación de a DÓNDE resuelve la hace\n`SsrfGuard` en el controlador: acá sólo se valida la forma.", "maxLength": 2048}, "description": {"type": ["string", "null"], "maxLength": 160}, "events": {"type": "array", "description": "Suscripciones iniciales, opcionales: un endpoint puede nacer sin\nninguna y suscribirse después. Lo que NO puede es nacer suscripto a\ntodo por omisión — eso sería lo contrario de \"por demanda\".", "items": {"type": "string", "enum": ["device.disconnected", "device.connected", "billing.payment_failed", "billing.plan_blocked", "billing.upcoming_charge", "subscription.expiring", "subscription.trial_ending", "subscription.reactivated", "subscription.cancellation_confirmed", "subscription.dormant_ending", "ai_quota.low_balance", "ai_quota.topup_succeeded", "ai_quota.topup_failed", "mac.usage_80", "mac.usage_100", "mac.expiring", "account.blocked"]}, "maxItems": 50}}, "required": ["url"], "title": "StoreWebhookEndpointRequest"}, "StoreWebhookSubscriptionRequest": {"type": "object", "description": "El cuerpo de `POST api/webhooks`. La autorización real la hacen los gates de\nla ruta (`can:user-admin`) y `AssertsAccountOwner` en el controlador.", "properties": {"url": {"type": "string", "format": "uri", "maxLength": 2048}, "description": {"type": ["string", "null"], "maxLength": 160}, "events": {"type": "array", "items": {"type": "string", "enum": ["device.disconnected", "device.connected", "billing.payment_failed", "billing.plan_blocked", "billing.upcoming_charge", "subscription.expiring", "subscription.trial_ending", "subscription.reactivated", "subscription.cancellation_confirmed", "subscription.dormant_ending", "ai_quota.low_balance", "ai_quota.topup_succeeded", "ai_quota.topup_failed", "mac.usage_80", "mac.usage_100", "mac.expiring", "account.blocked", "message.received", "flow.completed", "broadcast.finished", "contact.stage_changed", "message.status_changed", "conversation.closed", "conversation.assigned", "contact.tag_added", "broadcast.paused", "contact.tag_removed", "contact.created", "template.status_changed", "flow.failed", "whatsapp_form.completed", "group.created", "group.invite_link_down", "group.invite_link_renewed", "group_participant.joined", "group_participant.left", "scheduled_message.group_delivery_failed", "scheduled_message.response_received", "poll.voted", "pipeline.finished", "group.blocked", "group.recovered", "campaign.status_changed", "scheduled_message.finished", "group_action.failed", "daily_report.generated", "campaign.no_groups_available", "group.full", "group_action.completed", "group.message_received", "group.reaction_added", "group_join_request.created", "group_join_request.revoked"]}, "minItems": 1, "maxItems": 50, "uniqueItems": true}, "filters": {"type": "object", "properties": {"campaign_ids": {"type": ["array", "null"], "items": {"type": "string", "pattern": "^[A-Za-z0-9_-]+$", "minLength": 6, "maxLength": 64}, "maxItems": 100}}}}, "required": ["url", "events"], "title": "StoreWebhookSubscriptionRequest"}, "SwitchValidationMethodRequest": {"type": "object", "description": "Validates the payload for `POST /custom-domains/{customDomain}/switch-validation`\n(design.md D11): switches a domain's SSL Domain Control Validation (DCV)\nmethod between `HTTP` (default) and `TXT` (fallback for domains whose CNAME\ndoes not yet route to Cloudflare).\n\nAuthorization (admin-only, payment-valid) is enforced at the route-group\nlevel (`can:user-admin` + `can:payment-valid`); ownership of the target\ndomain is enforced by the controller's `findOwned()` (404 if not the\nauthenticated admin's own domain) — same pattern as `verify`/`destroy`.", "properties": {"method": {"$ref": "#/components/schemas/SslValidationMethod"}}, "required": ["method"], "title": "SwitchValidationMethodRequest"}, "TagResource": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}, "description": {"type": "string"}, "color": {"type": ["string", "null"]}, "created_at": {"type": ["string", "null"]}, "updated_at": {"type": ["string", "null"]}, "contact_count": {"anyOf": [{"type": "string"}, {"type": "integer", "minimum": 0}]}}, "required": ["id", "name", "description", "color", "created_at", "updated_at", "contact_count"], "title": "TagResource"}, "TemplateResource": {"type": "object", "properties": {"id": {"type": "integer"}, "name": {"type": "string"}, "text": {"type": "string"}, "header": {"type": "string"}, "footer": {"type": "string"}, "observation": {"type": "string"}, "uid": {"type": "string"}, "category": {"type": "string"}, "type": {"anyOf": [{"$ref": "#/components/schemas/MessageTemplateTypeEnum"}, {"type": "string", "enum": [""]}]}, "status": {"anyOf": [{"$ref": "#/components/schemas/MessageTemplateStatusEnum"}, {"type": "string", "enum": [""]}]}, "language_code": {"type": "string"}, "upload_session_id": {"type": "string"}, "header_handle": {"type": "string"}, "file": {"type": "string"}, "params": {"anyOf": [{"type": "array"}, {"type": "string", "enum": [""]}]}, "buttons": {"anyOf": [{"type": "array"}, {"type": "string", "enum": [""]}]}, "user_id": {"type": "integer"}, "device_id": {"type": "integer"}, "created_at": {"type": ["string", "null"], "format": "date-time"}, "recategorized_at": {"type": ["string", "null"], "format": "date-time"}, "quick_reply_buttons": {"type": "array", "items": {}}, "waba_id": {"type": ["string", "null"]}, "belongs_to_current_device": {"type": "boolean"}}, "required": ["id", "name", "text", "header", "footer", "observation", "uid", "category", "type", "status", "language_code", "upload_session_id", "header_handle", "file", "params", "buttons", "user_id", "device_id", "created_at", "recategorized_at", "quick_reply_buttons", "waba_id", "belongs_to_current_device"], "title": "TemplateResource"}, "Ticket": {"type": "object", "properties": {"id": {"type": "integer"}, "user_id": {"type": "integer"}, "contact_id": {"type": ["integer", "null"]}, "title": {"type": "string"}, "description": {"type": ["string", "null"]}, "deadline": {"type": ["integer", "null"]}, "created_at": {"type": ["string", "null"], "format": "date-time"}, "updated_at": {"type": ["string", "null"], "format": "date-time"}, "actor_user_id": {"type": ["integer", "null"]}}, "required": ["id", "user_id", "contact_id", "title", "description", "deadline", "created_at", "updated_at", "actor_user_id"], "title": "Ticket"}, "UpdateAlertDailyReportRequest": {"type": "object", "properties": {"enabled": {"type": "boolean"}, "send_time": {"type": ["string", "null"]}, "all_campaigns": {"type": "boolean"}, "campaign_ids": {"type": ["array", "null"], "items": {"type": "integer"}}}, "required": ["enabled", "all_campaigns"], "title": "UpdateAlertDailyReportRequest"}, "UpdateAlertDestinationRequest": {"type": "object", "properties": {"name": {"type": ["string", "null"], "description": "name is nullable per schema — allow sending null to clear the field.", "maxLength": 120}, "destination": {"type": "string", "description": "`destination` solo se acepta para destinos `webhook` (el controller\nrechaza el resto con 422). La forma real —https y que resuelva a\nuna IP publica— la decide `SsrfGuard` en el controller, no una\nregla de validacion: el guard resuelve DNS, y eso no se expresa\ncon `url`/`starts_with`.", "maxLength": 255}}, "title": "UpdateAlertDestinationRequest"}, "UpdateAlertQuietHourRequest": {"type": "object", "properties": {"enabled": {"type": "boolean"}, "start_time": {"type": ["string", "null"]}, "end_time": {"type": ["string", "null"]}}, "required": ["enabled"], "title": "UpdateAlertQuietHourRequest"}, "UpdateBroadcastFolderRequest": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 255}}, "required": ["name"], "title": "UpdateBroadcastFolderRequest"}, "UpdateContactRequest": {"type": "object", "properties": {"name": {"type": ["string", "null"], "maxLength": 255}, "email": {"type": ["string", "null"], "format": "email", "maxLength": 255}, "phone": {"type": ["string", "null"], "maxLength": 20}, "notes": {"type": ["string", "null"], "maxLength": 2500}, "status": {"type": "integer"}, "adviser_id": {"type": ["integer", "null"]}, "active_conversation": {"type": "boolean"}, "favorite": {"type": "boolean"}, "fixed": {"type": "boolean"}, "image": {"type": ["string", "null"]}, "can_execute_flows": {"type": "boolean"}, "manually_unread": {"type": "boolean"}}, "title": "UpdateContactRequest"}, "UpdateFolderRequest": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 255}, "parent_id": {"type": ["integer", "null"]}}, "title": "UpdateFolderRequest"}, "UpdateKanbanSettingRequest": {"type": "object", "properties": {"name": {"type": ["string", "null"], "maxLength": 50}, "mode": {"type": "string"}, "structure": {"type": ["array", "null"], "items": {"type": "object", "properties": {"tag_id": {"type": "integer"}, "position": {"type": "integer", "minimum": 0}}, "required": ["tag_id", "position"]}, "maxItems": 15}, "stagnant_days": {"type": ["integer", "null"], "minimum": 1, "maximum": 65535}, "view_mode": {"type": ["string", "null"], "enum": ["compact", "extended", null]}, "icon": {"type": ["string", "null"], "enum": ["target", "filter", "trending-up", "users", "briefcase", "shopping-cart", "heart", "star", "flag", "rocket", "layers", "zap", null]}, "description": {"type": ["string", "null"], "maxLength": 200}, "is_primary": {"type": "boolean"}, "color_by_lead_score": {"type": "boolean"}}, "title": "UpdateKanbanSettingRequest"}, "UpdateKanbanStageRequest": {"type": "object", "properties": {"name": {"type": "string", "maxLength": 50}}, "required": ["name"], "title": "UpdateKanbanStageRequest"}, "UpdateMessageTemplateRequest": {"type": "object", "properties": {"device_id": {"type": "integer"}, "category": {"type": "string", "enum": ["UTILITY", "MARKETING", "AUTHENTICATION"]}, "type": {"type": "string", "enum": ["text", "image", "document", "video", "location", "default"]}, "header": {"type": ["string", "null"]}, "file": {"type": "string"}, "body": {"type": "string"}, "params": {"type": ["array", "null"], "items": {"type": "string"}}, "footer": {"type": ["string", "null"]}, "buttons": {"type": ["array", "null"], "items": {"type": "object", "properties": {"text": {"type": "string"}, "type": {"type": "string", "enum": ["QUICK_REPLY", "URL", "PHONE_NUMBER"]}, "url": {"type": "string", "format": "uri"}, "phone_number": {"type": "string"}}}, "maxItems": 3}}, "required": ["device_id", "category", "type", "body"], "title": "UpdateMessageTemplateRequest"}, "UpdateWebhookEndpointEventsRequest": {"type": "object", "description": "El cuerpo de `PUT api/v1/webhooks/endpoints/{id}/events`.\n\nAcepta las dos formas, porque las dos son legítimas y pedirle al cliente que\nelija una sería gratuito:\n\n  `{\"events\": [\"device.disconnected\"]}`\n      → suscribe a esos tipos, activados, y da de baja los que no estén.\n\n  `{\"events\": [{\"event_type\": \"device.disconnected\", \"enabled\": false}]}`\n      → lo mismo, pero eligiendo el interruptor de cada uno. Un tipo con\n        `enabled: false` queda suscripto y apagado: no se entrega, pero la\n        configuración sobrevive a volver a prenderlo.", "properties": {"events": {"type": "array", "items": {"type": "object", "properties": {"event_type": {"type": "string", "enum": ["device.disconnected", "device.connected", "billing.payment_failed", "billing.plan_blocked", "billing.upcoming_charge", "subscription.expiring", "subscription.trial_ending", "subscription.reactivated", "subscription.cancellation_confirmed", "subscription.dormant_ending", "ai_quota.low_balance", "ai_quota.topup_succeeded", "ai_quota.topup_failed", "mac.usage_80", "mac.usage_100", "mac.expiring", "account.blocked"]}, "enabled": {"type": "boolean"}}, "required": ["event_type"]}, "maxItems": 50}}, "required": ["events"], "title": "UpdateWebhookEndpointEventsRequest"}, "UpdateWebhookEndpointRequest": {"type": "object", "properties": {"url": {"type": "string", "format": "uri", "maxLength": 2048}, "description": {"type": ["string", "null"], "maxLength": 160}, "status": {"type": "string", "description": "Prender y apagar el endpoint entero, sin perder sus suscripciones\nni su secreto.", "enum": ["enabled", "disabled"]}}, "title": "UpdateWebhookEndpointRequest"}, "UpdateWebhookSubscriptionRequest": {"type": "object", "description": "El cuerpo de `PUT api/webhooks/{id}`: reemplaza `url`, `events`, `description`\ny `filters`; `status` es opcional y solo admite `active` o `paused` (el cliente\nno puede dejarla `disabled`).", "properties": {"url": {"type": "string", "format": "uri", "maxLength": 2048}, "description": {"type": ["string", "null"], "maxLength": 160}, "events": {"type": "array", "items": {"type": "string", "enum": ["device.disconnected", "device.connected", "billing.payment_failed", "billing.plan_blocked", "billing.upcoming_charge", "subscription.expiring", "subscription.trial_ending", "subscription.reactivated", "subscription.cancellation_confirmed", "subscription.dormant_ending", "ai_quota.low_balance", "ai_quota.topup_succeeded", "ai_quota.topup_failed", "mac.usage_80", "mac.usage_100", "mac.expiring", "account.blocked", "message.received", "flow.completed", "broadcast.finished", "contact.stage_changed", "message.status_changed", "conversation.closed", "conversation.assigned", "contact.tag_added", "broadcast.paused", "contact.tag_removed", "contact.created", "template.status_changed", "flow.failed", "whatsapp_form.completed", "group.created", "group.invite_link_down", "group.invite_link_renewed", "group_participant.joined", "group_participant.left", "scheduled_message.group_delivery_failed", "scheduled_message.response_received", "poll.voted", "pipeline.finished", "group.blocked", "group.recovered", "campaign.status_changed", "scheduled_message.finished", "group_action.failed", "daily_report.generated", "campaign.no_groups_available", "group.full", "group_action.completed", "group.message_received", "group.reaction_added", "group_join_request.created", "group_join_request.revoked"]}, "minItems": 1, "maxItems": 50, "uniqueItems": true}, "filters": {"type": "object", "properties": {"campaign_ids": {"type": ["array", "null"], "items": {"type": "string", "pattern": "^[A-Za-z0-9_-]+$", "minLength": 6, "maxLength": 64}, "maxItems": 100}}}, "status": {"type": "string", "enum": ["active", "paused"]}}, "required": ["url", "events"], "title": "UpdateWebhookSubscriptionRequest"}, "UpsertAlertSettingRequest": {"type": "object", "properties": {"module": {"$ref": "#/components/schemas/AlertModule"}, "action": {"$ref": "#/components/schemas/AlertAction"}, "enabled": {"type": "boolean"}}, "required": ["module", "action", "enabled"], "title": "UpsertAlertSettingRequest"}, "VerifyAlertOtpRequest": {"type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"], "title": "VerifyAlertOtpRequest"}, "WabaCapiDatasetResource": {"type": "object", "properties": {"waba_id": {"type": "string"}, "dataset_id": {"type": ["string", "null"]}, "scope_ok": {"type": "boolean"}, "consented_at": {"type": ["string", "null"]}, "last_verified_at": {"type": ["string", "null"]}}, "required": ["waba_id", "dataset_id", "scope_ok", "consented_at", "last_verified_at"], "title": "WabaCapiDatasetResource"}, "WebhookDeliveryResource": {"type": "object", "properties": {"id": {"type": "integer"}, "event_id": {"type": "string", "description": "El id del sobre: agrupa los intentos de un mismo evento y es el\nmismo valor que el receptor vio en el cuerpo."}, "event_type": {"type": "string"}, "attempt": {"type": "integer"}, "url": {"type": "string"}, "response_status": {"type": ["integer", "null"]}, "succeeded": {"type": "boolean"}, "error": {"type": ["string", "null"]}, "duration_ms": {"type": ["integer", "null"]}, "attempted_at": {"type": "string"}}, "required": ["id", "event_id", "event_type", "attempt", "url", "response_status", "succeeded", "error", "duration_ms", "attempted_at"], "title": "WebhookDeliveryResource"}, "WebhookEndpointResource": {"type": "object", "properties": {"id": {"type": "integer"}, "url": {"type": "string"}, "description": {"type": ["string", "null"]}, "status": {"type": "string"}, "disabled_reason": {"type": ["string", "null"]}, "events": {"type": "array", "items": {"type": "object", "properties": {"event_type": {"type": "string"}, "enabled": {"type": "boolean"}}, "required": ["event_type", "enabled"]}}, "health": {"type": "object", "properties": {"consecutive_failures": {"type": "integer"}, "last_delivery_at": {"type": ["string", "null"]}, "last_success_at": {"type": ["string", "null"]}}, "required": ["consecutive_failures", "last_delivery_at", "last_success_at"]}, "secret_rotated_at": {"type": ["string", "null"]}, "created_at": {"type": ["string", "null"]}, "updated_at": {"type": ["string", "null"]}}, "required": ["id", "url", "description", "status", "disabled_reason", "health", "secret_rotated_at", "created_at", "updated_at"], "title": "WebhookEndpointResource"}, "WebhookSubscriptionCreatedResource": {"type": "object", "properties": {"id": {"type": "string"}, "url": {"type": "string"}, "description": {"type": ["string", "null"]}, "events": {"type": "array", "items": {}}, "filters": {"description": "Siempre un objeto: `{}` si no hay filtro.", "anyOf": [{"type": "object", "additionalProperties": {}}, {"type": "null"}, {"type": "object"}]}, "status": {"type": "string"}, "disabled_reason": {"type": ["string", "null"]}, "last_delivery_at": {"type": ["string", "null"]}, "last_success_at": {"type": ["string", "null"]}, "secret_rotated_at": {"type": ["string", "null"]}, "created_at": {"type": ["string", "null"]}, "updated_at": {"type": ["string", "null"]}, "migration": {"type": ["object", "null"], "properties": {"origin": {"type": "string"}, "status": {"type": "string"}, "activated_via": {"type": ["string", "null"]}, "activated_at": {"type": ["string", "null"]}, "last_result": {"type": ["string", "null"]}}, "required": ["origin", "status", "activated_via", "activated_at", "last_result"]}, "secret": {"type": "string"}, "secret_notice": {"type": "string", "const": "Guardá este secreto ahora: es la única vez que se muestra. Con él validás la firma del header de cada entrega."}}, "required": ["id", "url", "description", "filters", "status", "disabled_reason", "last_delivery_at", "last_success_at", "secret_rotated_at", "created_at", "updated_at", "migration", "secret", "secret_notice"], "title": "WebhookSubscriptionCreatedResource"}, "WebhookSubscriptionDeliveryResource": {"type": "object", "properties": {"id": {"type": "integer"}, "event_id": {"type": "string", "description": "El id del sobre: agrupa los intentos de un mismo evento y es el\nmismo valor que el receptor vio en el cuerpo."}, "event_type": {"type": "string"}, "attempt": {"type": "integer"}, "url": {"type": "string"}, "response_status": {"type": ["integer", "null"]}, "succeeded": {"type": "boolean"}, "error": {"type": ["string", "null"]}, "duration_ms": {"type": ["integer", "null"]}, "attempted_at": {"type": "string"}}, "required": ["id", "event_id", "event_type", "attempt", "url", "response_status", "succeeded", "error", "duration_ms", "attempted_at"], "title": "WebhookSubscriptionDeliveryResource"}, "WebhookSubscriptionResource": {"type": "object", "properties": {"id": {"type": "string"}, "url": {"type": "string"}, "description": {"type": ["string", "null"]}, "events": {"type": "array", "items": {}}, "filters": {"description": "Siempre un objeto: `{}` si no hay filtro.", "anyOf": [{"type": "object", "additionalProperties": {}}, {"type": "null"}, {"type": "object"}]}, "status": {"type": "string"}, "disabled_reason": {"type": ["string", "null"]}, "last_delivery_at": {"type": ["string", "null"]}, "last_success_at": {"type": ["string", "null"]}, "secret_rotated_at": {"type": ["string", "null"]}, "created_at": {"type": ["string", "null"]}, "updated_at": {"type": ["string", "null"]}, "migration": {"type": ["object", "null"], "properties": {"origin": {"type": "string"}, "status": {"type": "string"}, "activated_via": {"type": ["string", "null"]}, "activated_at": {"type": ["string", "null"]}, "last_result": {"type": ["string", "null"]}}, "required": ["origin", "status", "activated_via", "activated_at", "last_result"]}}, "required": ["id", "url", "description", "filters", "status", "disabled_reason", "last_delivery_at", "last_success_at", "secret_rotated_at", "created_at", "updated_at", "migration"], "title": "WebhookSubscriptionResource"}, "WhatsAppProviderEnum": {"type": "integer", "enum": [1, 2, 3, 4], "title": "WhatsAppProviderEnum"}, "WhatsappCostSummaryResource": {"type": "object", "additionalProperties": {}, "title": "WhatsappCostSummaryResource"}}, "responses": {"AccountsAuthenticationException": {"description": "Unauthenticated", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "description": "Error overview."}}, "required": ["message"]}}}}, "AccountsAuthorizationException": {"description": "Authorization error", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "description": "Error overview."}}, "required": ["message"]}}}}, "AccountsModelNotFoundException": {"description": "Not found", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "description": "Error overview."}}, "required": ["message"]}}}}, "AccountsValidationException": {"description": "Validation error", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "description": "Errors overview."}, "errors": {"type": "object", "description": "A detailed description of each field that failed validation.", "additionalProperties": {"type": "array", "items": {"type": "string"}}}}, "required": ["message", "errors"]}}}}, "CommunitiesAuthenticationException": {"description": "Unauthenticated", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "description": "Error overview."}}, "required": ["message"]}}}}, "CommunitiesAuthorizationException": {"description": "Authorization error", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "description": "Error overview."}}, "required": ["message"]}}}}, "CommunitiesModelNotFoundException": {"description": "Not found", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "description": "Error overview."}}, "required": ["message"]}}}}, "CommunitiesValidationException": {"description": "Validation error", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string", "description": "Errors overview."}, "errors": {"type": "object", "description": "A detailed description of each field that failed validation.", "additionalProperties": {"type": "array", "items": {"type": "string"}}}}, "required": ["message", "errors"]}}}}, "ConversationsAuthenticationException": {"description": "Unauthorized", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string"}}, "required": ["message"]}}}}, "ConversationsAuthorizationException": {"description": "Forbidden", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string"}}, "required": ["message"]}}}}, "ConversationsModelNotFoundException": {"description": "Not Found", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string"}}, "required": ["message"]}}}}, "ConversationsValidationException": {"description": "Unprocessable Content", "content": {"application/json": {"schema": {"type": "object", "properties": {"message": {"type": "string"}, "errors": {"type": "object", "additionalProperties": {"type": "array", "items": {"type": "string"}}}}, "required": ["message", "errors"]}}}}}, "securitySchemes": {"http": {"type": "http", "description": "Tu API key de Funnelchat, enviada en cada llamada como `Authorization: Bearer <id>|fc_live_...`.", "scheme": "bearer"}}}, "x-fc-sources": [{"domain": "conversations", "title": "Funnelchat Conversations", "version": "0.0.1", "sha": "68cce1b4e", "branch": "snapshot/referencia-mac-2210", "tree": "limpio", "snapshot_at": "2026-10-22T16:30:35-03:00", "spec": "specs/conversations.json", "operations": 125, "excluded_routes": 20, "description": "Public API of Funnelchat Conversations. Authenticate every request with `Authorization: Bearer <id>|fc_live_...`. Each operation shows the permission (`conversations:read`, `conversations:write` or `conversations:send`) your key needs.\n\n## Webhooks\n\nLos webhooks avisan a un sistema externo, por HTTP, cuando algo pasa en tu cuenta, sin que tengas que consultar la API cada tanto. Registrás una URL HTTPS, elegís los tipos de evento que querés recibir y Funnelchat envía a esa URL un `POST` firmado cada vez que ocurre uno. Las rutas de administración requieren una clave de API y que la use el titular de la cuenta.\n\n### Cómo suscribirse\n\nEstas son las nueve rutas para administrar suscripciones:\n\n| Ruta | Para qué sirve | Permiso |\n| --- | --- | --- |\n| `GET /api/v1/webhooks` | Listar las suscripciones | `conversations:read` |\n| `POST /api/v1/webhooks` | Crear una suscripción | `conversations:write` |\n| `GET /api/v1/webhooks/events` | Ver el catálogo de tipos de evento | `conversations:read` |\n| `GET /api/v1/webhooks/{webhook}` | Ver una suscripción | `conversations:read` |\n| `PUT /api/v1/webhooks/{webhook}` | Cambiar URL, tipos o estado activo | `conversations:write` |\n| `DELETE /api/v1/webhooks/{webhook}` | Eliminar una suscripción | `conversations:write` |\n| `POST /api/v1/webhooks/{webhook}/rotate-secret` | Generar un secreto nuevo | `conversations:write` |\n| `POST /api/v1/webhooks/{webhook}/test` | Enviar ahora un evento de prueba | `conversations:write` |\n| `GET /api/v1/webhooks/{webhook}/deliveries` | Ver el historial de entregas | `conversations:read` |\n\nEjemplo de alta:\n\n```bash\ncurl -X POST \"https://TU-HOST/api/v1/webhooks\" \\\n  -H \"Authorization: Bearer <id>|fc_live_...\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"url\": \"https://example.com/hooks/funnelchat\",\n    \"description\": \"Mi integración\",\n    \"events\": [\"message.received\"]\n  }'\n```\n\nRespuesta `201`:\n\n```json\n{\n  \"data\": {\n    \"uuid\": \"00000000-0000-4000-8000-000000000001\",\n    \"url\": \"https://example.com/hooks/funnelchat\",\n    \"description\": \"Mi integración\",\n    \"events\": [\"message.received\"],\n    \"active\": true,\n    \"inactive_reason\": null,\n    \"created_at\": \"2026-10-01T12:00:00+00:00\",\n    \"updated_at\": \"2026-10-01T12:00:00+00:00\",\n    \"secret\": \"<secreto de firma>\"\n  }\n}\n```\n\nEl `secret` se muestra **una sola vez**, en esta respuesta (y al rotarlo). Guardalo: no se puede volver a consultar. En `events` podés usar los tipos `message.received`, `flow.completed`, `broadcast.finished`, `contact.stage_changed`, `message.status_changed`, `conversation.closed`, `conversation.assigned`, `contact.tag_added`, `broadcast.paused`, `contact.tag_removed`, `contact.created`, `template.status_changed`, `flow.failed`, `whatsapp_form.completed` o `*` para todos los demás que se puedan suscribir (`message.status_changed` siempre se suscribe de forma explícita: `*` no lo incluye). La URL debe ser `https` y apuntar a un destino público.\n\n### El sobre\n\nCada entrega es un `POST` con un cuerpo JSON con esta forma. El contenido de `data` depende del tipo de evento (ver el catálogo más abajo).\n\n| Campo | Descripción |\n| --- | --- |\n| `id` | Identificador único del evento (`evt_...`). Es el mismo en todos los reintentos. |\n| `type` | Tipo de evento, por ejemplo `message.received`. |\n| `version` | Versión del formato del evento (hoy `1`). |\n| `occurred_at` | Momento en que ocurrió, en UTC (ISO 8601). |\n| `data` | Datos del evento. |\n\n### Cabeceras\n\n| Cabecera | Contenido |\n| --- | --- |\n| `Content-Type` | `application/json` |\n| `X-Funnelchat-Signature` | Firma de la entrega, con la forma `t=<timestamp>,v1=<firma>` |\n| `X-Funnelchat-Event` | Tipo de evento (igual que `type` en el cuerpo) |\n| `X-Funnelchat-Delivery` | Identificador de esta entrega (cambia en cada intento) |\n\n### Cómo verificar la firma\n\nCada entrega lleva la cabecera `X-Funnelchat-Signature` con la forma `t=<timestamp>,v1=<firma>`. Para verificarla:\n\n1. Tomá el **cuerpo crudo** de la petición, tal como llegó, sin volver a codificar el JSON.\n2. Armá el texto `t + \".\" + cuerpo` (el timestamp, un punto y el cuerpo).\n3. Calculá su HMAC-SHA256 con el secreto de la suscripción y comparalo, en tiempo constante, con `v1`.\n4. Rechazá la entrega si `t` se aleja del reloj de tu servidor más de 300 segundos.\n\nEjemplo en PHP:\n\n```php\nfunction verificarFirma(string $cuerpoCrudo, string $cabecera, string $secreto, int $tolerancia = 300): bool\n{\n    parse_str(str_replace(',', '&', $cabecera), $partes);\n\n    if (! isset($partes['t'], $partes['v1']) || ! ctype_digit($partes['t'])) {\n        return false;\n    }\n\n    if (abs(time() - (int) $partes['t']) > $tolerancia) {\n        return false;\n    }\n\n    $esperada = hash_hmac('sha256', $partes['t'].'.'.$cuerpoCrudo, $secreto);\n\n    return hash_equals($esperada, $partes['v1']);\n}\n```\n\nEjemplo en JavaScript (Node.js):\n\n```javascript\nconst crypto = require('crypto');\n\nfunction verificarFirma(cuerpoCrudo, cabecera, secreto, tolerancia = 300) {\n  const partes = Object.fromEntries(cabecera.split(',').map((p) => p.split('=')));\n  if (!/^\\d+$/.test(partes.t || '') || !partes.v1) return false;\n\n  if (Math.abs(Date.now() / 1000 - Number(partes.t)) > tolerancia) return false;\n\n  const esperada = crypto\n    .createHmac('sha256', secreto)\n    .update(`${partes.t}.${cuerpoCrudo}`)\n    .digest('hex');\n\n  const a = Buffer.from(esperada);\n  const b = Buffer.from(partes.v1);\n\n  return a.length === b.length && crypto.timingSafeEqual(a, b);\n}\n```\n\nEn JavaScript, leé el cuerpo como texto o `Buffer` antes de convertirlo a objeto (por ejemplo con `express.raw({ type: 'application/json' })`).\n\n### Reintentos\n\nUna entrega es exitosa si tu servidor responde con un código `2xx` en 10 segundos o menos. Funnelchat no sigue redirecciones.\n\n- Se hacen hasta **4 intentos** por evento y suscripción. Entre un intento y el siguiente se espera 60, 300, 1800 segundos.\n- Se reintenta cuando hay un error de red o un timeout, ante un `5xx` y ante un `429`.\n- Un `3xx` o cualquier otro `4xx` es terminal: no se reintenta.\n- Después del último intento fallido la entrega queda como `failed`, sin más reintentos.\n\n### Estados de entrega\n\n`GET /api/v1/webhooks/{webhook}/deliveries` devuelve un registro por intento, con estos estados:\n\n| Estado | Significado |\n| --- | --- |\n| `pending` | El intento está en curso: todavía no hay resultado. |\n| `succeeded` | Tu servidor respondió `2xx`. |\n| `failed` con `next_attempt_at` | El intento falló y hay otro programado para esa fecha. |\n| `failed` sin `next_attempt_at` | El intento falló y no habrá más: era terminal o se agotaron los intentos. |\n\n### Probar una suscripción\n\n`POST /api/v1/webhooks/{webhook}/test` envía ahora un evento `webhook.test` firmado a la URL de la suscripción, sin esperar a que ocurra nada en tu cuenta, y responde con el resultado del intento. Funciona también sobre una suscripción inactiva, para probarla antes de activarla.\n\nLa respuesta es un ítem del historial de entregas, con la misma forma que cada elemento de `GET /api/v1/webhooks/{webhook}/deliveries`:\n\n```json\n{\n  \"data\": {\n    \"uuid\": \"0d3c2f4e-6a1b-4c8d-9e2f-5b7a1c3d4e6f\",\n    \"event_id\": \"evt_01hexample\",\n    \"event_type\": \"webhook.test\",\n    \"attempt\": 1,\n    \"status\": \"succeeded\",\n    \"http_status\": 200,\n    \"duration_ms\": 184,\n    \"error\": null,\n    \"next_attempt_at\": null,\n    \"created_at\": \"2026-01-15T12:00:00+00:00\",\n    \"updated_at\": \"2026-01-15T12:00:00+00:00\"\n}\n}\n```\n\n`status` es `succeeded` solo si tu servidor respondió `2xx`; si no, es `failed` y `error` indica la causa. La prueba aparece en el historial con `event_type: \"webhook.test\"`, no se reintenta y no consume tu cupo de eventos.\n\n### Suscripciones creadas con una clave de API\n\nUna suscripción creada, o reactivada, con una clave de API queda atada a esa clave. También la atan a una clave cambiar su URL con esa clave, o rotar su secreto con esa clave (rotarlo desde la sesión web no modifica el vínculo): la suscripción queda atada a la última credencial que fijó su destino. Si la clave se revoca (o vence, que cuenta como revocada), la suscripción deja de recibir eventos: pasa a `active: false` con `inactive_reason: \"key_revoked\"`, y no se le envían más eventos.\n\nEse cambio se ve a partir del primer evento que ocurra después de revocar la clave. Hasta entonces, un `GET` puede seguir mostrándola activa, pero no recibe eventos.\n\nLa prueba manual (`POST /api/v1/webhooks/{webhook}/test`) sigue funcionando sobre una suscripción inactiva.\n\nUna suscripción con `inactive_reason: \"key_revoked\"` no se puede reactivar: `PUT /api/v1/webhooks/{webhook}` con `\"active\": true` responde 422, sin importar la credencial que se use. Para seguir recibiendo eventos, creá una suscripción nueva con `POST /api/v1/webhooks`. Una suscripción que desactivaste a mano sí se puede reactivar con `PUT` y `\"active\": true`; queda atada a la credencial que la reactiva. Mientras esté activa, `inactive_reason` es `null`.\n\nLas suscripciones cuyo destino fijó por última vez la sesión web no dependen de ninguna clave: revocar una clave no las afecta.\n\n### Idempotencia\n\nUn mismo evento puede llegarte más de una vez (por ejemplo, si tu servidor procesó la entrega pero respondió tarde). Usá el campo `id` del sobre para descartar los que ya procesaste: es el mismo en todos los reintentos del evento.\n\n### Eventos disponibles\n\nSe generan desde el catálogo vigente. Los tipos marcados como **planificado** todavía no se emiten: se anuncian para que puedas prepararte, pero no los recibirás.\n\n`webhook.test` solo se envía con `POST /api/v1/webhooks/{webhook}/test`.\n\n#### `message.received`\n\nUn contacto le escribió a un número de WhatsApp de la cuenta.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000001\",\n    \"type\": \"message.received\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"message\": {\n            \"id\": 1001,\n            \"type\": \"text\",\n            \"text\": \"Hola, quiero más información\",\n            \"media\": null\n        },\n        \"contact\": {\n            \"id\": 2001,\n            \"name\": \"Contacto de ejemplo\",\n            \"phone\": \"+5491100000000\"\n        },\n        \"receiver\": {\n            \"phone\": \"+5491100000001\"\n        },\n        \"device_id\": 4521\n    }\n}\n```\n\nValores posibles de `data.message.type`: `text`, `image`, `video`, `document`, `sticker`, `contact`, `audio`, `note`, `action`, `invalid`, `interactive`, `location`, `voice_call`, `video_call`, `event`, `button`, `template`, `notification`, `option_list`, `template_button_reply`, `poll`, `whatsapp_form`, `call_permission_request`, `call_permission_reply`, `request_contact_info`.\n\n`device_id` es el número de tu cuenta que recibió el mensaje: el mismo `device_id` que mandas para responder por ese número. Es `null` si el mensaje no tiene número asociado.\n\n#### `flow.completed`\n\nUn contacto terminó un flujo de automatización.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000003\",\n    \"type\": \"flow.completed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"flow\": {\n            \"id\": 3001,\n            \"name\": \"Flujo de ejemplo\"\n        },\n        \"contact\": {\n            \"id\": 2001,\n            \"name\": \"Contacto de ejemplo\",\n            \"phone\": \"+5491100000000\"\n        }\n    }\n}\n```\n\nSe envía una sola vez por cada ejecución de un flujo, cuando termina con éxito. No se envía si la ejecución se cortó, se canceló, quedó bloqueada o falló. `contact` es `null` si la ejecución no tiene un contacto asociado. Un flujo muy usado puede generar muchos eventos seguidos.\n\n#### `broadcast.finished`\n\nUna campaña llegó a su estado final (enviada, fallida o con fallos parciales), con sus conteos.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000011\",\n    \"type\": \"broadcast.finished\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"broadcast\": {\n            \"id\": 6001,\n            \"name\": \"Campaña de ejemplo\"\n        },\n        \"status\": \"partially_failed\",\n        \"reason\": \"completed\",\n        \"totals\": {\n            \"processed\": 1200,\n            \"failed\": 30,\n            \"skipped_unreachable\": 12\n        }\n    }\n}\n```\n\n`status` es `success`, `failed` o `partially_failed`. `reason` dice por qué terminó: `completed` (se enviaron todas las páginas), `cancelled` (se cerró una campaña pausada), `template_rejected` (Meta rechazó la plantilla, o su categoría cambió, mientras la campaña esperaba), `approval_expired` (la plantilla no se aprobó a tiempo), `mac_lockout` (la cuenta alcanzó su límite de contactos activos), `no_recipients` (la audiencia quedó vacía) o `error` (el envío no pudo despacharse). `totals` son los conteos del envío: `processed` (destinatarios procesados), `failed` y `skipped_unreachable` (destinatarios que el canal no puede alcanzar); valen 0 si la campaña terminó antes de enviar. Se emite una sola vez por campaña.\n\n#### `contact.stage_changed`\n\nUn contacto cambió de etapa en un tablero.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000008\",\n    \"type\": \"contact.stage_changed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"contact\": {\n            \"id\": 2001,\n            \"name\": \"Contacto de ejemplo\",\n            \"phone\": \"+5491100000000\"\n        },\n        \"board\": {\n            \"id\": 5001,\n            \"name\": \"Embudo de ejemplo\"\n        },\n        \"kind\": \"move\",\n        \"from_stage\": {\n            \"id\": 6001,\n            \"name\": \"Nuevo\",\n            \"position\": 0\n        },\n        \"to_stage\": {\n            \"id\": 6002,\n            \"name\": \"En seguimiento\",\n            \"position\": 1\n        }\n    }\n}\n```\n\nSe envía un evento por cada contacto que cambia de etapa en un tablero por etapas. `kind` es entry (entra al tablero, `from_stage` es null), move (pasa de una etapa a otra), exit (sale del tablero, `to_stage` es null) o stage_deleted (se reubica porque su etapa se borró; el nombre de la etapa borrada es null). No se envía si el contacto se coloca en la etapa en la que ya estaba.\n\n#### `message.status_changed`\n\nCambió el estado de entrega de un mensaje enviado.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000014\",\n    \"type\": \"message.status_changed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"message_id\": 1001,\n        \"contact_id\": 2001,\n        \"device_id\": 4521,\n        \"status\": \"failed\",\n        \"previous_status\": \"sent\",\n        \"error\": {\n            \"code\": 131047,\n            \"title\": \"Re-engagement message\",\n            \"reason\": null\n        },\n        \"occurred_at\": \"2026-10-01T12:00:00Z\",\n        \"client_reference\": \"pedido-1001\"\n    }\n}\n```\n\nSe emite cuando cambia el estado de entrega de un mensaje que enviaste a un contacto, ya sea desde el chat, una automatización o un asistente de IA (incluye los enviados con clave). No se emite para mensajes de difusiones ni para mensajes recibidos. `message_id` es el id que devuelve la API, `contact_id` el del contacto y `device_id` el número de tu cuenta por el que salió el mensaje, el mismo `device_id` que usas para enviar (`null` si no tiene). `status` es `sent`, `delivered`, `read` o `failed`. `previous_status` es el estado que el mensaje tenía justo antes de este cambio y es `null` en tres casos: es el primer aviso del mensaje, es el mismo estado que `status` (por ejemplo, el proveedor confirma el `sent` que ya registró el envío) o no se pudo leer. `error` solo viene cuando `status` es `failed`: `code` es el código numérico del proveedor y `title` su título, y cualquiera de los dos puede ser `null` si el proveedor no los informa. `error.reason` es `null` cuando el fallo lo informó el proveedor; cuando Funnelchat decidió que un mensaje enviado con clave no salía (por ejemplo, por saldo insuficiente o ventana de 24 horas cerrada) trae el motivo (`device_unavailable`, `insufficient_balance`, `window_closed`, `recipient_unreachable`, `template_not_found`, `template_unresolved_placeholder`, `not_queued` o `not_sent`), `code` es `null` y `title` es un texto llano en el idioma de la cuenta. `occurred_at` es el momento en que Funnelchat registró el estado, en UTC. `client_reference` es la referencia que mandaste al enviar el mensaje (por ejemplo `pedido-1001`) y `null` si el envío no la llevaba: sirve para relacionar cada aviso con tu envío también cuando la llamada de envío no respondió. Cada estado se informa una sola vez por mensaje: si llegan varios avisos juntos se informa el más avanzado y un aviso atrasado que no cambia el estado no genera evento. Un mensaje que no llegó a salir (por ejemplo, por ventana de 24 horas cerrada) no recibe avisos de estado, salvo uno enviado con clave: ese recibe un único aviso `failed` con `previous_status` en `null`, su `error.reason` y su `client_reference`. Un mensaje enviado con clave cuyo resultado no se conoce (el proveedor no respondió o el envío se interrumpió) no recibe aviso. Un mensaje cuyo envío no empezó dentro de la hora ya no sale: pasada la hora sin resultado, su estado es `null`, o `failed` con `error.reason` `not_sent` y su aviso si su envío llega tarde. Reintentar es un envío nuevo, que sólo puede duplicar si el envío había empezado y se cortó. Un mensaje `queued` no es enviado: su resultado llega por este evento o por la consulta del mensaje. En cuentas con mucho tráfico, los eventos que excedan el límite de entrega por minuto de la cuenta se descartan. Para recibirlo debes suscribirte a `message.status_changed` de forma explícita: una suscripción a `*` no lo incluye.\n\n#### `conversation.closed`\n\nUna conversación pasó a cerrada.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000004\",\n    \"type\": \"conversation.closed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"contact\": {\n            \"id\": 2001,\n            \"name\": \"Contacto de ejemplo\",\n            \"phone\": \"+5491100000000\"\n        }\n    }\n}\n```\n\nSe envía cuando una conversación pasa de abierta a cerrada. No se envía si la conversación ya estaba cerrada ni cuando se reabre.\n\n#### `conversation.assigned`\n\nUna conversación quedó asignada a una persona del equipo o a un agente de IA.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000005\",\n    \"type\": \"conversation.assigned\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"contact\": {\n            \"id\": 2001,\n            \"name\": \"Contacto de ejemplo\",\n            \"phone\": \"+5491100000000\"\n        },\n        \"assignee\": {\n            \"type\": \"human\",\n            \"name\": \"Agente de ejemplo\"\n        }\n    }\n}\n```\n\nSe envía cuando una conversación queda asignada a una persona del equipo (`assignee.type` = `human`) o a un agente de IA (`assignee.type` = `ai`), por cualquier camino. `assignee` trae el tipo y el nombre, nunca un identificador. No se envía al desasignar, ni cuando se vuelve a enviar el mismo asignado.\n\n#### `contact.tag_added`\n\nUn contacto recibió una etiqueta.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000006\",\n    \"type\": \"contact.tag_added\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"contact\": {\n            \"id\": 2001,\n            \"name\": \"Contacto de ejemplo\",\n            \"phone\": \"+5491100000000\"\n        },\n        \"tag\": {\n            \"id\": 4001,\n            \"name\": \"Etiqueta de ejemplo\"\n        },\n        \"source\": \"manual\"\n    }\n}\n```\n\nSe envía un evento por cada contacto y etiqueta agregados, también en operaciones en bloque; no se envía si el contacto ya tenía la etiqueta. `source` indica el origen: manual, flow, bulk, import, sync, walink o ai. En operaciones en bloque grandes el límite de volumen de la cuenta puede descartar parte de los eventos.\n\n#### `broadcast.paused`\n\nUna campaña se pausó sola, por ejemplo porque bajó la calidad del número.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000007\",\n    \"type\": \"broadcast.paused\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"broadcast\": {\n            \"id\": 6001,\n            \"name\": \"Campaña de ejemplo\"\n        },\n        \"reason\": \"quality_degraded\",\n        \"quality\": \"YELLOW\",\n        \"quality_baseline\": \"GREEN\"\n    }\n}\n```\n\n`reason` es `quality_degraded` (la calidad del número bajó respecto de la del inicio, o llegó a RED) o `quality_unreadable` (no se pudo leer la calidad en tres controles seguidos). `quality` y `quality_baseline` son la calidad de Meta leída en el control y la del inicio del envío (`GREEN`, `YELLOW`, `RED`), o `null` si no se pudo leer. Sólo las campañas con envío inteligente se pausan solas. Reanudar la campaña y que se pause otra vez emite otro evento.\n\n#### `contact.tag_removed`\n\nUn contacto perdió una etiqueta.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000009\",\n    \"type\": \"contact.tag_removed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"contact\": {\n            \"id\": 2001,\n            \"name\": \"Contacto de ejemplo\",\n            \"phone\": \"+5491100000000\"\n        },\n        \"tag\": {\n            \"id\": 4001,\n            \"name\": \"Etiqueta de ejemplo\"\n        },\n        \"source\": \"manual\"\n    }\n}\n```\n\nSe envía un evento por cada contacto y etiqueta quitados, también en operaciones en bloque; no se envía si el contacto no tenía la etiqueta. No se envía al borrar la etiqueta, al borrar o fusionar contactos ni al desvincular etiquetas de un tablero. `source` indica el origen: manual, flow, bulk o ai. En operaciones en bloque grandes el límite de volumen de la cuenta puede descartar parte de los eventos.\n\n#### `contact.created`\n\nSe creó un contacto nuevo en la cuenta.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000010\",\n    \"type\": \"contact.created\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"contact\": {\n            \"id\": 2001,\n            \"name\": \"Contacto de ejemplo\",\n            \"phone\": \"+5491100000000\",\n            \"email\": \"contacto@example.com\"\n        }\n    }\n}\n```\n\nSe envía un evento por cada contacto nuevo, sea cual sea el canal: un mensaje entrante, la creación manual, una importación o la API. No se envía cuando el contacto ya existía y sólo se actualiza, ni para los contactos que la plataforma crea para sus propias notificaciones. En importaciones grandes el límite de volumen de la cuenta puede descartar parte de los eventos.\n\n#### `template.status_changed`\n\nUna plantilla cambió de estado de aprobación en Meta.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000012\",\n    \"type\": \"template.status_changed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"template\": {\n            \"id\": 7001,\n            \"uid\": \"1234567890\",\n            \"name\": \"plantilla_ejemplo\",\n            \"category\": \"MARKETING\"\n        },\n        \"previous_status\": \"PENDING\",\n        \"status\": \"APPROVED\",\n        \"reason\": null\n    }\n}\n```\n\n`template` identifica la plantilla igual que `GET /templates` (`id`, `uid`, `name`, `category`). `previous_status` y `status` son los estados tal como los devuelve la API (por ejemplo `PENDING`, `APPROVED`, `REJECTED`, `PAUSED`); `previous_status` es `null` si la plantilla no tenía estado. `reason` trae la observación de rechazo (traducida como en la API y limitada a 500 caracteres) cuando el cambio es un rechazo informado al crear la plantilla, y es `null` en el resto. No se emite si Meta reenvía el estado que la plantilla ya tenía. Si una misma plantilla de Meta figura en varias cuentas, cada cuenta recibe su propio evento.\n\n#### `flow.failed`\n\nLa ejecución de un flujo para un contacto terminó por un error.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000013\",\n    \"type\": \"flow.failed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"flow\": {\n            \"id\": 3001,\n            \"name\": \"Flujo de ejemplo\"\n        },\n        \"contact\": {\n            \"id\": 2001,\n            \"name\": \"Contacto de ejemplo\",\n            \"phone\": \"+5491100000000\"\n        },\n        \"reason\": \"execution_error\"\n    }\n}\n```\n\nSe envía una sola vez por cada ejecución de un flujo que termina por un error, no porque llegó al final. `reason` indica el motivo y toma uno de estos valores: `node_missing` (el paso a ejecutar ya no existe), `unknown_node_type` (el paso es de un tipo no reconocido), `missing_subflow` (el flujo que debía ejecutarse dentro de otro ya no existe), `malformed_node` (el paso tiene una configuración dañada), `loop_detected` (la ejecución dio vueltas sin esperar nada) y `execution_error` (un error inesperado detuvo la ejecución). No se envía si la ejecución se canceló, quedó bloqueada o se cortó porque se editó el flujo o se unieron contactos. `contact` es `null` si la ejecución no tiene un contacto asociado.\n\n#### `whatsapp_form.completed`\n\nUn contacto completó un formulario de WhatsApp.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000015\",\n    \"type\": \"whatsapp_form.completed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"form\": {\n            \"id\": 4001,\n            \"name\": \"Captar un lead\",\n            \"version_id\": 4002\n        },\n        \"message_id\": 1001,\n        \"contact_id\": 2001,\n        \"device_id\": 5001,\n        \"answers\": [\n            {\n                \"key\": \"nombre\",\n                \"label\": \"Nombre\",\n                \"value\": \"Contacto de ejemplo\"\n            },\n            {\n                \"key\": \"intereses\",\n                \"label\": \"Intereses\",\n                \"value\": [\n                    \"op_a\",\n                    \"op_b\"\n                ]\n            },\n            {\n                \"key\": \"comentarios\",\n                \"label\": \"Comentarios\",\n                \"value\": null\n            }\n        ],\n        \"occurred_at\": \"2026-10-01T12:00:00Z\"\n    }\n}\n```\n\nSe envía cuando un contacto completa un formulario, aunque la respuesta llegue tarde. Puede llegar más de una vez con el mismo `id`: deduplica por `id`. Si el contacto vuelve a enviar el mismo formulario, no se genera otro aviso: queda la primera respuesta. Cada respuesta de texto llega con a lo sumo 600 caracteres. `form.id` es el formulario (el mismo `family_id` de la lectura de formularios) y `form.version_id` la versión que recibió el contacto. `answers` es una lista con una entrada por pregunta, en el orden del formulario: `key` es el nombre de la pregunta, `label` su etiqueta y `value` la respuesta (texto, número, `true` o `false`, una lista de textos en opción múltiple, o `null` si no la contestó). En las preguntas de lista y de opción múltiple, `value` es el id de la opción. `message_id` es el id del mensaje del formulario y es `null` si el envío no tuvo un mensaje público; `device_id` es el número de WhatsApp por el que salió, o `null` si ya no es de la cuenta. `contact_id` es `null` si el contacto se eliminó antes de que saliera el aviso (las respuestas llegan igual); si se fusionó con otro, es el id del contacto que quedó. Mientras se fusionan dos contactos, el id puede ser el del contacto que se está fusionando.\n\n#### `webhook.test`\n\nEvento de prueba enviado desde la ruta de prueba de una suscripción.\n\n```json\n{\n    \"id\": \"evt_000000000000000000000002\",\n    \"type\": \"webhook.test\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"subscription\": \"00000000-0000-4000-8000-000000000001\",\n        \"sent_at\": \"2026-10-01T12:00:00Z\"\n    }\n}\n```\n\n### Otros webhooks de la plataforma\n\nLos webhooks de alertas y el nodo «Petición HTTP» de las automatizaciones son otros productos, con otro formato y otra configuración. No se administran con las rutas de esta sección ni llevan este sobre ni esta firma.\n\n## Listados\n\nCon una clave de API, los listados siempre responden una página con la forma `{ \"data\": [...], \"links\": {...}, \"meta\": {...} }`: `data` trae los ítems de la página, `links` las URL de `first`, `last`, `prev` y `next` (`null` cuando no hay página), y `meta` el detalle (`current_page`, `from`, `last_page`, `links`, `path`, `per_page`, `to`, `total`).\n\n- **Tamaño de página:** el parámetro `perPage` es un entero entre 1 y 100. Si no lo mandás, la página trae 25 ítems. Un valor mayor que 100, menor que 1 o que no sea un entero responde 422 con el detalle en `errors.perPage`. Algunos listados aceptan además `per_page` como alias con los mismos límites; si mandás `perPage` y `per_page` con valores distintos, la respuesta es 422 con el detalle en `errors.perPage`.\n- **Orden:** cada listado documenta los parámetros de orden que admite y los valores permitidos de cada uno (por ejemplo `orderBy` y `orderByMethod`). Un parámetro de orden que el listado no admite, o un valor fuera de la lista, responde 422 con el detalle en `errors.<parámetro>`.\n- **Páginas siguientes:** las URL de `links` y de `meta.links` conservan `perPage`, el orden y los filtros que admite el listado; los demás parámetros no se copian y sólo cambia `page`. En los listados que se piden por `POST`, los filtros viajan en el cuerpo y las URL llevan sólo `perPage` y `page`.\n- **Primero el permiso:** si tu clave no tiene el permiso del listado, la respuesta es la de siempre (401 o 403), aunque mandes un `perPage` inválido.\n\n## Envío de mensajes con clave\n\nCuando envías con una clave de API, antes de enviar se aplican reglas de protección de tu número y de tus contactos. Si una regla frena el envío, la respuesta es un JSON `{ \"error\": \"<código>\", \"message\": \"<explicación>\" }` y no se envía nada ni se registra ningún mensaje. Cada operación de envío documenta los códigos que puede responder.\n- **Ritmo:** el límite depende sólo del tipo de número por el que envías; en los envíos no hay un límite por minuto por clave ni por cuenta. Al superarlo, la respuesta es 429 con `error` = `rate_limited` y el encabezado `Retry-After`, que trae los segundos que debes esperar antes de reintentar. Las reacciones sólo tienen esta regla.\n  - **Número de WhatsApp Cloud (API oficial):** hasta 60 mensajes por segundo y hasta 10 mensajes por minuto al mismo contacto.\n  - **Número en coexistencia** (el mismo teléfono conectado a WhatsApp Cloud y a la aplicación WhatsApp Business del celular): hasta 15 mensajes por segundo en total entre las dos conexiones, envíes por la que envíes. El límite de 10 mensajes por minuto al mismo contacto aplica sólo a lo que envías por WhatsApp Cloud.\n  - **Número conectado por código QR** (sin coexistencia): esta regla no limita sus envíos.\n- **Ventana de 24 horas:** por un número de WhatsApp Cloud sólo puedes enviar mensajes que no son plantilla a un contacto que te escribió en las últimas 24 horas. Fuera de esa ventana la respuesta es 422 con `error` = `window_closed`; para retomar la conversación envía una plantilla aprobada, que no tiene esta restricción.\n- **Antibloqueo:** en las últimas 24 horas, un mismo contenido se puede enviar como máximo 2 veces al mismo contacto (el siguiente responde 422 con `error` = `antiblock_duplicate_content`), y como máximo 5 mensajes seguidos sin que el contacto responda (el siguiente responde 422 con `error` = `antiblock_unanswered_limit`). Cuando el contacto responde, los contadores se reinician. Aplica a mensajes y a plantillas, no a reacciones.\n- **Destinatario:** `recipient_mismatch` (422) indica que el teléfono o el identificador de WhatsApp que mandaste en el cuerpo no corresponde al contacto de la ruta (sólo las operaciones que reciben ese dato en el cuerpo). `recipient_unsupported` (422) indica que el contacto es un grupo, una lista de difusión o un canal: por la API sólo se escribe a personas individuales.\n- **Estados de entrega:** el evento de webhook `message.status_changed` (enviado, entregado, leído, fallido) no se incluye al suscribirte a `*`: tienes que pedirlo de forma explícita en `events` al crear o editar la suscripción.\n- **Resultado de tus envíos:** cuando envías con una clave, la llamada no espera al proveedor: la respuesta trae el mensaje ya registrado y en cola, el mismo objeto que devuelve `GET /contacts/{contact}/messages/{message}`, y el envío sale enseguida después.\n  - **En cola no es enviado:** `status` `queued` solo dice que registramos el mensaje y que está por salir. El resultado llega por el evento `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`. Un mensaje `queued` no se reintenta: reenviarlo lo duplicaría.\n  - **Aceptado no es entregado:** `status` `sent` solo dice que el proveedor aceptó el mensaje. El estado final (entregado, leído o fallido) llega por el evento `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`.\n  - **`failed`:** el proveedor rechazó el mensaje dando un motivo, o Funnelchat lo frenó antes de enviarlo. No salió: reintentar es un intento nuevo y no lo duplica. Cuando lo decidió Funnelchat, `error.reason` trae el motivo, `error.code` es `null` y `error.title` es un texto llano en tu idioma; si lo rechazó el proveedor, `error.reason` es `null`. Qué hacer según `error.reason`: `device_unavailable`, reconecta el número en Funnelchat y envía de nuevo; `insufficient_balance`, recarga tu billetera de WhatsApp; `window_closed`, envía una plantilla aprobada; `recipient_unreachable`, revisa el teléfono del contacto; `template_not_found` o `template_unresolved_placeholder`, corrige la plantilla o sus variables; `not_queued` o `not_sent`, vuelve a intentarlo. Un mensaje enviado con clave que termina `failed` también llega por `message.status_changed`, con `previous_status` en `null` y su `client_reference`; un `status` `null` no recibe aviso.\n  - **`status` `null`:** el proveedor no confirmó ni rechazó el envío (no respondió, respondió con una falla sin motivo o el envío se interrumpió). Puede haber llegado al contacto, no recibirá avisos de estado y reintentarlo puede duplicarlo.\n  - **Más de una hora en `queued`:** un mensaje cuyo envío no empezó dentro de la hora ya no sale. Pasada la hora sin resultado, su `status` es `null`, o `failed` con `error.reason` `not_sent` y su aviso `message.status_changed` si su envío llega tarde. Reintentar es un envío nuevo, que sólo puede duplicar si el envío había empezado y se cortó.\n  - **Error 5xx o llamada sin respuesta:** busca tu `client_reference` en el historial del contacto antes de reintentar, y hazlo al menos un minuto después de la llamada. Si aparece, el mensaje quedó registrado: no lo reenvíes, su resultado llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`; la excepción son las tarjetas y los destinos de un reenvío tras un 5xx: si todos los que aparecen vienen `failed` con `not_queued`, se reintenta la llamada completa; si alguno aparece `queued`, quedó registrado: no lo reenvíes. Si no aparece, no salió nada y puedes reintentar. Repetir una referencia no evita duplicados: dos llamadas con la misma `client_reference` registran dos mensajes.\n  - **Error 404 en un envío:** el contacto no existe. En un reenvío, también cuando el mensaje de origen no existe o no es de ese contacto.\n  - **`POST /contacts/{contact}/message`:** la respuesta trae el mensaje en `message`: `queued` si es un mensaje, o el mensaje registrado de una nota.\n  - **`POST /contacts/{contact}/message/contact-card`:** la respuesta trae `messages`: un mensaje por tarjeta, en el orden de `contact_card_ids`, cada uno con su `id` y en cola (`queued`). Todas salen después, desde la cola: el resultado de cada una llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`, con tu `client_reference`. Si una tarjeta no pudo encolarse, su entrada viene `failed` con `error.reason` `not_queued`: no salió y volver a enviar esa tarjeta es un intento nuevo. Si alguna no pudo registrarse, la llamada responde un error 5xx y ninguna tarjeta salió: las que ya aparezcan en el historial del contacto vienen `failed` con `error.reason` `not_queued` y su propio aviso `message.status_changed` `failed`. Si todas las que aparecen vienen `failed` con `not_queued`, reintenta la llamada completa; si alguna aparece `queued`, quedó registrada: no la reenvíes.\n  - **Reenvío:** la respuesta trae un elemento por destino, en el orden de `targets`, y cada destino que quedó en cola trae su `message` con su `id` y en cola (`queued`). Todos salen después, desde la cola: el resultado de cada uno llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`, con tu `client_reference`. Un destino que no pudo encolarse viene `blocked` con `reason` `not_sent` y su `message` `failed` con `error.reason` `not_queued`: no salió y volver a reenviarlo a ese destino es un intento nuevo. Un destino `blocked` por MAC (`mac_limit_reached` o `mac_lockout_24h`) o con `reason` `target_not_found` (no existe o no es de tu cuenta) o `device_unavailable` trae `message` `null`: no salió nada. Si algún destino no pudo registrarse, la llamada responde un error 5xx y ninguno salió: los que ya aparezcan en el historial del contacto vienen `failed` con `error.reason` `not_queued` y su propio aviso `message.status_changed` `failed`. Si todos los que aparecen vienen `failed` con `not_queued`, reintenta la llamada completa; si alguno aparece `queued`, quedó registrado: no lo reenvíes.\n  - **`POST /contacts/{contact}/message/form`:** la respuesta trae solo `message`: el mensaje del formulario en cola (`queued`). Si no pudo ponerse en cola, la respuesta sigue siendo 200 con el mensaje en estado `failed` y `error.reason` `not_queued`: no salió, así que reintentar no lo duplica. Su resultado llega por `message.status_changed` o consultando `GET /contacts/{contact}/messages/{message}`, y la respuesta del contacto por `whatsapp_form.completed`. Si tu cuenta está en bloqueo por el límite de contactos activos responde 429 `mac_lockout_24h` y no guarda ningún mensaje ni `client_reference`. Sus errores propios están en la guía «Formularios de WhatsApp».\n  - **`POST /message/send-message` y `POST /devices/{device}/templates/{template_id}/send-template`:** la respuesta trae solo `message`: el mensaje en cola (`queued`), o el mensaje registrado de una nota en `send-message`. Si una variable de la plantilla se completa con un campo personalizado que no existe en tu cuenta, `send-template` responde 422 con `error` `template_unresolved_placeholder`, `placeholders` (hasta 20) y `count` (el total), y no registra ni envía nada; si la plantilla no existe en tu cuenta, responde 404 `template_not_found`.\n- **Correlacionar tus envíos:** manda `client_reference` en el cuerpo (texto opcional de 1 a 64 caracteres visibles, sin espacios) para identificar cada envío con tu propia referencia. La misma referencia vuelve en el historial de mensajes del contacto y en el evento `message.status_changed`, de modo que puedes relacionar cada aviso de estado con tu envío también cuando la llamada de envío no respondió. La referencia no es única (dos envíos pueden usar el mismo valor) y no está autenticada: la ven todas las claves y todas las suscripciones de la cuenta. Por eso relaciónala siempre junto con `message_id` y `contact_id`, y trátala como texto no confiable cuando la muestres. No pongas datos personales en la referencia. Una llamada que frena una regla de protección de esta sección (ritmo, ventana de 24 horas, antibloqueo o destinatario) no registra ningún mensaje ni referencia. Un rechazo por MAC (`mac_limit_reached` o `mac_lockout_24h`) es distinto: guarda un mensaje con estado de error que lleva la referencia; el envío de formularios (`POST /contacts/{contact}/message/form`) es la excepción: con `mac_lockout_24h` responde 429 y no guarda ningún mensaje ni referencia. Un mensaje enviado con clave que Funnelchat frena antes de que salga (por ejemplo, por saldo insuficiente) aparece en el historial con estado de error y su referencia, y recibe un aviso `failed` con esa referencia; uno enviado desde el panel no recibe avisos de estado. Las reacciones no aceptan `client_reference`.\n\n## Enviar un archivo por URL\nNo subes el archivo a Funnelchat: en `file` mandas la dirección (URL) del archivo y WhatsApp o el proveedor lo descarga desde ahí. Funnelchat no abre la URL al recibir tu llamada, así que un archivo inaccesible, vencido o fuera de los límites se conoce al enviarlo: el mensaje termina `failed` con el error del proveedor.\n- **La URL:** `file` admite hasta 4096 caracteres; más de eso se rechaza con un 422 en `file`. Si `file` empieza con `http`, en mayúsculas o minúsculas, tiene que ser una URL que empiece con `http://` o `https://`; si no (por ejemplo `httpfoto.jpg`), la respuesta es 422 con el error en `file`. Te recomendamos `https://`; `http://` también se acepta, pero viaja sin cifrar. Debe poder descargarse sin credenciales; puede ser una URL firmada. Tiene que seguir válida hasta que el mensaje sale: un envío con clave puede esperar en cola hasta 54 minutos, así que usa URLs que duren al menos una hora. No pongas secretos en la ruta ni en el dominio de la URL: en los registros de actividad (logs) del envío que escribe Funnelchat, de la URL quedan sólo el esquema, el dominio y la ruta (sin el usuario y la contraseña, la parte de consulta después de `?` ni el fragmento después de `#`) y del resultado sólo identificadores, el tipo del mensaje y, del error del proveedor, sus códigos y, sólo si tu número es de WhatsApp Cloud, su categoría, su título corto y su identificador de traza, nunca el texto de la respuesta del proveedor; la ruta y el dominio sí quedan. La URL completa viaja a WhatsApp o al proveedor de tu número, que la descarga y puede guardarla en sus propios registros. Además, la URL completa, con su consulta, se guarda en el historial del contacto, la ven los usuarios de tu cuenta y se copia al reenviar el mensaje: usa URLs firmadas de corta duración (al menos el tiempo de la cola, cerca de una hora) y limitadas a un solo archivo.\n- **El tipo:** decide la extensión de la ruta de la URL si es una de la lista; si no, el final de la URL completa si es una de la lista. Si la ruta no termina en una extensión de la lista y sólo lo hace el final de la URL completa (por ejemplo `https://cdn.example.com/video.mov?poster=frame.jpg`), el mensaje queda registrado con ese tipo, pero WhatsApp o el proveedor pueden enviarlo según la extensión de la ruta; si necesitas un tipo fijo, envía `file_type`:\n  - `.jpg`, `.jpeg`, `.png`: `image`\n  - `.webp`: `sticker`\n  - `.ogg`, `.oga`, `.mp3`, `.m4a`, `.aac`, `.opus`, `.wav`: `audio`\n  - `.gif`, `.mp4`: `video`\n  - `.doc`, `.docx`, `.pdf`: `document`\n- **Cuándo envías `file_type`:** si la URL no termina en una extensión reconocida (por ejemplo una URL firmada sin extensión), la respuesta es 422 con el error en `file_type`: agrega `file_type` con uno de estos valores: `image`, `video`, `audio`, `document`, `sticker`. Cuando lo envías, manda sobre la extensión: sirve también para un archivo cuya extensión no es de los tipos de la lista, como `.xlsx`, que se envía con `file_type` = `document` y `file_name` (por ejemplo `reporte.xlsx`), porque un documento cuya URL no termina en una extensión de la lista necesita su nombre. Si la URL termina en una extensión que no es la del archivo (como `/descarga.php?id=7`) y no envías `file_type`, la respuesta es 415 con la extensión recibida (`php`); con `file_type` se envía y, si es un documento, también hace falta `file_name`.\n- **Extensión no soportada:** si la ruta de la URL termina en un formato real que no enviamos solos (por ejemplo `.xlsx` o `.zip`), la URL completa no termina en una extensión de la lista y no mandas `file_type`, la respuesta es 415 con la extensión recibida.\n- **El nombre del archivo:** `file_name` (opcional, hasta 255 caracteres, sin `/`, `\\`, `?`, `#` ni caracteres de control) es el nombre con el que el contacto ve un documento; llévalo siempre con su extensión, por ejemplo `factura.pdf`. Es obligatorio cuando envías un documento por URL y la URL no termina en una extensión de la lista: sin él la respuesta es 422 con el error en `file_name`.\n- **Formatos y tamaños:** los formatos aceptados por WhatsApp y el tamaño máximo de cada tipo son:\n  - `image`: image/jpeg, image/png, image/webp; hasta 5 MB.\n  - `video`: video/mp4, video/3gpp, image/gif; hasta 16 MB.\n  - `audio`: audio/aac, audio/mp4, audio/mpeg, audio/amr, audio/ogg, audio/opus; hasta 16 MB.\n  - `document`: text/plain, application/pdf, application/vnd.ms-powerpoint, application/msword, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.openxmlformats-officedocument.presentationml.presentation, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet; hasta 100 MB.\n  - `sticker`: image/webp; hasta 100 KB si es estático y 500 KB si es animado.\n- **Historial y reenvío:** el historial del contacto guarda la URL tal cual la enviaste, y el nombre `file_name` no se guarda. Al reenviar un mensaje cuyo archivo es una URL, se reenvía la misma URL; con una extensión de la lista, como esa extensión indica; si no, un mensaje que enviaste se reenvía con su tipo sólo cuando su extensión es un formato que WhatsApp acepta para ese tipo (los de «Formatos y tamaños», por ejemplo `.3gp` para `video` o `.amr` para `audio`). Una URL sin extensión, un formato no aceptado, un documento cuya URL no termina en una extensión de la lista (por ejemplo `/descarga.php?id=7`) o un mensaje recibido sin una extensión de la lista no se puede reenviar: la respuesta es 422 con `error` = `not_forwardable` y `reason` = `unsupported_file_extension`.\n\n## Formularios de WhatsApp\n\nUn formulario de WhatsApp es un conjunto de pantallas con preguntas que tu contacto abre y completa dentro del chat. Con la API puedes armarlo, publicarlo, enviarlo a un contacto y recibir sus respuestas. Sólo funciona con números de WhatsApp Cloud (API oficial) de tu cuenta, y sólo se puede enviar dentro de las 24 horas siguientes al último mensaje del contacto a ese número. Los números conectados por código QR no sirven para formularios.\n- **Identificadores:** un formulario tiene un identificador estable y una versión por cada vez que lo cambias. No los confundas:\n\n  | Qué es | Dónde aparece |\n  |---|---|\n  | **Formulario** (estable, el mismo en todas sus versiones) | `family_id` en las respuestas de formularios, `{familyId}` en las rutas `/forms/families/{familyId}/…`, `form_id` al enviar y `data.form.id` en `whatsapp_form.completed` |\n  | **Versión** (cambia con cada versión nueva) | `id` en las respuestas de formularios, `{id}` en las rutas `/forms/{id}/…` de edición, publicación y números, y `data.form.version_id` en `whatsapp_form.completed` |\n\n  Al enviar pides el formulario, no la versión: sale la versión publicada vigente. Si publicas una versión nueva, lo que se envía cambia sin que cambies tu integración.\n- **Recorrido, paso a paso:**\n  1. **Crea el formulario:** `POST /forms` con `template_key` `blank` (un formulario para armar desde cero; `GET /forms/templates` lista las demás plantillas), `device_id` (el número de WhatsApp Cloud de tu cuenta) y, si quieres, `device_ids` con otros números. Responde `201` con la versión en borrador (`id`), su `family_id`, `lint` y `numbers`. Un formulario no se crea mandando su contenido: el contenido va en el paso siguiente. Ojo con `blank`: no viene vacío, trae una pantalla (`SCREEN_BLNK`) sin elementos cuyo `next` es `complete`, es decir, que termina el formulario. Si en el `PUT` conservas esa pantalla y agregas otra, la segunda queda inalcanzable y `lint` marca `screen_unreachable` (error que impide publicar) hasta que el `next` de la primera apunte a la segunda (o `auto`, que sigue el orden). Lo más simple es reemplazar `screens` entera con tus propias pantallas.\n  2. **Arma la definición:** `PUT /forms/{id}` con `definition` (`screens`, cada una con `id`, `title`, `cta`, `next` e `items`) y, si quieres, `name` y `expected_updated_at`. El servidor guarda la definición tal como llega y la respuesta trae `lint` junto a `data`: una lista de `{code, severity, screenId, itemId, message}` con lo que falta corregir. Lo incompleto se guarda igual; los errores (`severity` `error`) impiden publicar y los avisos (`aviso`) no. Una definición con la forma rota responde `422`. Para leerla de nuevo, `GET /forms/{id}` trae la versión con su definición y `GET /forms/families/{familyId}/definition` la del formulario: la de la versión publicada vigente o, si todavía no hay ninguna, la de la más nueva. Con `expected_updated_at` evitas pisar un cambio más nuevo: si no es el último guardado responde `409` `STALE`.\n  3. **Valida en Meta (opcional):** `POST /forms/{id}/sync` corre el linter, sube el formulario a Meta y trae sus observaciones en `lint` con `code` `meta`, sin publicar. Si la definición tiene errores responde `422` `LINT`, igual que al publicar.\n  4. **Publica:** `POST /forms/{id}/publish` hace la validación y publica; es inmediato. Si la definición tiene errores responde `422` `LINT` con la lista, y si Meta la rechaza, `502` `META`. Un formulario sin ninguna pregunta no se puede publicar. Los otros números del formulario se publican en segundo plano: consulta su estado (`draft`, `publishing`, `published` o `failed`) en `GET /forms/{id}/numbers`.\n  5. **Suma o quita números (opcional):** `POST /forms/{id}/numbers` con `device_id` publica el formulario en otro número de WhatsApp Cloud de tu cuenta, `POST /forms/{id}/numbers/retry` reintenta los que fallaron y `DELETE /forms/{id}/numbers/{deviceId}` quita uno. El número con el que creaste el formulario no se quita: `422` `PRIMARY_NUMBER`. Un número que no es de tu cuenta o no es WhatsApp Cloud responde `422` `NUMBER_NOT_ELIGIBLE`.\n  6. **Envía:** `POST /contacts/{contact}/message/form` con `form_id` (el formulario, no la versión) y `device_id`; opcionalmente `text` (hasta 1024 caracteres), `button_text` (hasta 30, sin emojis) y `client_reference`. Sólo se envía con una clave de API. La clave de un asiento sólo envía a los contactos asignados a ese asiento (con cualquier otro contacto responde el `404` de un contacto que no existe); la clave del titular envía a cualquier contacto de la cuenta. Responde `200` con `message` en cola (`queued`); sus errores propios están al final de esta guía.\n  7. **Recibe las respuestas:** suscríbete al evento `whatsapp_form.completed` (también llega si te suscribes a `*`) o lee `GET /forms/families/{familyId}/responses` y `GET /forms/families/{familyId}/responses/{runId}`. `GET /forms/families/{familyId}/metrics` trae los envíos, aperturas y respuestas.\n  - **Cambiar un formulario publicado:** un formulario publicado no se edita (`PUT` y `DELETE` responden `409` `PUBLISHED`). Crea una versión nueva con `POST /forms/{id}/versions`, que nace como borrador (`201`, o `200` con `created: false` si ya había un borrador), edítala y publícala. El `form_id` no cambia: el envío usa la versión publicada vigente.\n- **Reglas de la definición:** el servidor no genera ni corrige identificadores: quien arma la definición los genera, y deben ser válidos y únicos. Meta acepta sólo letras y guiones bajos.\n  - **Pantallas:** `id` con `SCREEN_` más letras (por ejemplo `SCREEN_ABCD`), sin dígitos, y únicos en el formulario. `next` es `auto` (la siguiente), `complete` (termina) o el `id` de una pantalla posterior: sólo se salta hacia adelante.\n  - **Preguntas:** `name` en minúsculas y guiones bajos (`a-z` y `_`), único en todo el formulario. Sin dígitos, tildes, `ñ` ni espacios. No uses los nombres reservados `flow_token`, `version` ni `extension_message_response`. No cambies un `name` después de crearlo: las respuestas, las ramas y las etiquetas lo usan.\n  - **Opciones:** `id` de cada opción en minúsculas y guiones bajos, único en su pregunta (por ejemplo `op_comercio`).\n  - **Qué marca el linter:** `field_name_reserved` (nombre reservado), `field_name_invalid` (nombre fuera de letras y guiones bajos) y `option_id_invalid` (id de opción fuera de ese alfabeto) son errores que impiden validar y publicar. El linter no revisa que los identificadores sean únicos ni que existan los campos personalizados y las etiquetas que referencias: compruébalo tú.\n  - **Campos personalizados y etiquetas:** si una pregunta guarda su respuesta en un campo personalizado (`map` con `target` `custom` y `key` con el nombre del campo) o asigna etiquetas (`tagMap`, con el nombre de la etiqueta), ese campo y esa etiqueta tienen que existir en tu cuenta. El formulario no los crea: si no existen, la respuesta se guarda en el formulario pero no se escribe en el contacto ni se le asigna la etiqueta. Un campo numérico sólo admite preguntas `number` y uno de fecha sólo preguntas `date`; el servidor no lo revisa.\n  - **No se pueden publicar** los formularios que necesitan un servidor propio (`endpoint` en `true`, opciones en vivo): el linter los marca como error.\n- **Límites del formulario:** hasta 5 pantallas, 50 elementos por pantalla, 3 imágenes por pantalla y 2 enlaces por pantalla; pasarse es un error que impide publicar. El nombre del formulario admite hasta 120 caracteres. Las imágenes (`type` `image`, con `src` en base64 sin el prefijo `data:`) deben ser JPG o PNG, de hasta 300 KB cada una y 4 MB entre todas las del formulario; el servidor no revisa su tamaño ni su formato, así que quien arma la definición debe respetarlos. El guardado acepta un `src` con el prefijo `data:`, pero se envía a Meta tal como lo guardaste y Meta dibuja un recuadro roto: manda siempre la carga en base64 pelada, sin prefijo.\n  - **Tope de cada guardado:** además, un guardado rechaza (`422`) una definición con más de 20 pantallas, 100 elementos en una pantalla o 200 opciones en una pregunta. Esos topes quedan por encima de los límites de arriba. Además, cada guardado envía la definición entera, imágenes incluidas, y debe pesar menos de unos 6 MB: pasado ese tamaño la plataforma rechaza la llamada antes de que llegue al formulario, así que no cuentes con más.\n- **Qué puede hacer cada clave:** los permisos se heredan hacia abajo: `conversations:send` incluye `conversations:write`, que incluye `conversations:read`. Con `read` lees formularios, su definición (`GET /forms/families/{familyId}/definition`), versiones, números, respuestas y métricas; con `write` además armas, guardas, publicas, validas, creas versiones, eliminas borradores y sumas, reintentas o quitas números; con `send` además envías. Por eso una clave con `conversations:send`, aunque sólo la hayas pensado para enviar, también puede armar, publicar y quitar números de formularios: dale `send` sólo a quien confías para eso. Las operaciones `summary`, `analysis` y `deliveries` del panel no están abiertas a claves.\n  - **Topes de llamadas por clave:** cada clave puede hacer hasta 120 lecturas por minuto y hasta 60 escrituras por minuto en formularios (cada clave lleva su propio contador: dos claves de la misma cuenta, sea del titular o de un asiento, no se frenan entre sí, y ninguna gasta el tope del panel). Al superarlos la respuesta es `429` con el encabezado `Retry-After`, que trae los segundos que debes esperar. Validar, publicar y tocar números tienen además el tope de 30 por minuto compartido por toda la cuenta, que se explica en los errores al final de esta guía.\n  - **Cuenta:** la clave de un asiento (un agente) opera sobre los formularios del titular de la cuenta, no sobre formularios propios del asiento.\n  - **Respuestas con clave de un asiento:** `responses` y `responses/{runId}` se limitan a los contactos que ese asiento puede ver en Funnelchat; las métricas (`metrics`) cuentan todas las respuestas del formulario.\n  - **Detalle de una respuesta con clave:** `GET /forms/families/{familyId}/responses/{runId}` no trae `last_error` en sus efectos; el resto de cada efecto es igual al del panel.\n- **Aviso `whatsapp_form.completed`:** `data.form` trae `id` (el formulario), `name` y `version_id` (la versión que recibió el contacto); `message_id` es el mensaje del formulario y puede ser `null`; `answers` es una lista de `{key, label, value}`, una por pregunta y en el orden del formulario (`key` es el `name` de la pregunta, y `value` es `null` si no la contestó). El mismo aviso puede llegar más de una vez con el mismo `id`: deduplica por `id`. Cada respuesta de texto llega con a lo sumo 600 caracteres. Si el contacto vuelve a enviar el mismo formulario no se genera otro aviso: queda la primera respuesta. Los detalles y el ejemplo están en la sección «Webhooks».\n- **Errores al armar y publicar:** `404` si el `id` no existe o no es de tu cuenta (un id de otra cuenta responde igual); `409` `STALE` (el guardado no es el último) y `409` `PUBLISHED` (la versión ya está publicada); `422` `LINT` al publicar con errores en la definición, `422` `NUMBER_NOT_ELIGIBLE` y `422` `PRIMARY_NUMBER` en los números; `502` `META` si Meta rechaza la operación o no responde. Validar, publicar y tocar números (`sync`, `publish` y las operaciones de `numbers`) sube a Meta, que limita por cuenta de WhatsApp Business: todas las claves de tu cuenta, la del titular y las de los asientos, comparten un tope de 30 de estas llamadas por minuto. Al superarlo responde `429` con el encabezado `Retry-After`, que trae los segundos que debes esperar.\n- **Errores al enviar un formulario:** además de los errores de las reglas de protección de esta documentación, `POST /contacts/{contact}/message/form` responde: `404` si el contacto no existe, no es de tu cuenta o, con la clave de un asiento, no está asignado a ese asiento; `404` con `form_not_found` si el formulario no existe en tu cuenta; `422` con `device_unavailable` (el número no es de tu cuenta o no está conectado), `number_not_eligible` (no es un número de WhatsApp Cloud que pueda enviar formularios), `form_not_published` (el formulario no tiene versión publicada: publícalo), `form_not_on_number` (no está publicado en ese número: súmalo con `POST /forms/{id}/numbers` y espera a que se publique), `recipient_unreachable` (el contacto no tiene teléfono ni identificador de WhatsApp al que ese número pueda escribir) y `window_closed` (pasaron más de 24 horas desde el último mensaje del contacto); `429` con `rate_limited` y `Retry-After` si superas el ritmo; y `422` con `not_sent` (el destinatario que se registró no coincide con el contacto verificado, o el mensaje no quedó como un envío de la conversación: no se registró ni salió nada y puedes reintentar; son las únicas dos situaciones y en el uso normal no ocurren). Si Funnelchat no pudo poner el mensaje en cola, la respuesta no es un error: es `200` con el mensaje en estado `failed` y `error.reason` `not_queued`; no salió, así que reintentar no lo duplica. Si tu cuenta está bloqueada por el límite de contactos activos, el envío responde `429` con `mac_lockout_24h`, como los demás envíos con clave, pero a diferencia de ellos no guarda ningún mensaje ni `client_reference`: no queda nada en el historial del contacto ni se puede buscar por la referencia, así que reintenta cuando termine el bloqueo. Un `500` indica que el mensaje no pudo registrarse. Si la llamada no responde o da un error 5xx, busca tu `client_reference` en el historial del contacto antes de reintentar.\n\n## Contactos inactivos por MAC\n\nTu plan incluye un límite de contactos activos (MAC) por ciclo. Cuando lo alcanzas, empiezan 24 horas de gracia en las que todo sigue funcionando. Si terminan sin que sumes capacidad, tu cuenta entra en bloqueo: `GET /mac/status` responde `state` = `enforcing`.\n- **Contacto inactivo:** mientras tu cuenta está en bloqueo, es inactivo todo contacto que todavía no es un contacto activo del ciclo actual. A los contactos que ya son activos en el ciclo puedes seguir escribiéndoles con normalidad.\n- **Cuándo deja de serlo:** cuando tu cuenta sale del bloqueo, al sumar capacidad (por ejemplo, con una compra o un cambio de plan) o al empezar un ciclo nuevo con cupo disponible. No hace falta ninguna acción sobre el contacto.\n- **`GET /mac/status`:** `state` puede ser `normal`, `grace` (corren las 24 horas de gracia), `enforcing` (bloqueo) o `cleared` (el último período de gracia o de bloqueo ya terminó). `inactive_contacts_count` (entero) cuenta los contactos que quedaron registrados como inactivos en el ciclo actual: aquellos a los que se les frenó un mensaje, que te escribieron estando inactivos o que una difusión dejó afuera. No es el total de contactos que podrían ser inactivos: para saber si lo es un contacto puntual, usa `is_mac_inactive`. Si tu cuenta no tiene activo el control de contactos activos, la respuesta trae `enforcement_enabled` en `false` y no incluye este campo.\n- **`is_mac_inactive`** (booleano): viene en cada contacto de `GET /contacts` y de `GET /contacts/{contact}`, y en cada conversación de `GET /conversations`. Es `true` si el contacto es inactivo en el momento de la consulta.\n- **Envíos a un contacto inactivo:** `POST /contacts/{contact}/message`, `POST /message/send-message` y `POST /contacts/{contact}/message/contact-card` responden `429` con `error` = `mac_lockout_24h`, igual que desde el panel, y guardan en el historial del contacto un mensaje con estado de error que lleva tu `client_reference`. Esta respuesta no trae `Retry-After`: el envío vuelve a funcionar cuando el contacto deja de ser inactivo. `POST /contacts/{contact}/message/form` responde el mismo `429` pero no guarda ningún mensaje. En un reenvío (`POST /contacts/{contact}/messages/{message}/forward`) la respuesta es por destino: cada destino inactivo viene con `status` `blocked`, `reason` `mac_lockout_24h` y `message` `null`, y los demás destinos siguen su curso.\n- **`POST /kanban-settings/{kanbanSetting}/bulk-move`:** mueve sólo los contactos que no son inactivos; los inactivos se quedan donde estaban, sin error. Si dejó afuera a alguno, la respuesta es `200` con `{ \"excluded_inactive\": <cantidad> }`; si no, el cuerpo es `null`."}, {"domain": "communities", "title": "Funnelchat Communities", "version": "1.0.0", "sha": "6f32f0b4", "branch": "snapshot/referencia-2210", "tree": "cambios sin commitear", "snapshot_at": "2026-10-22T09:46:45-03:00", "spec": "specs/communities.json", "operations": 44, "excluded_routes": 6, "description": "API de Funnelchat para gestionar desde tu propio sistema las campañas de grupos de WhatsApp de tu cuenta: sus grupos, los participantes y los envíos programados de mensajes a grupos.\n\n## Autenticación\n\nCada llamada lleva tu API key en el encabezado `Authorization: Bearer <id>|fc_live_...`. En «Try It» pegá tu key: esta documentación no usa las cookies de tu sesión.\n\n## Permisos\n\nCada operación indica el permiso que necesita. Una key solo puede llamar a las operaciones de los permisos que tiene.\n\n- `communities:read` (Leer): llama a las operaciones de lectura, que consultan datos de tu cuenta sin cambiar nada.\n- `communities:write` (Escribir): incluye leer; en v0, `communities:write` solo administra suscripciones de webhooks (crear, editar, borrar, rotar el secreto y probar).\n- `communities:send` (Enviar): incluye escribir y leer, pero en v0 no hay ninguna operación de envío disponible.\n- `communities:operate` (Operar): en v0 no habilita ninguna operación.\n\n## Errores de la API key\n\n- `401` con `error: unauthenticated`: falta la key, o es inválida, venció o fue revocada. La respuesta trae `WWW-Authenticate: Bearer`.\n- `403` con `error: permission_denied`: la key no tiene el permiso que necesita la operación.\n- `403` con `error: route_not_available`: la operación no está disponible para API keys.\n- `403` con `error: use_public_endpoint`: la key llegó por una dirección que no es la pública. El mensaje dice cuál usar.\n- `429` con `error: daily_cap_exceeded`: la key alcanzó su límite diario de llamadas. La respuesta trae `Retry-After` con los segundos hasta que se renueve.\n- `503` con `error: gate_unavailable`: no se pudo verificar la key en este momento. La respuesta trae `Retry-After`; reintentá pasado ese tiempo.\n\n## Listados con API key\n\nCon una API key, los listados que llevan la línea «Con API key» devuelven la lista acotada. Las paginadas traen 25 elementos por defecto y 100 como máximo; las de página fija traen el tamaño fijo que dice su línea. Las demás listas no están acotadas en la v0, y esa línea no aparece en ellas. En las paginadas, pedir más, o un tamaño menor que 1 o no entero, responde `422`. El orden solo acepta las columnas que documenta cada operación, y la dirección `asc` o `desc`; otro valor responde `422`. Cada operación dice cómo se llaman sus parámetros de tamaño y de orden. Las estadísticas por período no son listas: con una API key exigen `start_date` y `end_date` (formato `AAAA-MM-DD`) y aceptan un rango de como máximo 366 días entre las dos (ambos incluidos); su línea «Con API key» lo dice. La retención por cohorte y los abandonos no están disponibles con una API key en la v0.\n\n## Versión v0\n\nLa v0 puede cambiar sin aviso, y algunas respuestas incluyen identificadores internos (por ejemplo `userId`).\n\n## Rutas que no aparecen\n\nSolo se listan las operaciones disponibles para API keys en esta versión. Si una ruta no está en esta documentación, no está disponible para API keys: llamarla con una key responde 403.\n\n## Especificación descargable\n\nLa especificación OpenAPI completa se descarga en `/docs/api.json`, para importarla en tu propia herramienta.\n\n## Webhooks\n\n### Cómo suscribirse\n\nCreá una suscripción con tu API key. Crear, editar, borrar, rotar el secreto y probar necesitan el permiso `communities:write` (o `communities:send`, que la incluye); listar y leer (suscripciones, entregas y eventos) necesitan `communities:read`, `communities:write` o `communities:send`. Recordá: en v0, `communities:write` solo administra suscripciones de webhooks. Una suscripción creada con una API key (o reapuntada a otra URL o reactivada con ella) queda ligada a esa key. Si revocás la key, la suscripción deja de recibir eventos: con el próximo evento de tu cuenta de un tipo al que esté suscrita pasa a `active: false` con `inactive_reason: \"key_revoked\"`, visible en `GET /api/v1/webhooks/{id}`. Las suscripciones creadas con la sesión no cambian. Una suscripción `key_revoked` no se puede reactivar: editarla con `active: true` responde `422`. Creá una suscripción nueva y revisá que la URL sea la tuya: si la key se filtró, pudo haberla cambiado. Suscripciones inactivas: `active: false` + `inactive_reason` (`key_revoked`), y reactivar una `key_revoked` responde 422 en todos los dominios. Lo mismo vale para una suscripción que ya estaba inactiva cuando se revocó la key: al intentar reactivarla pasa a `key_revoked`. Cada cuenta puede tener hasta **10 suscripciones activas**; la 11ª responde `409`. Una suscripción recibe los hechos que ocurren desde que está activa y suscrita a su tipo (o a `*`). No se pueden pedir hechos anteriores.\n\n- `GET /api/v1/webhooks`: lista tus suscripciones.\n- `POST /api/v1/webhooks`: crea una suscripción.\n- `GET /api/v1/webhooks/events`: lista todos los tipos de evento del dominio, con su estado (`live` o `planned`), si se pueden suscribir y un ejemplo (`null` en los `planned`).\n- `GET`, `PUT`, `DELETE /api/v1/webhooks/{webhook}`: lee, edita o elimina una suscripción.\n- `POST /api/v1/webhooks/{webhook}/rotate-secret`: genera un secreto nuevo (el anterior deja de firmar de inmediato).\n- `POST /api/v1/webhooks/{webhook}/test`: envía un evento de prueba.\n- `GET /api/v1/webhooks/{webhook}/deliveries`: historial de intentos de entrega.\n\n`events` acepta los tipos de la tabla de abajo o `\"*\"` para todos. La URL debe ser `https` y pública.\n\nPedido:\n\n```json\n{\n    \"url\": \"https://example.com/hooks/funnelchat\",\n    \"description\": \"Mi CRM\",\n    \"events\": [\n        \"group.created\",\n        \"group_participant.joined\"\n    ]\n}\n```\n\nRespuesta `201`. El campo `secret` se muestra **una sola vez** (también al rotarlo): guardalo.\n\n```json\n{\n    \"data\": {\n        \"uuid\": \"00000000-0000-4000-8000-0000000000a1\",\n        \"url\": \"https://example.com/hooks/funnelchat\",\n        \"description\": \"Mi CRM\",\n        \"events\": [\n            \"group.created\",\n            \"group_participant.joined\"\n        ],\n        \"active\": true,\n        \"inactive_reason\": null,\n        \"created_at\": \"2026-10-01T12:00:00Z\",\n        \"updated_at\": \"2026-10-01T12:00:00Z\",\n        \"secret\": \"whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\"\n    }\n}\n```\n\n### Qué vas a recibir\n\nCada entrega es un `POST` con cuerpo JSON `{id, type, version, occurred_at, data}` y estos headers:\n\n| Header | Contenido |\n|---|---|\n| `Content-Type` | `application/json` |\n| `X-Funnelchat-Event` | El tipo del evento (igual al `type` del cuerpo). |\n| `X-Funnelchat-Delivery` | UUID del intento de entrega; cambia en cada reintento. |\n| `X-Funnelchat-Signature` | `t=<unix>,v1=<hex>`, la firma del cuerpo. |\n\n`id` identifica el evento y es estable entre reintentos; `occurred_at` es UTC; `version` es la versión del formato de `data`.\n\n### Verificar la firma\n\n`X-Funnelchat-Signature` es `t=<unix>,v1=<hex>`, donde `v1` es el HMAC-SHA256 en hexadecimal de `t + \".\" + cuerpo`. La clave es el secreto **completo** tal como se muestra, incluido el prefijo `whsec_` (no se decodifica nada). Comparalo en tiempo constante y rechazá entregas con un `t` de más de 5 minutos de diferencia.\n\n**Importante:** solo la firma está firmada. `X-Funnelchat-Event` y `X-Funnelchat-Delivery` no, así que tomá `type` e `id` del cuerpo ya verificado.\n\nPHP:\n\n```php\n<?php\n$secret = 'whsec_...'; // el secreto COMPLETO, con el prefijo whsec_\n$body = file_get_contents('php://input');\n$header = $_SERVER['HTTP_X_FUNNELCHAT_SIGNATURE'] ?? '';\n\nif (! preg_match('/^t=(\\d+),v1=([0-9a-f]{64})$/', $header, $m)) {\n    http_response_code(400);\n    exit;\n}\n[, $t, $signature] = $m;\n\n// Rechazá entregas viejas (protección contra repetición).\nif (abs(time() - (int) $t) > 300) {\n    http_response_code(400);\n    exit;\n}\n\n$expected = hash_hmac('sha256', $t.'.'.$body, $secret);\nif (! hash_equals($expected, $signature)) {\n    http_response_code(400);\n    exit;\n}\n\n$event = json_decode($body, true); // tomá type e id de ACÁ, no de los headers\n```\n\nJavaScript (Node.js):\n\n```javascript\nimport crypto from 'node:crypto';\nimport express from 'express';\n\nconst secret = 'whsec_...'; // el secreto COMPLETO, con el prefijo whsec_\nconst app = express();\n\n// Necesitás el cuerpo crudo: firmá sobre los bytes recibidos, no sobre JSON re-serializado.\napp.post('/hooks/funnelchat', express.raw({ type: 'application/json' }), (req, res) => {\n  const match = /^t=(\\d+),v1=([0-9a-f]{64})$/.exec(req.get('X-Funnelchat-Signature') ?? '');\n  if (!match) return res.sendStatus(400);\n  const [, t, signature] = match;\n\n  // Rechazá entregas viejas (protección contra repetición).\n  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(400);\n\n  const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(req.body).digest('hex');\n  const a = Buffer.from(expected);\n  const b = Buffer.from(signature);\n  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(400);\n\n  const event = JSON.parse(req.body.toString('utf8')); // tomá type e id de ACÁ, no de los headers\n  res.sendStatus(200);\n});\n```\n\n### Reintentos e idempotencia\n\n- Una entrega se considera exitosa si respondés **2xx dentro de 10 segundos**.\n- Hay hasta **4 intentos**, con esperas de **60 s, 300 s y 1800 s** entre ellos.\n- Se reintenta solo ante un error de red, un `5xx` o un `429`. Un `3xx` u otro `4xx` es definitivo y no se reintenta.\n- Las redirecciones nunca se siguen: configurá la URL final.\n- Es una entrega *al menos una vez*: podés recibir el mismo evento más de una vez. **Deduplicá por el `id` del cuerpo**; `X-Funnelchat-Delivery` cambia en cada intento.\n- El historial de intentos está en `GET /api/v1/webhooks/{webhook}/deliveries`.\n- La entrega puede estar desactivada en un entorno; en ese caso no se envía nada y la ruta de prueba responde `503`.\n\nEl campo `error` de cada intento fallido usa un vocabulario cerrado:\n\n| `error` | Significado |\n|---|---|\n| `HTTP <código>` | Tu servidor respondió con un estado que no es 2xx. |\n| `network error: timeout` | Tu servidor no respondió a tiempo. |\n| `network error: tls` | Falló la negociación TLS. |\n| `network error: connect_failed` | No se pudo conectar con tu servidor. |\n| `network error: other` | Otro error de red. |\n| `unsafe destination` | La URL resuelve a una dirección no pública; no se envió nada. |\n| `interrupted` | El intento se cortó antes de terminar. |\n| `deadline` | Se agotó el tiempo disponible antes de llegar a tu URL; no se envió nada y se reintenta. |\n| `secret unreadable` | No se pudo leer el secreto de la suscripción; rotalo con `rotate-secret`. |\n\n### Evento de prueba\n\n`POST /api/v1/webhooks/{webhook}/test` envía un único evento `webhook.test` a la URL de la suscripción, firmado igual que uno real y sin reintentos. Evento de prueba que se envía al pedir la prueba de una suscripción; no se puede suscribir a él.\n\nLa prueba espera la respuesta hasta **5 segundos** (en las entregas reales, 10 s) y se limita a **6 por minuto**. Responde `503` si la entrega está desactivada en el entorno y `409` si el secreto no se puede leer (rotalo con `rotate-secret`).\n\n```json\n{\n    \"id\": \"evt_0000000000000000000000000000000e\",\n    \"type\": \"webhook.test\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"subscription\": \"00000000-0000-4000-8000-00000000000c\",\n        \"sent_at\": \"2026-10-01T12:00:00Z\"\n    }\n}\n```\n\n### Tipos de evento\n\n| Tipo | Descripción | Estado | Suscribible |\n|---|---|---|---|\n| `group.created` | Se creó o se importó un grupo dentro de una campaña de la cuenta. | live | sí |\n| `group_participant.joined` | Una persona entró a un grupo de la cuenta; `campaign` es null si el grupo no es de una campaña. | live | sí |\n| `group_participant.left` | Una persona salió de un grupo de la cuenta; `campaign` es null si el grupo no es de una campaña. | live | sí |\n| `scheduled_message.finished` | Un envío programado terminó, con su resultado agregado. | live | sí |\n| `group.blocked` | Un grupo quedó inoperable: sin administrador conectado, con el link de invitación inválido o suspendido por WhatsApp. | live | sí |\n| `group.recovered` | Un grupo que estaba bloqueado volvió a operar. | live | sí |\n| `scheduled_message.group_delivery_failed` | Un envío programado falló en un grupo concreto, con el motivo desagregado. | live | sí |\n| `scheduled_message.response_received` | Alguien respondió a un envío programado votando una encuesta o respondiendo a un evento. | live | sí |\n| `campaign.status_changed` | Una campaña se activó o se pausó. | live | sí |\n| `pipeline.finished` | Terminó un pipeline de operaciones sobre grupos, con su resultado agregado y la clave de idempotencia del pedido que lo originó, cuando la hay. | live | sí |\n| `poll.voted` | Un participante votó en una encuesta enviada a un grupo, o cambió su voto. | live | sí |\n| `group.invite_link_renewed` | El enlace de invitación de un grupo cambió por una renovación, automática al detectarse inválido o pedida por el usuario; en v0 no se publican los cambios que detecta la sincronización ni el enlace inicial al crear el grupo. | live | sí |\n| `group.invite_link_down` | El enlace de invitación de un grupo quedó confirmado caído (WhatsApp respondió que no existe y la renovación no consiguió otro); no se publica por errores transitorios ni por grupos prohibidos o suspendidos, y se publica una sola vez por episodio de bloqueo. | live | sí |\n| `webhook.test` | Evento de prueba que se envía al pedir la prueba de una suscripción; no se puede suscribir a él. | live | no |\n\nLos tipos con estado `planned` todavía no se emiten y no se pueden suscribir: pedirlos en `events` responde `422`. `webhook.test` no se suscribe: llega solo al pedir la prueba de una suscripción.\n\n### Ejemplos\n\n#### `group.created`\n\n```json\n{\n    \"id\": \"evt_00000000000000000000000000000001\",\n    \"type\": \"group.created\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"group\": {\n            \"uid\": \"120363000000000001@g.us\",\n            \"name\": \"Grupo de ejemplo\",\n            \"type\": \"group\"\n        },\n        \"campaign\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000001\",\n            \"name\": \"Campaña de ejemplo\"\n        },\n        \"source\": \"created\"\n    }\n}\n```\n\n#### `group_participant.joined`\n\n```json\n{\n    \"id\": \"evt_00000000000000000000000000000002\",\n    \"type\": \"group_participant.joined\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"group\": {\n            \"uid\": \"120363000000000001@g.us\",\n            \"name\": \"Grupo de ejemplo\"\n        },\n        \"campaign\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000001\"\n        },\n        \"participant\": {\n            \"identity\": \"+5491100000000\",\n            \"identity_type\": \"phone\",\n            \"name\": \"Ana Pérez\"\n        }\n    }\n}\n```\n\n#### `group_participant.left`\n\n```json\n{\n    \"id\": \"evt_00000000000000000000000000000003\",\n    \"type\": \"group_participant.left\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"group\": {\n            \"uid\": \"120363000000000001@g.us\",\n            \"name\": \"Grupo de ejemplo\"\n        },\n        \"campaign\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000001\"\n        },\n        \"participant\": {\n            \"identity\": \"+5491100000000\",\n            \"identity_type\": \"phone\",\n            \"name\": \"Ana Pérez\"\n        }\n    }\n}\n```\n\n#### `scheduled_message.finished`\n\n```json\n{\n    \"id\": \"evt_00000000000000000000000000000004\",\n    \"type\": \"scheduled_message.finished\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"scheduled_message\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000004\",\n            \"name\": \"Envío de ejemplo\"\n        },\n        \"outcome\": \"partially_failed\",\n        \"groups\": {\n            \"total\": 10,\n            \"failed\": 1,\n            \"partially_failed\": 2\n        },\n        \"occurrence\": 1\n    }\n}\n```\n\n#### `group.blocked`\n\n```json\n{\n    \"id\": \"evt_00000000000000000000000000000005\",\n    \"type\": \"group.blocked\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"group\": {\n            \"uid\": \"120363000000000001@g.us\",\n            \"name\": \"Grupo de ejemplo\"\n        },\n        \"campaign\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000001\"\n        },\n        \"reason\": \"admin_access_lost\"\n    }\n}\n```\n\n#### `group.recovered`\n\n```json\n{\n    \"id\": \"evt_00000000000000000000000000000006\",\n    \"type\": \"group.recovered\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"group\": {\n            \"uid\": \"120363000000000001@g.us\",\n            \"name\": \"Grupo de ejemplo\"\n        },\n        \"campaign\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000001\"\n        },\n        \"reason\": \"access_restored\"\n    }\n}\n```\n\n#### `scheduled_message.group_delivery_failed`\n\n```json\n{\n    \"id\": \"evt_00000000000000000000000000000007\",\n    \"type\": \"scheduled_message.group_delivery_failed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"scheduled_message\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000007\"\n        },\n        \"group\": {\n            \"uid\": \"120363000000000001@g.us\",\n            \"name\": \"Grupo de ejemplo\"\n        },\n        \"reason\": \"device_disconnected\",\n        \"partial\": false,\n        \"occurrence\": 1\n    }\n}\n```\n\n#### `scheduled_message.response_received`\n\n```json\n{\n    \"id\": \"evt_00000000000000000000000000000008\",\n    \"type\": \"scheduled_message.response_received\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"scheduled_message\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000008\"\n        },\n        \"group\": {\n            \"uid\": \"120363000000000001@g.us\"\n        },\n        \"participant\": {\n            \"identity\": \"+5491100000000\",\n            \"identity_type\": \"phone\",\n            \"name\": \"Ana Pérez\"\n        },\n        \"response\": {\n            \"type\": \"poll\",\n            \"options\": [\n                \"Opción A\"\n            ],\n            \"rsvp\": null\n        }\n    }\n}\n```\n\n#### `campaign.status_changed`\n\n```json\n{\n    \"id\": \"evt_00000000000000000000000000000009\",\n    \"type\": \"campaign.status_changed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"campaign\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000009\",\n            \"name\": \"Campaña de ejemplo\"\n        },\n        \"from\": \"active\",\n        \"to\": \"paused\"\n    }\n}\n```\n\n#### `pipeline.finished`\n\n```json\n{\n    \"id\": \"evt_0000000000000000000000000000000a\",\n    \"type\": \"pipeline.finished\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"campaign\": {\n            \"uuid\": \"00000000-0000-4000-8000-00000000000a\"\n        },\n        \"pipeline\": {\n            \"uuid\": \"00000000-0000-4000-8000-00000000000b\",\n            \"name\": \"Pipeline de ejemplo\"\n        },\n        \"outcome\": \"completed\",\n        \"actions\": {\n            \"total\": 5,\n            \"succeeded\": 4,\n            \"failed\": 0,\n            \"skipped\": 1,\n            \"cancelled\": 0\n        },\n        \"idempotency_key\": \"00000000-0000-4000-8000-00000000000d\"\n    }\n}\n```\n\n#### `poll.voted`\n\n```json\n{\n    \"id\": \"evt_0000000000000000000000000000000b\",\n    \"type\": \"poll.voted\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"poll\": {\n            \"uid\": \"3EB0000000000000000001\"\n        },\n        \"group\": {\n            \"uid\": \"120363000000000001@g.us\"\n        },\n        \"scheduled_message\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000004\"\n        },\n        \"participant\": {\n            \"identity\": \"+5491100000000\",\n            \"identity_type\": \"phone\",\n            \"name\": null\n        },\n        \"options\": [\n            \"Opción A\"\n        ]\n    }\n}\n```\n\n#### `group.invite_link_renewed`\n\n```json\n{\n    \"id\": \"evt_0000000000000000000000000000000c\",\n    \"type\": \"group.invite_link_renewed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"group\": {\n            \"uid\": \"120363000000000001@g.us\",\n            \"name\": \"Grupo de ejemplo\",\n            \"type\": \"group\"\n        },\n        \"campaign\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000001\",\n            \"name\": \"Campaña de ejemplo\"\n        },\n        \"invite_link\": \"https://chat.whatsapp.com/EjemploNuevo0000000000001\",\n        \"previous_invite_link\": \"https://chat.whatsapp.com/EjemploAnterior000000000001\",\n        \"trigger\": \"automatic\",\n        \"recovered\": false\n    }\n}\n```\n\n#### `group.invite_link_down`\n\n```json\n{\n    \"id\": \"evt_0000000000000000000000000000000d\",\n    \"type\": \"group.invite_link_down\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"group\": {\n            \"uid\": \"120363000000000001@g.us\",\n            \"name\": \"Grupo de ejemplo\",\n            \"type\": \"group\"\n        },\n        \"campaign\": {\n            \"uuid\": \"00000000-0000-4000-8000-000000000001\",\n            \"name\": \"Campaña de ejemplo\"\n        },\n        \"reason\": \"http_404\",\n        \"renewal_attempted\": true\n    }\n}\n```\n\n### No confundir con los webhooks de campaña\n\nLas rutas `campaigns/{campaign}/webhooks` son un producto distinto (los webhooks legados de campaña), con otro formato, y **no reciben** estos eventos. Los eventos de esta sección se reciben solo a través de `/api/v1/webhooks`."}, {"domain": "accounts", "title": "Funnelchat Accounts — API", "version": "0.1.0", "sha": "300cc8435", "branch": "snapshot/referencia-2210", "tree": "limpio", "snapshot_at": "2026-10-22T09:46:54-03:00", "spec": "specs/accounts.json", "operations": 68, "excluded_routes": 0, "description": "La API pública de **Funnelchat Accounts**: los números de WhatsApp de la cuenta\n(devices), sus alertas operativas, los dominios propios y los datos de la cuenta.\n\n### Cómo autenticarse\n\nCada llamada viaja con la clave de API en el header `Authorization`:\n\n```\nAuthorization: Bearer <id>|fc_live_...\n```\n\nLas claves se emiten y se revocan desde la pantalla **Desarrolladores** del panel.\nEl valor completo se muestra **una sola vez**, al crearla.\n\n### Permisos\n\nCada ruta de esta documentación declara el permiso que exige. La clave sólo puede\nllamar las rutas para las que se le otorgó el permiso al crearla:\n\n| Permiso | Qué habilita |\n|---|---|\n| `accounts:read` | Leer. Todas las rutas `GET` salvo las de las suscripciones unificadas. |\n| `accounts:write` | Modificar de forma reversible. |\n| `accounts:operate` | Las cinco operaciones **irreversibles** (reset, limpieza, purga de cola y los dos borrados). |\n| `webhooks:read` | Leer las suscripciones de webhook unificadas y sus entregas. |\n| `webhooks:write` | Crear, modificar, probar y borrar suscripciones. Implica `webhooks:read`. |\n\n`accounts:operate` es un eje aparte, no un escalón más alto: `accounts:write` **no**\nhabilita las irreversibles, y `accounts:operate` no habilita nada más.\n\n`webhooks:*` y `accounts:*` no se implican entre sí. Además, para crear o modificar una\nsuscripción con una clave, la clave necesita el permiso de lectura del área de cada tipo\nde evento (`accounts:read`, `conversations:read` o `communities:read`; `write` o `send`\nde esa misma área también sirven); si le falta alguno, responde `422`.\n\nLeer, cambiar, borrar o probar una suscripción con una clave sigue la misma regla de\náreas: para verla (`GET api/v1/webhooks/{id}`), ver su historial de entregas\n(`GET api/v1/webhooks/{id}/deliveries`), cambiarla (`PUT`), borrarla (`DELETE`) o\nprobarla (`POST api/v1/webhooks/{id}/test`), la clave necesita el permiso de lectura\ndel área de cada tipo de evento que la suscripción tiene ahora; si le falta alguno\nresponde `403 permission_denied`, el mensaje nombra el permiso que falta y no se\ncambia, borra ni envía nada. El listado (`GET api/v1/webhooks`) muestra solo las\nsuscripciones que la clave puede leer, y el historial muestra solo las entregas de\ntipos cuya área la clave puede leer (si la suscripción estuvo suscripta antes a otra\nárea, esas filas no aparecen); las entregas de prueba (`webhook.test`) se muestran\nsiempre.\n\n### Lo que no está acá no existe para una clave\n\nLa superficie es cerrada. Una ruta que no figure en esta documentación responde\n`403 route_not_available` a cualquier clave, sin importar qué permisos tenga — y eso\nincluye la facturación, la cuota de IA y la propia pantalla de claves.\n\n### Webhooks\n\nLas suscripciones se administran en `api/v1/webhooks`, el sistema unificado: cada\nsuscripción tiene un solo secreto y un solo historial de entregas, y recibe eventos de\nlas tres áreas (`accounts`, `conversations` y `communities`). Rotar el secreto y\n«Activar» una suscripción migrada se hacen desde el panel. `api/v1/webhooks/endpoints`\nes el sistema anterior y sigue disponible con los permisos `accounts:*`.\n\nUna suscripción queda atada a la clave de API que la creó, o a la última clave que\n**cambió su dirección** (`url`), aunque la haya creado el panel u otra clave: así, si\nuna clave robada redirige una suscripción, revocarla corta ese destino. Cuando se\nrevoca la clave a la que está atada, la suscripción deja de recibir eventos de\ninmediato y pasa a `status: disabled` con `disabled_reason: key_revoked`. No se puede\nvolver a habilitar: hay que crear una suscripción nueva. Cambiar la dirección desde el\npanel no la desata de la clave. Una suscripción que no está atada a ninguna clave\n(creada desde el panel y sin que ninguna clave le haya cambiado la dirección) es de la\ncuenta: revocar claves no la afecta.\n\nEl catálogo (`GET api/v1/webhooks/events`) lista todos los eventos de las tres áreas\n(`accounts`, `conversations` y `communities`). En cada entrada, `status` dice si el\nevento está habilitado (`active`) o si se puede suscribir pero todavía no emite\n(`planned`), y `example` trae el cuerpo completo que vas a recibir. Al final de esta\npágina, en «Ejemplos de payload de webhooks», están los ejemplos de los eventos\nactivos del área `accounts` a los que te podés suscribir; los de `conversations` y\n`communities` están en el catálogo.\n\nSobre los dos eventos de los números de la cuenta:\n\n- `device.connected`: un número de la cuenta quedó conectado. Cubre dos caminos: la\n  conexión por código QR y el alta de un número de WhatsApp Cloud. No se emite por un\n  cambio manual hecho por el soporte de Funnelchat, ni cuando se bloquea un número de\n  prueba que ya se usó en otra cuenta, ni dos veces por la misma conexión.\n- `device.disconnected`: un número conectado por código QR perdió la conexión. Los\n  números de WhatsApp Cloud todavía no emiten este evento.\n\n#### Sobre\n\nCada entrega es un `POST` con cuerpo JSON `{id, type, version, occurred_at, data}`:\n`id` identifica el hecho (es el mismo en todos los reintentos y en todos tus endpoints\nsuscriptos al mismo evento, así que sirve para deduplicar), `type` es el nombre del\nevento, `version` es la versión de ese tipo de evento y `occurred_at` es UTC.\n\n#### Cabeceras\n\n- `X-Funnelchat-Signature`: la firma (ver abajo).\n- `X-Funnelchat-Event`: el tipo de evento.\n- `X-Funnelchat-Delivery`: un UUID que cambia en cada intento de entrega.\n\n#### Firma\n\nLa firma es `t=<unix>,v1=<hex>`, donde `v1` es el HMAC-SHA256 de `\"<t>.<cuerpo crudo>\"`\ncon el secreto del endpoint (completo, con el prefijo `whsec_`). Verificá sobre el cuerpo\n**crudo**, antes de parsear el JSON, y rechazá las entregas con más de 300 segundos de\ndiferencia. Más detalle en `/docs/webhooks/#firma`.\n\nPHP:\n\n```php\nfunction verificarFirma(string $cuerpoCrudo, string $cabecera, string $secreto, int $tolerancia = 300): bool\n{\n    parse_str(str_replace(',', '&', $cabecera), $partes);\n\n    if (! isset($partes['t'], $partes['v1']) || ! ctype_digit($partes['t'])) {\n        return false;\n    }\n\n    if (abs(time() - (int) $partes['t']) > $tolerancia) {\n        return false;\n    }\n\n    $esperada = hash_hmac('sha256', $partes['t'].'.'.$cuerpoCrudo, $secreto);\n\n    return hash_equals($esperada, $partes['v1']);\n}\n```\n\nNode:\n\n```js\nconst crypto = require('crypto');\n\nfunction verificarFirma(cuerpoCrudo, cabecera, secreto, tolerancia = 300) {\n  const partes = Object.fromEntries(cabecera.split(',').map((p) => p.split('=')));\n  if (!/^\\d+$/.test(partes.t || '') || !partes.v1) return false;\n\n  if (Math.abs(Date.now() / 1000 - Number(partes.t)) > tolerancia) return false;\n\n  const esperada = crypto\n    .createHmac('sha256', secreto)\n    .update(`${partes.t}.${cuerpoCrudo}`)\n    .digest('hex');\n\n  const a = Buffer.from(esperada);\n  const b = Buffer.from(partes.v1);\n\n  return a.length === b.length && crypto.timingSafeEqual(a, b);\n}\n```\n\n#### Reintentos\n\nUna entrega es exitosa con cualquier `2xx`. Ante un error de red, un `5xx` o un `429` se\nreintenta hasta 4 intentos en total, esperando 60 segundos, 5 minutos y 30 minutos entre\nellos. Cualquier otra respuesta (`3xx`, `4xx` salvo `429`) no se reintenta. Un endpoint que\nacumula 5 entregas fallidas consecutivas (agotadas o con una respuesta que no se\nreintenta) se deshabilita automáticamente.\n\n#### Probar una suscripción\n\n`POST api/v1/webhooks/{id}/test` envía una sola entrega firmada de `webhook.test`, sin\nreintentos, a una suscripción `active` o `paused`, y la registra en su historial. Si la\nsuscripción está `disabled` responde `422` sin enviar ni registrar nada: si se\ndeshabilitó por entregas fallidas, reactivala (`PUT` con `status: active`) y probala\ndespués; si se revocó la clave a la que estaba atada (`key_revoked`), creá una\nsuscripción nueva. Con una clave, la regla de áreas de «Permisos» va primero: si falta\nun permiso responde `403`, aunque la suscripción esté `disabled`.\n\n\n### Listados\n\nCon una clave, todo listado pagina. `perPage` va de 1 a 100 y vale 25 si no se\nenvía; más de 100 (o algo que no sea un entero) responde `422`. Algunas rutas\naceptan además su nombre histórico (`per_page`, `pageSize`, `limit`) con el\nmismo tope. El orden es fijo en cada ruta y está indicado en ella: ningún\nlistado acepta parámetros de ordenamiento (`orderBy`, `sort`, …) y enviar uno\nresponde `422`. La sesión web del panel no cambia.\n\n### Estado: v0\n\nEsta API está en **v0**: puede cambiar sin aviso previo.\n\n- Algunas respuestas de rutas existentes incluyen identificadores internos\n  (`user_id` en los devices, `owner_id` en los datos de la cuenta). No los uses\n  como claves estables.\n- Las suscripciones de webhook atadas a una clave (creadas o redirigidas con ella)\n  mueren con ella (ver «Webhooks»).\n\n<!-- webhook-examples -->\n### Ejemplos de payload de webhooks\n\n`device.disconnected`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"device.disconnected\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-05T03:00:00Z\",\n    \"data\": {\n        \"device\": {\n            \"id\": 4521,\n            \"name\": \"Ventas\",\n            \"number\": \"5491100000000\"\n        },\n        \"disconnected_at\": \"2026-10-05T03:00:00+00:00\"\n    }\n}\n```\n\n`device.connected`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"device.connected\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-05T03:00:00Z\",\n    \"data\": {\n        \"device\": {\n            \"id\": 4521,\n            \"name\": \"Ventas\",\n            \"number\": \"5491100000000\"\n        },\n        \"connected_at\": \"2026-10-05T03:00:00+00:00\"\n    }\n}\n```\n\n`billing.payment_failed`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"billing.payment_failed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"amount_cents\": 2990,\n        \"currency\": \"USD\"\n    }\n}\n```\n\n`billing.upcoming_charge`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"billing.upcoming_charge\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"charge_date\": \"2026-11-05T00:00:00Z\",\n        \"amount_cents\": 2990,\n        \"currency\": \"USD\"\n    }\n}\n```\n\n`subscription.trial_ending`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"subscription.trial_ending\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"trial_ends_at\": \"2026-10-04T00:00:00Z\"\n    }\n}\n```\n\n`subscription.reactivated`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"subscription.reactivated\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"renews_at\": \"2026-11-05T00:00:00Z\"\n    }\n}\n```\n\n`subscription.cancellation_confirmed`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"subscription.cancellation_confirmed\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"access_until\": \"2026-11-05T00:00:00Z\"\n    }\n}\n```\n\n`subscription.dormant_ending`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"subscription.dormant_ending\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"charge_date\": \"2026-10-08T00:00:00Z\",\n        \"amount_cents\": 1990,\n        \"currency\": \"USD\"\n    }\n}\n```\n\n`mac.usage_80`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"mac.usage_80\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"used\": 8000,\n        \"limit\": 10000,\n        \"percentage\": 80,\n        \"cycle_started_at\": \"2026-10-01T00:00:00Z\"\n    }\n}\n```\n\n`mac.usage_100`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"mac.usage_100\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"used\": 10000,\n        \"limit\": 10000,\n        \"percentage\": 100,\n        \"cycle_started_at\": \"2026-10-01T00:00:00Z\",\n        \"grace_expires_at\": \"2026-10-02T12:00:00Z\"\n    }\n}\n```\n\n`mac.expiring`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"mac.expiring\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"contacts_expiring\": 1200,\n        \"expires_at\": \"2026-10-08T03:00:00Z\"\n    }\n}\n```\n\n`account.blocked`:\n\n```json\n{\n    \"id\": \"evt_0f8e4b526c1d4c7a9a3e2b5d\",\n    \"type\": \"account.blocked\",\n    \"version\": 1,\n    \"occurred_at\": \"2026-10-01T12:00:00Z\",\n    \"data\": {\n        \"reason\": \"mac_grace_expired\",\n        \"grace_started_at\": \"2026-10-01T12:00:00Z\",\n        \"grace_expired_at\": \"2026-10-02T12:00:00Z\"\n    }\n}\n```\n"}], "x-fc-excluded-routes": [{"domain": "conversations", "methods": ["GET"], "path": "/api/v1/webhooks/{webhook}/deliveries", "edge_domain": "accounts"}, {"domain": "conversations", "methods": ["GET"], "path": "/api/v1/webhooks/events", "edge_domain": "accounts"}, {"domain": "conversations", "methods": ["GET", "POST"], "path": "/api/v1/webhooks", "edge_domain": "accounts"}, {"domain": "conversations", "methods": ["GET", "PUT", "DELETE"], "path": "/api/v1/webhooks/{webhook}", "edge_domain": "accounts"}, {"domain": "conversations", "methods": ["POST"], "path": "/api/v1/webhooks/{webhook}/rotate-secret", "edge_domain": "accounts"}, {"domain": "conversations", "methods": ["POST"], "path": "/api/v1/webhooks/{webhook}/test", "edge_domain": "accounts"}, {"domain": "conversations", "methods": ["GET"], "path": "/api/v1/forms/templates", "edge_domain": null}, {"domain": "conversations", "methods": ["GET", "POST"], "path": "/api/v1/forms", "edge_domain": null}, {"domain": "conversations", "methods": ["GET"], "path": "/api/v1/forms/families/{familyId}/metrics", "edge_domain": null}, {"domain": "conversations", "methods": ["GET"], "path": "/api/v1/forms/families/{familyId}/responses", "edge_domain": null}, {"domain": "conversations", "methods": ["GET"], "path": "/api/v1/forms/families/{familyId}/responses/{runId}", "edge_domain": null}, {"domain": "conversations", "methods": ["GET"], "path": "/api/v1/forms/families/{familyId}/versions", "edge_domain": null}, {"domain": "conversations", "methods": ["GET"], "path": "/api/v1/forms/families/{familyId}/definition", "edge_domain": null}, {"domain": "conversations", "methods": ["GET", "PUT", "DELETE"], "path": "/api/v1/forms/{id}", "edge_domain": null}, {"domain": "conversations", "methods": ["GET", "POST"], "path": "/api/v1/forms/{id}/numbers", "edge_domain": null}, {"domain": "conversations", "methods": ["POST"], "path": "/api/v1/forms/{id}/versions", "edge_domain": null}, {"domain": "conversations", "methods": ["POST"], "path": "/api/v1/forms/{id}/sync", "edge_domain": null}, {"domain": "conversations", "methods": ["POST"], "path": "/api/v1/forms/{id}/publish", "edge_domain": null}, {"domain": "conversations", "methods": ["DELETE"], "path": "/api/v1/forms/{id}/numbers/{deviceId}", "edge_domain": null}, {"domain": "conversations", "methods": ["POST"], "path": "/api/v1/forms/{id}/numbers/retry", "edge_domain": null}, {"domain": "communities", "methods": ["GET", "POST"], "path": "/api/v1/webhooks", "edge_domain": "accounts"}, {"domain": "communities", "methods": ["GET"], "path": "/api/v1/webhooks/events", "edge_domain": "accounts"}, {"domain": "communities", "methods": ["GET", "PUT", "DELETE"], "path": "/api/v1/webhooks/{webhook}", "edge_domain": "accounts"}, {"domain": "communities", "methods": ["POST"], "path": "/api/v1/webhooks/{webhook}/rotate-secret", "edge_domain": "accounts"}, {"domain": "communities", "methods": ["POST"], "path": "/api/v1/webhooks/{webhook}/test", "edge_domain": "accounts"}, {"domain": "communities", "methods": ["GET"], "path": "/api/v1/webhooks/{webhook}/deliveries", "edge_domain": "accounts"}]}