{"openapi":"3.1.0","info":{"title":"VChat — API Pública","version":"1.0.0","description":"API REST pública e versionada da VChat, **account-scoped** e autenticada por **API Key** por conta.\n\n## Base URL\nTodos os endpoints ficam sob **`https://app.vchat.dev.br/api/public/v1`**. Os caminhos abaixo (ex.: `/campaigns`) são **relativos a essa base** — sempre inclua o segmento `/public/`.\n\n## Autenticação\nEnvie a API Key no header **`Authorization: Bearer vck_live_...`**. Crie/revogue chaves no painel em **Configurações → API Keys** (o segredo é exibido uma única vez).\n\nExemplo mínimo (liste suas campanhas):\n```bash\ncurl https://app.vchat.dev.br/api/public/v1/campaigns \\\n  -H \"Authorization: Bearer vck_live_SEU_TOKEN\"\n```\n\nCada endpoint exige um **escopo** (ex.: `reports:read`, `campaigns:write`) — concedido à chave na criação. Sem o escopo → `403`.\n\n> ❌ **Erros comuns de autenticação/URL:**\n> - Usar `/api/v1/...` (sem `/public/`) → **404**. O correto é `/api/public/v1/...`.\n> - Usar o header `X-API-KEY` → não é reconhecido. Use **`Authorization: Bearer`**.\n> - Usar o JWT de sessão do painel → não vale aqui. A API pública aceita **apenas** a API Key `vck_live_...`.\n\n## Erros\n`400` validação · `401` key ausente/inválida/revogada/expirada · `403` escopo ausente · `404` não encontrado · `409` Idempotency-Key duplicada · `422` validação de negócio (ex.: preflight de campanha) · `429` rate-limit (com `Retry-After`).\n\n## Codificação (UTF-8 obrigatório)\nTodos os corpos devem ser **UTF-8**. Requisições com bytes inválidos (mojibake — texto salvo em ANSI/Windows-1252 em vez de UTF-8) são rejeitadas com **400 `INVALID_ENCODING`**, antes de qualquer persistência. Envie `Content-Type: application/json; charset=utf-8` e, no Windows, salve arquivos de origem (ex.: planilha de campanha) como **UTF-8**, não ANSI/CP1252.\n\n## MCP\nHá também um servidor **MCP** (Streamable HTTP) em `POST /mcp`, autenticado pela mesma API Key, que expõe estas capabilities como **tools** (mesmos escopos/guardrails).\n\n> ⚠️ **Cache de ferramentas do cliente MCP.** Publicamos novas tools e campos com frequência. Clientes MCP carregam a lista de ferramentas **uma única vez, na abertura da sessão**, e a mantêm em cache. Se um campo novo (ex.: `templateParams` em `create_campaign`) não aparecer no schema da ferramenta, **reconecte / reinicie a sessão MCP** para atualizar a lista.\n\n_Cobertura: Fases A–E (relatórios, campanhas, fluxos+debug, IA, gestão de conta, usuários, canais, MCP)._\n\n## Receita: campanha WhatsApp Oficial (template HSM)\nNo canal **OFICIAL**, o corpo do disparo vem de um **template HSM aprovado** (`templateId`). As variáveis do corpo (`{{1}}`, `{{2}}`…) vêm de **`templateParams`** — um array ORDENADO de expressões (índice 0 = `{{1}}`), renderizadas **por destinatário** com os `vars` daquele contato. O `messageTemplate` **não** alimenta o corpo do HSM (é o texto livre de janela de 24h / QR Code). Sem `templateParams`, a Meta rejeita com **`#132000`**.\n\nTemplate `boas_vindas` com 1 variável (`{{1}}` = nome):\n```bash\ncurl -X POST https://app.vchat.dev.br/api/public/v1/campaigns \\\n  -H \"Authorization: Bearer vck_live_SEU_TOKEN\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"name\": \"Boas-vindas\",\n    \"channelId\": 30,\n    \"templateId\": 18,\n    \"replyFlowId\": 306,\n    \"templateParams\": [\"${nome}\"],\n    \"recipients\": [\n      { \"number\": \"5581999990000\", \"name\": \"Weslley\", \"vars\": { \"nome\": \"Weslley\" } }\n    ]\n  }'\n```\nVia MCP: a tool `create_campaign` aceita os mesmos campos (`templateId` + `templateParams` + `recipients[].vars`). Depois `launch_campaign` para disparar.\n\n### Botão de URL dinâmica\nSe o template tem um botão de **URL dinâmica** (a URL aprovada termina em `{{1}}`), passe **`templateButtons`** — um por botão dinâmico: `{ index, subType: \"url\", value }`. O `value` é renderizado **por destinatário** e preenche o `{{1}}` **do botão**. ⚠️ Esse `{{1}}` é de **escopo próprio do botão** — NÃO é o `{{1}}` do corpo (`templateParams`); a Meta os entrega em parâmetros separados. Botões estáticos (resposta rápida / URL fixa / telefone) NÃO precisam disto — já viajam no template aprovado.\n```json\n{\n  \"templateId\": 18,\n  \"templateParams\": [\"${nome}\"],\n  \"templateButtons\": [ { \"index\": 0, \"subType\": \"url\", \"value\": \"${fatura_id}\" } ],\n  \"recipients\": [ { \"number\": \"5581999990000\", \"vars\": { \"nome\": \"Ana\", \"fatura_id\": \"F-42\" } } ]\n}\n```\n\n---\n\n# Iniciando um Fluxo com MCP e Claude Code\nVocê monta um fluxo de atendimento de ponta a ponta **sem abrir o painel**, usando o servidor MCP pelo Claude Code (ou outro cliente MCP).\n\n**1. Conectar** (crie a API Key no painel → Configurações → API Keys, com escopos `flows:read/write`, `ai:read/write`, `account:read/write`, `channels:read/write`, `reports:read`):\n```bash\nclaude mcp add --transport http vchat https://app.vchat.dev.br/mcp \\\n  --header \"Authorization: Bearer vck_live_SEU_TOKEN\"\n```\n\n**2. Instrução para o agente** (cole como 1ª mensagem):\n> Crie um fluxo no VChat usando só as ferramentas MCP `vchat`. Ordem:\n> 1. **Aprenda o contrato**: leia os resources `vchat://docs/schema`, `vchat://docs/nodes` e `vchat://docs/flow-scripting`. Use só os campos de `data` que existem ali (não invente).\n> 2. **Levante os blocos**: `list_departments`, `list_ai_models`/`list_ai_profiles`/`list_ai_rules`, `list_channels`, `list_flows`. Onde um campo tiver `ref`/`refEndpoint` (ex.: `react_agent.toolIds → AITool, GET /ai/tools`), pegue os IDs válidos por esse endpoint.\n> 3. **Crie o que faltar**: `upsert_ai_model`/`upsert_ai_profile`/`upsert_ai_rule`/`upsert_ai_tool`, `create_department`.\n> 4. **Monte** `nodes` (`{id,type,data}`) + `edges`. As ligações vão em campos de `data` (`nextNodeId`, `failNodeId`, `options[].nextNodeId`, `conditions[].trueNodeId/falseNodeId`, `workingNodeId/closedNodeId`…). Prioridade de fila: `set(\"priority\", n)` num nó Código antes do `handover`.\n> 5. **Valide**: `validate_flow` e corrija tudo em `errors` até `ok:true`.\n> 6. **Salve**: `create_flow` (não use `allowInvalid`). Guarde o `id`.\n> 7. **Teste**: `flow_debug_start` → `flow_debug_input`; confira `currentNode`/`output`/`history`/`requests`/`errors`; ajuste com `update_flow` e re-teste.\n> 8. **Publique**: `update_flow {isDefault:true}` **ou** `update_channel {id, defaultFlowId}`.\n>\n> Regras: respeite os escopos (403 = falta escopo); FK só com IDs dos `list_*`; segredos são write-only; `update_flow` versiona sozinho se houver histórico de uso (201).\n\nAs referências de nós e scripting estão logo abaixo.\n\n---\n\n# Autoria de fluxos — tipos de nó e scripting\nAs referências abaixo são **geradas da mesma fonte única** (`backend/src/schemas/nodeManifest.ts` + `scriptingManifest.ts`) que valida o backend e alimenta as tools/resources do MCP — então a doc humana e o LLM veem exatamente o mesmo contrato. Também disponíveis cruas em `GET /api/public/v1/schema` (campos `nodes`/`scripting`).\n\n> **Chaves-estrangeiras (`ref`)**: campos que guardam ID(s) de outra entidade trazem `ref` (entidade alvo) e, quando listável via API pública, `refEndpoint` (onde buscar os IDs válidos). Ex.: no nó `react_agent`, `toolIds` → `ref: AITool` (liste em `GET /ai/tools`). Aparece tanto nos nós (abaixo) quanto nos schemas das entidades (`components.schemas`) e no `/schema`.\n\n# Tipos de Nó do Fluxo (FlowEngine)\n\nReferência dos 16 tipos de nó: campos de `data` e saídas (links). Use ao montar `nodes`/`edges` de um fluxo via API/MCP.\n\n## start\n**Início** — Ponto de entrada do fluxo. Envia uma 1ª mensagem opcional e segue para o próximo nó.\n\n**Campos (`data`)**:\n- `text` (string) — Mensagem inicial opcional enviada ao entrar no fluxo.\n- `media` (array) — Mídias opcionais na 1ª mensagem: [{ mediaAssetId, mimetype, name, sizeBytes, caption }]. ↗ **ref:** `MediaAsset`\n- `nextNodeId` (string, obrigatório) — Nó seguinte (denormalizado — a transição usa este campo, não as edges).\n\n**Saídas (links)**:\n- Próximo → campo `nextNodeId` — **obrigatório**\n\n## message\n**Mensagem** — Envia uma mensagem (texto e/ou mídia) e segue para o próximo nó.\n\n**Campos (`data`)**:\n- `text` (string) — Texto da mensagem. Interpola variáveis nas 3 notações equivalentes ${a.b} / {$a.b} (aninhado, com |padrão). Ex.: ${customer.vars.plano|não informado}. Ex.: `\"Olá, {$name}!\"`\n- `media` (array) — Mídias anexadas (enviadas após o texto): [{ mediaAssetId, mimetype, name, sizeBytes, caption }]. Cada item referencia um MediaAsset da conta. ↗ **ref:** `MediaAsset`\n- `cta` (object) — whatsapp-interactive-replies: botão de link (CTA URL) do WhatsApp Oficial — { bodyText, buttonText (≤20), url, footerText?, header?: { mediaAssetId } (imagem de cabeçalho via MediaAsset) }. Enviado após texto/mídia; demais canais caem em texto com o link. Ex.: `{\"bodyText\":\"Acesse seu portal\",\"buttonText\":\"Abrir\",\"url\":\"https://exemplo.com\"}` ↗ **ref:** `MediaAsset`\n- `nextNodeId` (string, obrigatório) — Nó seguinte.\n\n**Saídas (links)**:\n- Próximo → campo `nextNodeId` — **obrigatório**\n\n## choice\n**Opções / Menu** — Mostra um menu de opções e ramifica conforme a escolha do cliente. Tem reprocesso por timeout (inatividade) e por opção inválida, com nº de tentativas e mensagens próprias. CLASSIFICADOR DE INTENÇÃO (opcional): quando a resposta LIVRE do cliente não casa com nenhuma opção, um LLM tenta mapeá-la p/ uma opção antes do retry de \"inválido\". Configurado no FLUXO (não no nó): campos `classifierSystemPrompt` (vazio = desligado), `classifierModelId`, `classifierProfileIds` no create/update do fluxo. Expõe as vars efêmeras `{$menu_opcoes}` e `{$menu_resposta}` ao prompt.\n\n**Campos (`data`)**:\n- `text` (string) — Enunciado do menu (antes das opções) — Meta \"Body text\".\n- `textFooter` (string) — Mensagem exibida após as opções — Meta \"Footer text\".\n- `headerText` (string) — whatsapp-interactive-replies: cabeçalho de texto (Meta \"Header text\", ≤60 chars). Só aparece no formato interativo (botões/lista).\n- `listButtonText` (string) — whatsapp-interactive-replies: rótulo do botão que abre o menu de lista (Meta \"Button text\", ≤20). Default \"Ver opções\". Só usado no formato LISTA (4–10 opções).\n- `interactive` (boolean) — whatsapp-interactive-replies: default true. No WhatsApp Oficial, 1–3 opções viram botões e 4–10 viram lista; false força o texto numerado clássico. >10 opções sempre vira texto.\n- `options` (array, obrigatório) — Opções: [{ label, sublabel?, section?, nextNodeId, variables?: [{key,value}] }]. `label` = título da opção (Meta \"Row title\"); `sublabel` = descrição (Meta \"Row description\", só na lista); `section` agrupa linhas numa seção da lista (opções com o mesmo valor caem juntas). `variables` seta variáveis ao escolher. Cada opção precisa de destino (ou usa defaultNextNodeId). Ex.: `[{\"label\":\"1) Vendas\",\"nextNodeId\":\"node_vendas\"}]`\n- `timeout` (number) — Segundos de inatividade até disparar timeout (default 30). Ex.: `30`\n- `timeoutRetry` (number) — Nº de retentativas após timeout (default 1).\n- `timeoutMsg` (string) — Mensagem ao reapresentar o menu por timeout.\n- `timeoutFailMsg` (string) — Mensagem ao exceder as retentativas de timeout (antes de ir p/ onTimeoutFail).\n- `invalidRetry` (number) — Nº máximo de entradas inválidas (default 2).\n- `invalidMsg` (string) — Mensagem ao rejeitar uma opção inválida.\n- `invalidFailMsg` (string) — Mensagem ao exceder as tentativas inválidas (antes de ir p/ onInvalidFail).\n\n**Saídas (links)**:\n- Opção N → `options[].nextNodeId` — **obrigatório**\n- Destino padrão (fallback) → campo `defaultNextNodeId` (handle `default`)\n- Timeout (inatividade) → campo `onTimeoutFail` (handle `timeout`)\n- Opção inválida → campo `onInvalidFail` (handle `invalid`)\n\n## dynamic_choice\n**Escolha Dinâmica** — Monta o menu a partir de uma variável de array do estado e ramifica pela escolha. Mesmo padrão de timeout/inválido do nó Opções. CLASSIFICADOR DE INTENÇÃO (opcional): quando a resposta LIVRE do cliente não casa com nenhum item (dinâmico ou estático), um LLM tenta mapeá-la p/ uma opção antes do retry de \"inválido\". Configurado no FLUXO (não no nó): campos `classifierSystemPrompt` (vazio = desligado), `classifierModelId`, `classifierProfileIds` no create/update do fluxo. Expõe as vars efêmeras `{$menu_opcoes}` e `{$menu_resposta}` ao prompt.\n\n**Campos (`data`)**:\n- `variableArray` (string, obrigatório) — Caminho da variável (array) que gera as opções. Ex.: `\"pedidos.items\"`\n- `text` (string) — Enunciado do menu — Meta \"Body text\".\n- `textFooter` (string) — Mensagem após as opções — Meta \"Footer text\".\n- `headerText` (string) — whatsapp-interactive-replies: cabeçalho de texto (Meta \"Header text\", ≤60). Só no formato interativo.\n- `listButtonText` (string) — whatsapp-interactive-replies: rótulo do botão da lista (Meta \"Button text\", ≤20). Default \"Ver opções\". Só no formato LISTA.\n- `interactive` (boolean) — whatsapp-interactive-replies: default true. 1–3 itens → botões, 4–10 → lista (WhatsApp Oficial); false força texto numerado. >10 sempre texto.\n- `variable` (string) — Variável onde salvar o valor escolhido.\n- `itemDescription` (string) — Template do rótulo de cada item (ex.: ${item.nome}) — Meta \"Row title\". Ex.: `\"${item.nome}\"`\n- `itemSubDescription` (string) — Template do sub-rótulo de cada item (ex.: ${item.descricao}) — Meta \"Row description\" (só na lista).\n- `itemSection` (string) — whatsapp-interactive-replies: template da seção da lista por item (ex.: ${item.categoria}) — Meta \"Section title\". Itens com a mesma seção agrupam.\n- `itemValue` (string) — Template do valor a salvar (ex.: ${item.id}); vazio = salva o item inteiro.\n- `staticOptions` (array) — Opções estáticas extras: [{ label, sublabel?, section?, value }]. `section` agrupa na lista.\n- `agentContext` (object) — choice→react_agent (NÍVEL DO NÓ — 1 config serve TODAS as opções, ≠ do nó Opções que é por opção): quando o destino escolhido for um nó react_agent, realimenta o agente em vez de transferir \"seco\". Campos: { enabled: boolean (liga/desliga), toolRes?: string (resultado entregue ao agente, texto ou JSON; vazio = usa o rótulo da opção; se o classificador de intenção identificar, o `contexto` extraído sobrescreve), startMsg?: string, prevMsg?: string, chatRestart?: boolean (default true), activeAgent?: boolean (default true) }. Ex.: `{\"enabled\":true,\"toolRes\":\"\",\"activeAgent\":true}`\n- `timeout` (number) — Segundos de inatividade até timeout (default 30).\n- `timeoutRetry` (number) — Retentativas após timeout (default 1).\n- `timeoutMsg` (string) — Mensagem ao reapresentar por timeout.\n- `timeoutFailMsg` (string) — Mensagem ao exceder timeout.\n- `invalidRetry` (number) — Máx. de entradas inválidas (default 2).\n- `invalidMsg` (string) — Mensagem ao rejeitar inválida.\n- `invalidFailMsg` (string) — Mensagem ao exceder inválidas.\n\n**Saídas (links)**:\n- Sucesso → campo `nextNodeId` (handle `success`) — **obrigatório**\n- Timeout (inatividade) → campo `onTimeoutFail` (handle `timeout`)\n- Opção inválida → campo `onInvalidFail` (handle `invalid`)\n\n## input\n**Entrada de Texto** — Captura uma resposta livre do cliente e salva numa variável.\n\n**Campos (`data`)**:\n- `text` (string) — Pergunta/instrução enviada ao cliente.\n- `variable` (string, obrigatório) — Caminho da variável onde salvar a resposta (usável depois como {$var}). Ex.: `\"pedido.numero\"`\n- `timeout` (number) — Segundos de inatividade até timeout (default 30).\n- `timeoutRetry` (number) — Retentativas após timeout (default 1).\n- `timeoutMsg` (string) — Mensagem ao repetir a pergunta por timeout.\n- `timeoutFailMsg` (string) — Mensagem ao exceder timeout (antes de onTimeoutFail).\n- `nextNodeId` (string, obrigatório) — Nó seguinte após capturar a entrada.\n\n**Saídas (links)**:\n- Sucesso / Próximo → campo `nextNodeId` — **obrigatório**\n- Timeout (inatividade) → campo `onTimeoutFail` (handle `timeout`)\n\n## classification\n**Classificação (Tags)** — Aplica TAGS ao ticket (não é classificação por IA): define/adiciona/remove tags e segue. Registra em TagHistory.\n\n**Campos (`data`)**:\n- `mode` (string) — Operação sobre as tags: set (substitui todas), add (acrescenta), remove (remove as que casarem). Default \"set\". Ex.: `\"set\"`\n- `tags` (array, obrigatório) — Tags a aplicar: [{ type, name }]. Ex.: [{ \"type\": \"motivo\", \"name\": \"suporte\" }]. Ex.: `[{\"type\":\"motivo\",\"name\":\"suporte\"}]`\n- `nextNodeId` (string, obrigatório) — Nó seguinte.\n\n**Saídas (links)**:\n- Próximo → campo `nextNodeId` — **obrigatório**\n\n## api\n**API (HTTP)** — Chama uma API HTTP externa e guarda a resposta numa variável (acessível depois via ${minhaVar.campo}).\n\n**Campos (`data`)**:\n- `method` (string) — Método HTTP: GET | POST | PUT | PATCH | DELETE (default GET). Ex.: `\"GET\"`\n- `url` (string, obrigatório) — URL do endpoint. Interpola ${a.b} / {$a.b} (aninhado, com |padrão). Ex.: `\"https://api.exemplo.com/cep/{$cep}\"`\n- `queryParams` (array) — Parâmetros de query: [{ key, value }] (value interpola ${a.b} / {$a.b} aninhado).\n- `authType` (string) — Autenticação: none | bearer | basic | key (default none).\n- `authData` (object) — Dados de auth conforme authType: bearer {token}; basic {username,password}; key {keyName,keyValue}.\n- `customHeaders` (array) — Cabeçalhos extras: [{ key, value }].\n- `contentType` (string) — Corpo p/ não-GET: json | urlencoded | formdata (default json).\n- `body` (string) — Corpo JSON raw (quando contentType=json). Interpola ${a.b} / {$a.b} (aninhado, com |padrão).\n- `formBody` (array) — Campos do corpo quando contentType≠json: [{ key, value }].\n- `timeout` (number) — Timeout em segundos (default 10, máx 120).\n- `responseEncoding` (string) — Encoding da resposta: em branco = utf-8; 'latin1' p/ ERPs ISO-8859-1; 'auto' p/ consertar acentos de APIs com encoding quebrado/misto.\n- `variable` (string) — Variável onde salvar a resposta da API (ex.: \"api\" → use ${api.campo}). Ex.: `\"api\"`\n- `nextNodeId` (string, obrigatório) — Nó seguinte.\n\n**Saídas (links)**:\n- Próximo → campo `nextNodeId` — **obrigatório**\n\n## condition\n**Condição** — Avalia condições sobre o estado e ramifica (verdadeiro/falso por condição + senão).\n\n**Campos (`data`)**:\n- `conditions` (array, obrigatório) — Condições (SE/SENÃO-SE): [{ variable, operator, value, trueNodeId?, falseNodeId? }]. `variable` aceita nome plano (ex.: `idade`) OU caminho com ponto p/ campo aninhado de objeto retornado por nó API/Código (ex.: `viacep.cep`) — igual à interpolação `${obj.campo}` de message/handover. `value` também é interpolado (`${path.campo}`, `{$var}`, `${var}`) — permite comparar com outra variável; sem token = literal. operator ∈ == | != | > | < | >= | <= | contains | not_contains | is_empty | is_not_empty (os 2 últimos dispensam value). Ex.: `[{\"variable\":\"viacep.cep\",\"operator\":\"is_not_empty\",\"trueNodeId\":\"node_ok\"}]`\n\n**Saídas (links)**:\n- Condição N → Verdadeiro/Falso → `conditions[].trueNodeId|falseNodeId` — **obrigatório**\n- Senão (else) → campo `elseNodeId` (handle `else`)\n\n## set_variables\n**Definir Variáveis** — Define/sobrescreve variáveis no estado do ticket e segue.\n\n**Campos (`data`)**:\n- `setVars` (array, obrigatório) — Pares a setar: [{ key, value }]. value interpola ${a.b} / {$a.b} (aninhado, com |padrão); ex.: ${campaign.vars.cod_erp}. Ex.: `[{\"key\":\"origem\",\"value\":\"whatsapp\"}]`\n- `target` (string) — Onde gravar as variáveis: 'flow' (flowState, temporário por-ticket — DEFAULT) | 'ticket' (persistente, aparece no painel CONTEXTO EDITÁVEL do agente) | 'customer' (perfil do cliente dono do contato) | 'channel' | 'account'. Ex.: `\"ticket\"`\n- `secret` (boolean) — Grava como VARIÁVEL SECRETA (credencial) no escopo escolhido, em vez da coluna pública. Segredos NÃO aparecem no flowState/snapshot da Jornada e são mascarados (••••) sem a permissão variables:view-secret. INVÁLIDO com target='flow' (o flowState é sempre snapshotado) → nesse caso é ignorado e tratado como público. Default false. Ex.: `false`\n- `nextNodeId` (string, obrigatório) — Nó seguinte.\n\n**Saídas (links)**:\n- Próximo → campo `nextNodeId` — **obrigatório**\n\n## sleep\n**Aguardar (Sleep)** — Pausa o fluxo por um tempo e então segue para o próximo nó. NÃO bloqueia — o ticket fica parado no nó e é retomado automaticamente quando o tempo expira (sobrevive a restart). Útil em automações (esperar entre passos) e em fluxos de cliente. Cap máximo de 7 dias.\n\n**Campos (`data`)**:\n- `durationValue` (number, obrigatório) — Quantidade de tempo a aguardar (>0). Ex.: `30`\n- `durationUnit` (string, obrigatório) — Unidade do tempo: 'seconds' | 'minutes' | 'hours' | 'days'. Ex.: `\"minutes\"`\n- `nextNodeId` (string, obrigatório) — Nó seguinte, após o tempo expirar.\n\n**Saídas (links)**:\n- Próximo (após aguardar) → campo `nextNodeId` — **obrigatório**\n\n## code\n**Código** — Executa JavaScript num sandbox (timeout 3s). Ramifica no fim: `return false`/`return -1` (ou erro/timeout) → saída de FALHA; qualquer outro retorno → SUCESSO. Pode navegar explicitamente via navigateTo()/navigateToFlow() (vence o roteamento por retorno). Ver o resource de scripting.\n\n**Campos (`data`)**:\n- `code` (string) — Código JS do sandbox (funções set/navigateTo/sendMsg/sendRequest/... — ver vchat://docs/flow-scripting).\n- `nextNodeId` (string) — Nó seguinte em caso de SUCESSO (retorno != false/-1 e sem erro), se o código não navegar explicitamente.\n- `failNodeId` (string) — Nó destino em caso de FALHA (código retorna false/-1 ou lança erro/timeout). Vazio = sem desvio → cai no sucessor (nextNodeId).\n\n**Saídas (links)**:\n- Sucesso (se não navegar no código) → campo `nextNodeId`\n- Falha (retorno false/-1 ou erro) → campo `failNodeId` (handle `fail`)\n\n## react_agent\n**Agente (ReAct)** — Agente de IA multi-step (ReAct) com profile/model/tools/rules. Conversa, usa ferramentas e ramifica no fim (sucesso/falha).\n\n**Campos (`data`)**:\n- `modelId` (number) — Modelo LLM da conta (sistema v2). Obrigatório se não usar `agent` legado. ↗ **ref:** `AIModel` (liste em `GET /ai/models`)\n- `agent` (string) — Chave legada do agente (compat de fluxos antigos; preferir modelId).\n- `profileId` (number) — Perfil de inferência (parâmetros de sampling). ↗ **ref:** `AIProfile` (liste em `GET /ai/profiles`)\n- `modelProfileIds` (array) — Profiles opcionais do modelo, no formato \"${src}:${id}\".\n- `toolIds` (array) — IDs de ferramentas disponíveis ao agente. ↗ **ref:** `AITool` (liste em `GET /ai/tools`)\n- `ruleIds` (array) — IDs de regras (AIRule) concatenadas com o `systemPrompt` numa ÚNICA mensagem `system`, na ordem: `<regras (priority asc)>\\n\\n<systemPrompt>`. PRECEDÊNCIA: regras e systemPrompt convivem no mesmo system message — uma regra IMPERATIVA (ex.: \"ofereça envio por WhatsApp ou e-mail\") pode contradizer/dominar o systemPrompt na prática. Se uma regra está sobrescrevendo o comportamento desejado, ajuste o texto da regra, desative-a, ou alinhe-a ao prompt — não há hierarquia formal além dessa concatenação. ↗ **ref:** `AIRule` (liste em `GET /ai/rules`)\n- `systemPrompt` (string) — Persona/instruções do agente. É concatenado APÓS as `ruleIds` na mensagem system (ver nota de precedência em ruleIds).\n- `startMessage` (string) — Mensagem que inicia o agente (simula entrada do cliente).\n- `preMessage` (string) — Mensagem enviada antes do processamento do agente.\n- `preMessageToHistory` (boolean) — Incluir o preMessage no histórico da IA.\n- `preMessageToChat` (boolean) — Enviar o preMessage no chat.\n- `maxAttempts` (number) — Máx. de iterações do loop ReAct (default 3).\n- `timeout` (number) — Timeout do agente em segundos (default 180).\n- `inactivityTimeout` (number) — Timeout de inatividade (s); vazio herda do fluxo.\n- `sttEnabled` (boolean) — Habilita STT (transcrição de áudio) neste nó.\n- `sttModelId` (number) — Modelo STT (override do default do fluxo). ↗ **ref:** `AIModel` (liste em `GET /ai/models`)\n- `sttProfileIds` (array) — Profiles opcionais do STT (\"${src}:${id}\").\n- `ttsEnabled` (boolean) — Habilita TTS (resposta em áudio) neste nó.\n- `ttsModelId` (number) — Modelo TTS (override do default do fluxo). ↗ **ref:** `AIModel` (liste em `GET /ai/models`)\n- `ttsProfileIds` (array) — Profiles opcionais do TTS (\"${src}:${id}\").\n- `nextNodeId` (string) — Nó seguinte ao concluir com sucesso.\n- `failNodeId` (string, obrigatório) — Nó destino em caso de falha do agente.\n\n**Saídas (links)**:\n- Sucesso / Próximo → campo `nextNodeId`\n- Falha → campo `failNodeId` (handle `fail`) — **obrigatório**\n- Inatividade → campo `onTimeoutFail` (handle `timeout`)\n\n## flow_transfer\n**Transferir p/ Fluxo** — Transfere o ticket para outro fluxo (opcionalmente num nó específico), setando variáveis antes.\n\n**Campos (`data`)**:\n- `targetFlowId` (number, obrigatório) — Fluxo de destino. ↗ **ref:** `Flow` (liste em `GET /flows`)\n- `nextNodeId` (string) — Nó inicial NO FLUXO DE DESTINO; vazio = \"start\".\n- `setVars` (array) — Variáveis a setar antes de transferir: [{ key, value }].\n\n**Saídas (links)**:\n- Próximo → campo `nextNodeId` — **obrigatório**\n\n## business_hours\n**Horário de Funcionamento** — Ramifica pelo horário de funcionamento (working/near/closed) e injeta {$businessHoursMessage} (o text da janela casada). Resolve o BusinessHoursProfile por gatilhos do setor. Em near/closed também injeta ${nextOpen.horario} (\"HH:MM\"), ${nextOpen.data} (\"DD/MM/AAAA\") e ${nextOpen.diaSemana} (pt-BR minúsculo, ex. \"segunda-feira\") com o próximo horário aberto (busca até 30 dias à frente); em working ou sem profile, nextOpen é null.\n\n**Campos (`data`)**:\n- `departmentId` (number) — Setor p/ resolver o perfil de horário; vazio = usa o setor atual do ticket. ↗ **ref:** `Department` (liste em `GET /departments`)\n\n**Saídas (links)**:\n- Funcionando → campo `workingNodeId` (handle `working`) — **obrigatório**\n- Próximo de funcionamento → campo `nearNodeId` (handle `near`)\n- Fora de funcionamento → campo `closedNodeId` (handle `closed`) — **obrigatório**\n\n## handover\n**Transbordo (Fila/Agente)** — Encaminha o ticket para a fila/atendimento humano (status WAITING + setor; dispara auto-assignment). Fora do horário do setor, desvia para o fallback se configurado. A ORDEM DE ATENDIMENTO na fila segue a Multi-Level Queue: 0-49 Absoluta (\"fura-fila\", menor primeiro), 50-69 ratio 2:1, 70-99 ratio 1:1, 100+ FIFO (default 100). A prioridade vem do campo `priority` deste nó (se preenchido) ou, na falta dele, de `flowState.priority` (definido via `set(\"priority\", n)` no nó Código); default 100.\n\n**Campos (`data`)**:\n- `departmentId` (number, obrigatório) — Setor de destino do atendimento. ↗ **ref:** `Department` (liste em `GET /departments`)\n- `text` (string) — Mensagem de transbordo enviada antes de liberar o controle.\n- `priority` (number) — Prioridade na fila (0-49 Absoluta/fura-fila, 50-69 ratio 2:1, 70-99 ratio 1:1, 100+ FIFO). Se preenchido, sobrepõe flowState.priority. Vazio = usa flowState.priority ou 100. Ex.: `10`\n\n**Saídas (links)**:\n- Fora do horário (fallback) → campo `fallbackNodeId` (handle `fallback`)\n\n## close\n**Encerrar** (terminal) — Encerra o atendimento (nó terminal — sem saída).\n\n**Campos (`data`)**:\n- `text` (string) — Mensagem de despedida enviada antes de encerrar (opcional).\n\n\n---\n\n# Fluxo — Scripting do nó Código & Variáveis\n\nReferência das funções disponíveis dentro de um **nó `code`** e das variáveis usáveis em textos/condições. Tudo roda num sandbox seguro (timeout 3s).\n\n> Estas funções valem **no nó `code` E nas tools de um nó `react_agent` (AITool)** — o sandbox é o mesmo, INCLUSIVE `sendMediaUrl`/`sendMediaBase64`/`setTicketVar`/`setCustomerVar`. As marcadas **`só nó Código`** (tags, `crypto`, `date/hour/now`) NÃO existem no sandbox de tool. Dentro de uma tool você também recebe `args` (o input da tool).\n\n## Funções (nó Código e tools de react_agent)\n\n> ⚠️ **Funções marcadas `async` exigem `await`.** Esquecer o `await` numa função que retorna valor (ex.: `sendRequest`) NÃO lança erro: devolve uma `Promise` que passa em `typeof === \"object\"` e serializa pra `{}` — o sintoma é uma \"resposta vazia\" que parece falha de rede/firewall, mas é só o `await` faltando.\n\n- **`set(nome, valor)`** — Salva uma variável no flowState do ticket; reutilize com {$nome} em mensagens/nós. Ex.: `set(\"cliente_vip\", true); set(\"saldo\", 150.50);`\n- **`set(\"priority\", n)`** — Define a prioridade de atendimento (0-49 absoluta / 50-99 ratio / 100+ normal). Só vale ao entrar na FILA por um nó Handover. Ex.: `set(\"priority\", 10);`\n- **`sha256(texto)`** — Hash SHA256 hex (assinar requisições / validar tokens). Ex.: `const sig = sha256(vars.token + \"chave\");`\n- **`crypto`** `só nó Código` — Módulo nativo crypto do Node (randomBytes, createHash, ...). Ex.: `crypto.randomBytes(8).toString(\"hex\")`\n- **`log(dado)`** — Envia ao console do servidor + painel DEBUG. Ex.: `log(\"Pedido #\" + vars.orderId);`\n- **`navigateTo(\"nodeId\")`** — Navega para um nó do fluxo atual (desvios condicionais). ⚠️ Dentro de uma tool de um nó react_agent NÃO use navigateTo para devolver o resultado ao agente: ela apenas pula de nó e o resultado da tool (toolRes) e o contexto do agente se perdem (o loop ReAct não continua). Para navegar E realimentar o agente, use agentChatToolContext. Ex.: `if (vars.idade < 18) navigateTo(\"bloqueio\"); else navigateTo(\"continuar\");`\n- **`navigateToFlow(\"nomeFluxo\", \"nodeId?\")`** — Transfere o ticket para outro fluxo; 2º arg (nó inicial) opcional (padrão \"start\"). Ex.: `navigateToFlow(\"Vendas\", \"promo-verao\");`\n- **`agentChatToolContext(nodeId, toolRes, options)`** — Navega E realimenta o agente ReAct com o resultado da tool — use isto (não navigateTo) dentro de tools de um nó react_agent. Args: nodeId = nó destino; toolRes = resposta da tool, entregue ao próximo agente; options = { activeAgent, chatRestart, toolName, flowId, startMsg, prevMsg }, onde prevMsg = mensagem injetada como se enviada pelo próprio agente.\n\n  ```js\n  agentChatToolContext(\"node_cep\", { ok: true }, {\n    activeAgent: true,\n    chatRestart: true,\n    prevMsg: \"Me informe seu CEP.\"\n  });\n  ```\n\n- **`awaitReply()  // alias: aguardarResposta()`** — (tool de react_agent) CEDE A VEZ ao cliente: encerra o turno do agente SEM o LLM gerar outra fala e AGUARDA a resposta do cliente. Use logo após enviar algo que pede ação (ex.: sendList/sendButtons) — senão, como toda tool devolve resultado ao LLM, o agente responde ANTES de o cliente escolher. A resposta do cliente reentra no mesmo nó e o agente continua de onde parou. Sem efeito em nó Código (lá não há loop ReAct).\n\n  ```js\n  sendList(\"Suas faturas\", \"Ver faturas\", secoes);\n  awaitReply(); // não fala de novo; espera o cliente tocar numa opção\n  ```\n\n- **`close(\"mensagem?\")`** — Encerra o ticket imediatamente (mensagem opcional).\n- **`appendMsg(\"texto\")`** — Acumula texto p/ enviar numa única bolha ao fim do nó.\n- **`sendMsg(\"texto\")`** — Envia uma bolha individual e imediata.\n- **`sendAppendedMsg()`** `async` (use `await`) — Envia o acumulado via appendMsg() e limpa o buffer.\n- **`setTicketVar(chave, valor)`** `async` (use `await`) — Grava uma variável PERSISTENTE do ticket (coluna própria, editável também via UI/API/MCP). Write-through: salva no banco E reflete no contexto. Leia depois com ${ticket.vars.chave}. Ex.: `await setTicketVar(\"protocolo\", \"2026-001\");`\n- **`setCustomerVar(chave, valor)`** `async` (use `await`) — Grava uma variável PERSISTENTE do CLIENTE dono do contato (se houver; senão é no-op). Write-through (banco + contexto). Leia com ${customer.vars.chave}. Ex.: `await setCustomerVar(\"plano\", \"fibra-600\");`\n- **`setSecret(escopo, chave, valor)`** `async` (use `await`) — Grava uma variável SECRETA (credencial) no escopo indicado — escopo ∈ \"ticket\" | \"customer\" | \"channel\" | \"account\". Diferente de setTicketVar/setCustomerVar, o segredo NÃO aparece no contexto/flowState nem no snapshot da Jornada, e é MASCARADO (••••) para quem não tem a permissão variables:view-secret. Na interpolação, ${escopo.vars.chave} resolve o valor secreto EM MEMÓRIA (sobrepõe o público de mesmo nome) — nunca é persistido no flowState. Use para token de API, senha, chave de assinatura etc.\n\n  ```js\n  await setSecret(\"account\", \"api_token\", vars.token_gerado);\n  // depois, num nó API: Authorization: Bearer ${account.vars.api_token}\n  ```\n\n- **`sendMediaBase64(base64, mime?, fileName?, caption?)`** `async` (use `await`) — Envia uma mídia ao contato a partir de base64 (ou data URI). Se `mime` ausente, detecta por magic bytes. Aceita PDF/imagem/áudio/vídeo (≤16MB). Ideal após um nó API que retorna o arquivo. Ex.: `await sendMediaBase64(vars.viacep_boleto, \"application/pdf\", \"boleto.pdf\", \"Segue seu boleto\");`\n- **`sendMediaUrl(url, caption?)`** `async` (use `await`) — Baixa a URL (http/https, anti-SSRF) e envia como mídia ao contato (≤16MB; MIME pelo Content-Type). Ex.: `await sendMediaUrl(\"https://exemplo.com/boleto.pdf\", \"Seu boleto\");`\n- **`downloadMedia(url)`** `async` (use `await`) — Baixa uma mídia UMA vez (url http/https, anti-SSRF; ou já um base64/data URI = no-op de rede) e devolve um data URI base64. Use p/ NÃO baixar 2x quando precisa enviar E processar o mesmo arquivo (ex.: enviar o boleto e extrair o Pix do mesmo download).\n\n  ```js\n  const pdf = await downloadMedia(vars.pdf_url);\n  await sendMediaBase64(pdf, \"application/pdf\");\n  const pix = await extractPix(pdf); // sem rebaixar\n  ```\n\n- **`extractPix(pdf)`** `async` (use `await`) — Extrai o código Pix EMVCo \"copia e cola\" (000201...) de um PDF de boleto. `pdf` = base64 | data URI | URL (normaliza os 3). Tenta texto (rápido) e cai no QR code da imagem (fallback). Retorna a string Pix ou null se não achar. Dica: combine com downloadMedia p/ enviar o PDF e extrair o Pix sem baixar 2x.\n\n  ```js\n  const pix = await extractPix(vars.pdf_url);\n  if (pix) { sendMsg(\"Pix copia e cola:\"); sendMsg(pix); }\n  ```\n\n- **`sendButtons(body, buttons, opts?)`** — Envia botões de resposta rápida (≤3) no WhatsApp Oficial; demais canais/erro caem em texto numerado. buttons = [{ title }]. opts: { header?, footer? }. Ex.: `sendButtons(\"Confirma o pedido?\", [{ title: \"Sim\" }, { title: \"Não\" }], { footer: \"Toque numa opção\" });`\n- **`sendList(body, buttonText, sections, opts?)`** — Envia uma lista interativa (4–10 linhas em até 10 seções) no WhatsApp Oficial; demais canais/erro caem em texto numerado. sections = [{ title?, rows: [{ title, description? }] }]. opts: { header?, footer? }. Ex.: `sendList(\"Escolha um produto\", \"Ver opções\", [{ title: \"Bebidas\", rows: [{ title: \"Água\", description: \"500ml\" }, { title: \"Refri\", description: \"Lata\" }] }], { footer: \"Cardápio\" });`\n- **`sendCta(body, url, buttonText, opts?)`** — Envia um botão de link (CTA URL) no WhatsApp Oficial; demais canais/erro caem em texto com o link. opts: { footer?, headerMediaId? } (headerMediaId = id de um MediaAsset p/ imagem de cabeçalho). Ex.: `sendCta(\"Acesse seu portal\", \"https://portal.exemplo.com\", \"Abrir portal\", { footer: \"Link seguro\" });`\n- **`sendContact(name, phones) · sendContact([{name, phones}])`** — Envia um card de contato (vCard) ao cliente — WhatsApp Oficial e QR Code. Aceita 1 contato (`name` + `phones` = telefone único ou lista) ou vários (array de `{name, phones}`). Só nome + telefone(s); números BR são normalizados. Canais sem suporte caem em texto (nome — telefone).\n\n  ```js\n  sendContact(\"Suporte VChat\", \"5581991234567\");\n  // ou vários:\n  sendContact([{ name: \"Vendas\", phones: [\"5581990000000\"] }, { name: \"Financeiro\", phones: [\"5581988887777\"] }]);\n  ```\n\n- **`setTicketTags([{type, name}])`** `async` (use `await`) `só nó Código` — Define EXCLUSIVAMENTE estas tags (remove as anteriores).\n- **`addTicketTags([{type, name}])`** `async` (use `await`) `só nó Código` — Adiciona tags sem remover as existentes.\n- **`removeTicketTags([{type, name}])`** `async` (use `await`) `só nó Código` — Remove apenas as tags que coincidirem (type+name).\n- **`date() · hour() · now()`** `só nó Código` — Data (DD/MM/AAAA), hora (HH:mm) e data-hora completa, no fuso do sistema.\n- **`sendRequest(url, params, headers, method, contentType, responseEncoding?)`** `async` (use `await`) — Requisição HTTP — assíncrona, SEMPRE use `await`. ⚠️ Sem `await` o retorno NÃO são os dados e sim uma Promise: ela passa em `typeof === \"object\"`, serializa pra `{}` em JSON.stringify e NÃO lança erro — o sintoma é uma \"resposta vazia\" que parece falha de rede/firewall (mas não é). contentType: \"json\" | \"form-data\" (padrão) | \"urlencoded\". responseEncoding (opcional): encoding da resposta (ex.: \"latin1\"/\"iso-8859-1\" para ERPs legados que respondem em ISO-8859-1). \"auto\" = decodifica de forma tolerante e conserta acentos de APIs com encoding quebrado/misto (ex.: ERP legado que mistura UTF-8 e latin-1 por campo). Em branco = utf-8 (NÃO detecta o charset do header automaticamente — servidores legados mentem). Retorna o corpo (JSON já parseado quando a resposta é JSON).\n\n  ```js\n  const r = await sendRequest(\n    \"https://api/x\",\n    { a: 1 },\n    { Authorization: \"Bearer t\" },\n    \"POST\", \"json\"\n  );\n  // ERP com encoding quebrado/misto (acentos) → \"auto\":\n  const erp = await sendRequest(\n    \"https://erp/clientes\",\n    { doc: \"123\" },\n    {}, \"POST\", \"form-data\", \"auto\"\n  );\n  ```\n\n- **`startConversation({ channelId, phone, name?, text?, templateId?, templateParams?, departmentId? })`** `async` (use `await`) `só nó Código` — Inicia uma conversa INDIVIDUAL (proativa) com um número: cria um ticket na FILA (WAITING) do setor e dispara a 1ª mensagem. Baileys exige o canal CONECTADO + `text`; Oficial (fora da janela 24h) exige `templateId` aprovado + `templateParams`. ⚠️ LIMITE de 200 por execução — para MUITOS destinatários use uma CAMPANHA (createCampaign + addCampaignRecipients + launchCampaign), não um laço. Retorna { ok, ticketId } ou { ok:false, error }.\n\n  ```js\n  const r = await startConversation({ channelId: 3, phone: \"5581991234567\", text: \"Olá! Sua fatura venceu.\", departmentId: 2 });\n  if (!r.ok) log(r.error);\n  ```\n\n- **`createCampaign({ name, channelId, messageType?, messageTemplate?, mediaAssetId?, templateId?, templateParams?, ratePerSecond?, batchSize?, ... })`** `async` (use `await`) `só nó Código` — Cria uma CAMPANHA de envio em rascunho (DRAFT) e retorna o id. Ideal para disparos em massa (ex.: avisar todos os clientes com fatura atrasada) — o dispatcher respeita rate-limit, quiet-hours e opt-out. Depois use addCampaignRecipients + launchCampaign. Ex.: `const id = await createCampaign({ name: \"Fatura atrasada \" + date(), channelId: 3, messageType: \"TEXT\", messageTemplate: \"Olá {{1}}, sua fatura venceu.\" });`\n- **`addCampaignRecipients(campaignId, [{ number, name?, vars? }])`** `async` (use `await`) `só nó Código` — Adiciona destinatários a uma campanha (DRAFT). `vars` são as colunas custom por destinatário (ex.: { valor: \"99,90\" }) usadas no template {{n}}/${var}. Ex.: `await addCampaignRecipients(id, [{ number: \"5581991234567\", name: \"Ana\", vars: { valor: \"120,00\" } }]);`\n- **`launchCampaign(campaignId)`** `async` (use `await`) `só nó Código` — Valida (preflight) e AGENDA a campanha (DRAFT → SCHEDULED); o dispatcher (PRIMARY) assume o envio. Retorna { ok, recipientCount }.\n\n  ```js\n  const r = await launchCampaign(id);\n  log(\"destinatários: \" + r.recipientCount);\n  ```\n\n- **`getAgentStatus(agentId)`** `async` (use `await`) — Consulta a situação de um OPERADOR (use o código do último agente em ${ticket.lastAgent.id}). Retorna { id, name, status, online, activeTickets, capacity } — status ∈ READY (pronto) | ON_BREAK (pausa) | ... ; online = teve atividade nos últimos 5 min (heartbeat); activeTickets/capacity = carga atual/máxima. Agente inexistente/de outra conta → { error: \"agent_not_found\", online: false }. Use para decidir, num fluxo de espera, se devolve o ticket ao mesmo operador ou manda pra fila.\n\n  ```js\n  const st = await getAgentStatus(vars.ticket.lastAgent.id);\n  if (st.online && st.status === \"READY\" && st.activeTickets < st.capacity) {\n    await assignTicketToAgent(st.id);   // volta pro mesmo operador\n  } else {\n    await sendTicketToQueue();           // cai na fila do setor atual\n  }\n  ```\n\n- **`assignTicketToAgent(agentId)`** `async` (use `await`) — Ação TERMINAL: atribui o atendimento a um operador específico (status IN_PROGRESS) e ENCERRA o controle do bot (o fluxo para aqui). Grava também o \"último agente\" (${ticket.lastAgent.*}). Se o agente não existir/for de outra conta, é NO-OP (o fluxo continua normalmente). Combine com getAgentStatus para só atribuir quando o operador está online e com vaga. Ex.: `await assignTicketToAgent(vars.ticket.lastAgent.id);`\n- **`sendTicketToQueue(departmentId?)`** `async` (use `await`) — Ação TERMINAL: devolve o atendimento à FILA (status WAITING) e ENCERRA o controle do bot. Sem argumento mantém o setor atual do ticket; passe um departmentId para trocar de setor. Dispara auto-atribuição + aviso de posição na fila. Preserva o \"último agente\" (${ticket.lastAgent.*}).\n\n  ```js\n  await sendTicketToQueue();        // fila do setor atual\n  // ou: await sendTicketToQueue(3); // manda pro setor 3\n  ```\n\n- **`queryTickets({ status?, departmentId?, channelId?, userId?, unassigned?, tags?, createdBeforeMinutes?, idleMinutes?, limit? })`** `async` (use `await`) `só nó Código` — CONSULTA (leitura) tickets da conta por filtros ricos. status/departmentId/channelId/userId = arrays (casa QUALQUER um); unassigned:true = sem operador; tags = { any:[{type,name}] } (tem alguma) e/ou { all:[{type,name}] } (tem todas); createdBeforeMinutes = aberto há mais de X min (createdAt); idleMinutes = sem atividade há mais de X min (updatedAt). limit default 100, MÁX 500. Retorna { tickets:[{ id, status, departmentId, channelId, userId, priority, createdAt, updatedAt, contact:{number,name}, tags:[{type,name}] }], total }. Combine com getDepartmentStatus + as ações para automatizar (ex.: reencaminhar tickets em espera de um setor sem agente online).\n\n  ```js\n  const { tickets } = await queryTickets({ status: [\"WAITING\"], departmentId: [2], idleMinutes: 30, limit: 200 });\n  for (const t of tickets) { await transferTicketToQueue(t.id, 5); }\n  ```\n\n- **`getDepartmentStatus(departmentId)`** `async` (use `await`) `só nó Código` — Situação operacional de um SETOR: { departmentId, onlineAgents, readyAgents, waitingTickets } — onlineAgents = agentes com atividade nos últimos 5 min (heartbeat); readyAgents = com status READY; waitingTickets = tickets em espera no setor. Setor inexistente/de outra conta → null. Use para decidir remanejamento (ex.: se onlineAgents === 0, reencaminhar a fila para outro setor/fluxo).\n\n  ```js\n  const st = await getDepartmentStatus(2);\n  if (st && st.onlineAgents === 0) {\n    const { tickets } = await queryTickets({ status: [\"WAITING\"], departmentId: [2] });\n    for (const t of tickets) await transferTicketToQueue(t.id, 5); // setor de plantão\n  }\n  ```\n\n- **`transferTicketToQueue(ticketId, departmentId?)`** `async` (use `await`) `só nó Código` — AÇÃO (conta no teto de 200): reencaminha o ticket para a FILA (WAITING) + auto-atribuição + aviso de fila. Sem departmentId mantém o setor atual; passe um para trocar de setor (setor de outra conta é ignorado). Retorna { ok } ou { ok:false, error:\"not_found\" }. Ex.: `await transferTicketToQueue(t.id, 5);`\n- **`transferTicketToFlow(ticketId, flowId)`** `async` (use `await`) `só nó Código` — AÇÃO (conta no teto): re-injeta o ticket num FLUXO automático (volta pro bot, status BOT, começa pelo nó inicial). O fluxo precisa estar ATIVO e aceitar transferência de atendente. Retorna { ok } ou { ok:false, error } (ex.: fluxo inativo/sem essa permissão/sem nó inicial). Ex.: `await transferTicketToFlow(t.id, 12);`\n- **`closeTicket(ticketId, { reasonId?, message? })`** `async` (use `await`) `só nó Código` — AÇÃO (conta no teto): ENCERRA o ticket (registrado como fechado pela automação). message = mensagem opcional enviada ao cliente antes de fechar; reasonId = motivo de resolução (ignorado se não for da conta). Se houver pesquisa de satisfação casando no fechamento, o ticket entra em SURVEY (comportamento padrão). Retorna { ok } ou { ok:false, error:\"not_found\" }. Ex.: `await closeTicket(t.id, { message: \"Encerramos por inatividade. Qualquer coisa, é só chamar!\" });`\n- **`assignTicket(ticketId, agentId)`** `async` (use `await`) `só nó Código` — AÇÃO (conta no teto): atribui o ticket a um OPERADOR (status IN_PROGRESS + grava \"último agente\"). Agente inexistente/de outra conta → { ok:false, error:\"invalid_agent\" }. Retorna { ok } em caso de sucesso. Ex.: `await assignTicket(t.id, 7);`\n- **`setTicketPriority(ticketId, prioridade)`** `async` (use `await`) `só nó Código` — AÇÃO (conta no teto): define a prioridade do ticket (número; menor = mais prioritário na fila de múltiplos níveis). Retorna { ok } ou { ok:false, error }. Ex.: `await setTicketPriority(t.id, 1); // topo da fila`\n- **`sendTicketMessage(ticketId, texto)`** `async` (use `await`) `só nó Código` — AÇÃO (conta no teto): envia uma mensagem (como BOT) no canal do ticket-ALVO — diferente de sendMsg/appendMsg, que falam no ticket ATUAL do fluxo. Vai pela fila durável de envio. Retorna { ok } ou { ok:false, error:\"not_found\"|\"empty_message\" }. Ex.: `await sendTicketMessage(t.id, \"Olá! Retomando seu atendimento.\");`\n- **`setTicketTagsById(ticketId, [{type, name}])`** `async` (use `await`) `só nó Código` — AÇÃO (conta no teto): define EXCLUSIVAMENTE estas tags no ticket-ALVO (remove as anteriores) — versão by-id de setTicketTags. Retorna { ok, tags } ou { ok:false, error:\"not_found\" }. Ex.: `await setTicketTagsById(t.id, [{ type: \"Motivo\", name: \"Reengajamento\" }]);`\n- **`addTicketTagsById(ticketId, [{type, name}])`** `async` (use `await`) `só nó Código` — AÇÃO (conta no teto): adiciona tags ao ticket-ALVO sem remover as existentes (by-id). Retorna { ok, tags } ou { ok:false, error:\"not_found\" }. Ex.: `await addTicketTagsById(t.id, [{ type: \"Automação\", name: \"Fila estagnada\" }]);`\n- **`removeTicketTagsById(ticketId, [{type, name}])`** `async` (use `await`) `só nó Código` — AÇÃO (conta no teto): remove do ticket-ALVO apenas as tags que coincidirem (type+name) (by-id). Retorna { ok, tags } ou { ok:false, error:\"not_found\" }. Ex.: `await removeTicketTagsById(t.id, [{ type: \"Automação\", name: \"Fila estagnada\" }]);`\n- **`kvSet(namespace, key, value, { ttl? })`** `async` (use `await`) `só nó Código` — STORE chave→valor da CONTA: grava (sobrescreve) `value` (qualquer JSON, ≤64KB) sob namespace+key. `ttl` (segundos) opcional define expiração. namespace ≤64 chars, key ≤191. Use p/ estado durável de automação (cursor de sincronização, contador, cache leve). Retorna true.\n\n  ```js\n  await kvSet(\"sync\", \"ultimo_cursor\", { id: 4821, ts: now() });\n  await kvSet(\"cache\", \"cotacao_usd\", 5.12, { ttl: 3600 }); // expira em 1h\n  ```\n\n- **`kvGet(namespace, key)`** `async` (use `await`) `só nó Código` — STORE: lê o valor de namespace+key. Retorna null se a chave não existe, expirou, ou foi gravada sem valor (membership pura). Para checar SÓ existência use kvHas.\n\n  ```js\n  const cur = await kvGet(\"sync\", \"ultimo_cursor\");\n  if (cur) log(\"retomando do id \" + cur.id);\n  ```\n\n- **`kvHas(namespace, key)`** `async` (use `await`) `só nó Código` — STORE: true se a chave existe e não expirou (existência pura, independente do valor). Use quando gravou uma marca sem valor (dedup). Ex.: `if (await kvHas(\"processados\", \"pedido:\" + id)) return; // já tratei`\n- **`kvSetIfAbsent(namespace, key, value?, { ttl? })`** `async` (use `await`) `só nó Código` — STORE (atômico): grava SÓ se a chave ainda não existe (ou expirou). Retorna true só na 1ª vez, false se já existia — base do \"fazer exatamente uma vez\". `value` opcional (omita p/ marca de membership). `ttl` (segundos) opcional.\n\n  ```js\n  if (await kvSetIfAbsent(\"boletos\", \"boleto:\" + id, null, { ttl: 90*24*3600 })) {\n    await startConversation({ channelId, phone, text: \"Seu boleto venceu.\" });\n  } // senão: já avisei este boleto\n  ```\n\n- **`dispatchOnce(key, { ttl? })`** `async` (use `await`) `só nó Código` — STORE (açúcar de kvSetIfAbsent no namespace \"dispatch\"): retorna true só na 1ª vez que `key` é vista — o jeito mais curto de garantir envio único. `ttl` (segundos) opcional (após expirar, a mesma key dispara de novo).\n\n  ```js\n  if (await dispatchOnce(\"aniversario:\" + clienteId + \":\" + ano)) {\n    await sendTicketMessage(t.id, \"Feliz aniversário!\");\n  }\n  ```\n\n- **`kvDelete(namespace, key)`** `async` (use `await`) `só nó Código` — STORE: remove uma chave. Retorna true se removeu algo, false se não existia. Ex.: `await kvDelete(\"cache\", \"cotacao_usd\");`\n- **`kvList(namespace, { prefix?, limit?, after?, withValues? })`** `async` (use `await`) `só nó Código` — STORE: LISTA as keys (não-expiradas) de um namespace, PAGINADO. Retorna { items: [{ key, expiresAt, updatedAt, value? }], nextCursor }. Ordena por key (asc). `prefix` filtra (ex.: \"boleto:\"); `limit` default 100 (máx 1000; 200 com `withValues`); `after` = key da última página (cursor de continuação; passe o nextCursor recebido); `withValues:true` inclui o valor de cada chave. ⚠️ Namespace pode ser GRANDE — SEMPRE pagine (repita enquanto nextCursor não for null) e prefira `prefix` p/ fatiar.\n\n  ```js\n  let after; do {\n    const r = await kvList(\"boletos\", { prefix: \"boleto:\", limit: 500, after });\n    for (const it of r.items) log(it.key);\n    after = r.nextCursor;\n  } while (after);\n  ```\n\n- **`kvCount(namespace, { prefix? })`** `async` (use `await`) `só nó Código` — STORE: conta as keys (não-expiradas) de um namespace (opcionalmente por `prefix`). Ex.: quantos boletos já avisei.\n\n  ```js\n  const n = await kvCount(\"boletos\", { prefix: \"boleto:\" });\n  log(\"avisados: \" + n);\n  ```\n\n- **`findOrCreateContact({ number, name? })`** `async` (use `await`) `só nó Código` — Busca um CONTATO (número) da conta por variantes BR (com/sem 9º dígito) e CRIA se não existir — idempotente. Use em automação para garantir o contato antes de vincular a um cliente. Retorna { id, number, name } ou { ok:false, error:\"invalid_number\" }.\n\n  ```js\n  const c = await findOrCreateContact({ number: \"5581991234567\", name: \"Ana\" });\n  log(\"contato \" + c.id);\n  ```\n\n- **`createCustomer({ name, document?, email?, company?, variables? })`** `async` (use `await`) `só nó Código` — Cria um CLIENTE (identidade que agrupa contatos) e retorna { id }. IDEMPOTENTE por `document` (CPF/CNPJ): se já existe um cliente com esse documento na conta, RETORNA o existente e faz MERGE das `variables` passadas (não duplica). `variables` = mapa string→valor (≤32KB). Sem `document`, sempre cria. Retorna { id } ou { ok:false, error }. Ex.: `const cli = await createCustomer({ name: \"Ana Souza\", document: \"12345678900\", variables: { mk_codigo: \"MK-4821\" } });`\n- **`findCustomerByVariable(chave, valor)`** `async` (use `await`) `só nó Código` — Busca o 1º CLIENTE (não deletado) da conta cuja variável `chave` seja igual a `valor` (comparado como texto). Retorna { id, name, document, active, variables, contacts:[{id,number}] } (contatos = donos atuais) ou null. ⚠️ Sem índice no campo de variáveis: é uma varredura O(n) na conta — não chame em laço quente; cacheie o resultado (ex.: kvSet).\n\n  ```js\n  const cli = await findCustomerByVariable(\"mk_codigo\", vars.cod);\n  if (!cli) { /* criar */ } else { log(\"já existe: \" + cli.id); }\n  ```\n\n- **`linkContactToCustomer(customerId, contactId)`** `async` (use `await`) `só nó Código` — VINCULA um contato a um cliente (posse atual). Mantém a invariante de ≤1 vínculo aberto por contato: se o contato já pertence a OUTRO cliente, o vínculo anterior é ENCERRADO e um novo é aberto (histórico preservado, nada é apagado); se já é o mesmo cliente, é no-op. Ambos os ids precisam ser da conta. Retorna { id } (vínculo atual) ou { ok:false, error:\"not_found\" }.\n\n  ```js\n  const cli = await createCustomer({ name: \"Ana\", document: \"12345678900\" });\n  const c = await findOrCreateContact({ number: \"5581991234567\" });\n  await linkContactToCustomer(cli.id, c.id);\n  ```\n\n- **`setCustomerVarById(customerId, chave, valor)`** `async` (use `await`) `só nó Código` — Grava/atualiza uma variável do CLIENTE por id (merge por chave; valor null/\"\" REMOVE a chave). Versão by-id de setCustomerVar (que usa o cliente do ticket atual) — necessária em automação headless, que não tem \"cliente atual\". Leia depois com ${customer.vars.chave} quando o cliente for o dono do contato do ticket. Retorna { ok:true } ou { ok:false, error:\"not_found\" }. Ex.: `await setCustomerVarById(cli.id, \"mk_status\", \"adimplente\");`\n- **`updateContactNumber(contactId, number)`** `async` (use `await`) `só nó Código` — Atualiza o NÚMERO de um contato (normalizado BR). Se o novo número já pertence a OUTRO contato da conta, NÃO sobrescreve: retorna { ok:false, error:\"number_in_use\", existingContactId } — nesse caso use findOrCreateContact + linkContactToCustomer para \"cadastrar o número novo e revincular\" o cliente. Sucesso → { id, number }.\n\n  ```js\n  const r = await updateContactNumber(c.id, \"5581988887777\");\n  if (!r.ok && r.error === \"number_in_use\") {\n    const novo = await findOrCreateContact({ number: \"5581988887777\" });\n    await linkContactToCustomer(cli.id, novo.id);\n  }\n  ```\n\n\n## Contexto de leitura no nó Código\n- `vars.name` — nome do contato\n- `vars.phone` — número limpo\n- `vars.phoneId` — JID completo\n- `vars.msgStart` — 1ª mensagem do cliente\n- `vars.priority` — prioridade atual\n- `vars.<minhaVar>` — qualquer variável salva via set() ou nó Entrada\n- `vars.ttsEnabled` — tts-flow-output: liga/desliga a saída em ÁUDIO (TTS) do bot em runtime — set(\"ttsEnabled\", true) passa a responder por voz, false volta a texto (também setável pelo nó Set Variables). Semeado pelo toggle do fluxo (ttsDefaultEnabled) e pela pergunta de preferência de áudio; exige um modelo TTS configurado no fluxo.\n- `ticketId` — ID numérico do ticket (somente leitura)\n- `vars.ticket.vars.<chave>` — flow-context-variables: variáveis persistentes do ticket (use ${ticket.vars.chave} em textos; grave com setTicketVar)\n- `vars.ticket.lastAgent.{id,name}` — flow-agent-status-routing: último OPERADOR que atendeu este ticket — ${ticket.lastAgent.id} (código, use em getAgentStatus/assignTicketToAgent) e ${ticket.lastAgent.name} (nome de exibição). Sobrevive a devolução à fila/encerramento. Ausente se o ticket nunca foi atendido por um humano (ou se esse operador foi removido).\n- `vars.customer.{name,document,vars.<chave>}` — flow-context-variables: dados do cliente dono do contato — ${customer.name}, ${customer.vars.chave} (grave com setCustomerVar); ausente se o contato não tem cliente\n- `vars.campaign.{name,status,templateId,vars.<chave>}` — flow-context-variables: dados da campanha de origem — ${campaign.name}, ${campaign.vars.chave}; ausente se o ticket não nasceu de campanha\n- `vars.channel.{id,name,type}` — flow-context-variables: dados do canal do ticket — ${channel.name}, ${channel.type} (whatsapp_qrcode | whatsapp_official | api), ${channel.id} (código do canal)\n- `vars.referral.{headline,body,sourceId,sourceUrl,sourceType,mediaType,imageUrl,videoUrl,thumbnailUrl,ctwaClid,welcomeMessage}` — whatsapp-ad-referral: dados do anúncio Click-to-WhatsApp (CTWA) quando o cliente chegou clicando num anúncio da Meta — ${referral.headline}, ${referral.sourceId}, ${referral.ctwaClid}; presente só na 1ª mensagem do clique e ausente se o ticket não veio de anúncio\n- `vars.nextOpen.{horario,data,diaSemana}` — business-hours-next-open: próximo horário de funcionamento aberto, injetado pelo nó Horário de Funcionamento quando o status é near/closed — ${nextOpen.horario} (\"HH:MM\"), ${nextOpen.data} (\"DD/MM/AAAA\"), ${nextOpen.diaSemana} (pt-BR minúsculo, ex.: \"segunda-feira\"); busca até 30 dias à frente. null em working ou sem perfil de horário\n\n## Variáveis em textos (mensagens, opções, captions)\n- `{$name}` nome do contato\n- `{$phone}` número limpo\n- `{$phoneId}` JID completo\n- `{$ticketId}` id do atendimento\n- `{$msgStart}` 1ª mensagem do cliente\n- `{$nomeDaVar}` qualquer variável salva\n- `{$businessHoursMessage}` department-business-hours: texto da janela de horário casada, injetado pelo nó Horário de Funcionamento (working/near/closed) — use num nó de destino para exibir o aviso configurado (ex.: \"Estamos fechados, retornamos às 9h\"). Vazio quando a janela não tem texto configurado.\n- `${caminho.ponto} · {$caminho.ponto} (· ${caminho.ponto} legado)` PADRÃO: `${...}` (ou `{$...}`) resolve campo ANINHADO do estado — ex.: ${customer.vars.plano}, {$campaign.vars.cod_erp}, ${api.user.email}. Funciona em QUALQUER nó (mensagem, condição, API, Set Variables, agente) — notação unificada. A notação antiga `{{...}}` ainda é aceita (equivalente), mas é LEGADO/compatibilidade — prefira `${...}`.\n- `${caminho|padrão} (default)` valor de fallback quando a variável está ausente/vazia: ex.: ${customer.vars.plano|não informado}. Vale nas três notações; sem default e ausente, o token fica literal.\n- `{@var}` flow-send-dynamic-media: ENVIA a variável como MÍDIA (PDF/imagem/áudio/vídeo). O valor pode ser data URI, base64 cru (detecta tipo por magic bytes) ou URL http(s). Em qualquer texto de nó (ex.: Mensagem \"Segue seu boleto: {@boleto}\"). O restante do texto vira legenda; vários {@} = várias mídias. ≤16MB; a var é descartada do estado após o envio.\n\n## Funções de tempo \"ao vivo\" (em textos — avaliadas no envio)\n- `{date()}` (DD/MM/AAAA)\n- `{hour()}` (HH:mm)\n- `{now()}` (DD/MM/AAAA HH:mm:ss)\n\n## Bloqueado por segurança (sandbox)\n`require` · `process` · `global` · `eval` · `Function` · `fetch` · `Buffer` · `setTimeout` · `setInterval`. Timeout máximo de **3 segundos**; erros vão para o log do servidor + DEBUG.\n"},"servers":[{"url":"/api/public/v1","description":"Base da API pública (v1)"}],"security":[{"bearerApiKey":[]}],"tags":[{"name":"Relatórios","description":"Atendimentos, operadores, campanhas e satisfação (somente leitura)."},{"name":"Campanhas","description":"Criar e disparar campanhas de mensagens."},{"name":"Automações","description":"Automações agendadas (cron) que executam um fluxo headless. Criar/editar/excluir, disparar manualmente e consultar o histórico de execuções."},{"name":"Fluxos","description":"CRUD de fluxos do chatbot, validação e debug síncrono. Os tipos de nó (campos de `data` + saídas/links) são documentados em `GET /schema` (`nodes`) e no resource MCP `vchat://docs/nodes`."},{"name":"Gestão de conta","description":"Setores, tags, motivos, textos predefinidos, filas, horários e pesquisas."},{"name":"IA","description":"Perfis, ferramentas, regras e modelos de IA. Modelos: ACCOUNT (próprios) + SYSTEM (leitura, sem a infra interna); escrita só ACCOUNT. Segredos write-only."},{"name":"Usuários","description":"Operadores da conta (nunca SUPER_ADMIN; senha write-only; sem escalada de permissões)."},{"name":"Canais","description":"Canais (Master/Detail). Segredos write-only; sem operações de sessão de conexão (QR Code)."},{"name":"Tickets","description":"Variáveis (contexto) do atendimento — SÓ a coluna `variables` (mapa string→valor), lida no fluxo como `${ticket.vars.*}`. CRUD de ticket não é exposto."},{"name":"Atendimento","description":"Atendimento AO VIVO (paridade total com a AgentView): leitura de conversas/mensagens em tempo real, download de mídia, envio como operador, ações de ticket (claim/transferir/encerrar/handover), tags, início proativo de conversa e presença/sessão/status de operadores. **Identidade do operador**: as ações com autor humano exigem, além da API Key da conta, o header **`X-Operator-Id`** (fallback `body.operatorId`/`query.operatorId`) identificando o operador (User ⊆ conta) em cujo nome a ação é executada — gateadas pelo escopo `agent:act` (envio de mensagem também exige `messages:send`). O operador carimba `senderId`/`lastAgentId`/auditoria e, **por padrão, o RBAC do operador é aplicado** — a ação respeita as permissões de painel dele (ex.: `tickets:edit-message`, `tickets:delete-message`, `tags:manage`, `tickets:transfer-*`, `tickets:manage`); sem a permissão → **403**. **Modo override**: uma API Key com o escopo `agent:rbac-override` pode, enviando o header **`X-VChat-Auth-Mode: token`**, IGNORAR o RBAC do operador e usar o teto da própria key — todo uso é auditado e, quando o operador não teria a permissão, o audit grava `operatorWouldBeDenied:true` + `missingPermission`. Os endpoints de LEITURA não exigem operador (só `tickets:read`/`users:read`). Envios são ASSÍNCRONOS (fila durável) → **202**."},{"name":"Webhooks","description":"Webhooks de SAÍDA (tempo real): cadastre endpoints HTTPS da sua aplicação para receber eventos de atendimento (`message.received`, `message.sent`, `ticket.created`, `ticket.status_changed`, `ticket.assigned`, `ticket.closed`). Cada entrega é assinada (`X-VChat-Signature: sha256=...` HMAC do corpo cru com o `secret`) e idempotente (`X-VChat-Delivery-Id`). O `secret` é **write-only** — só é exibido em claro na criação e na rotação. **Reentrega resiliente:** uma entrega que falha é reagendada com backoff em 2 estágios — **denso** (≈15s × 6 tentativas, para soluços rápidos) → **exponencial** (1min dobrando até o teto de 6h). O sistema segue tentando por **até 24h desde a 1ª falha**; esgotado esse prazo o evento vai para a **DLQ** (dead-letter queue), de onde pode ser **reentregue** (`POST /webhooks/dlq/redrive`) preservando o mesmo `X-VChat-Delivery-Id`. Como o mesmo evento pode chegar mais de uma vez, **deduplique por `X-VChat-Delivery-Id`**. Escopo: `webhooks:manage`."},{"name":"Clientes","description":"Variáveis (contexto) do cliente — SÓ a coluna `variables` (mapa string→valor), lida no fluxo como `${customer.vars.*}`. CRUD de cliente não é exposto."},{"name":"Schema","description":"Manifesto único de contrato (tipos de nó, scripting e schemas das entidades graváveis) — a mesma fonte que valida o backend e alimenta o MCP."}],"components":{"securitySchemes":{"bearerApiKey":{"type":"http","scheme":"bearer","description":"API Key da conta (`vck_live_...`). Gerada no painel → Configurações → API Keys."}},"schemas":{"Pagination":{"type":"object","properties":{"total":{"type":"integer"},"page":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"}}},"Error":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]},"WebhookEndpoint":{"type":"object","properties":{"id":{"type":"integer"},"url":{"type":"string","format":"uri","example":"https://api.suaempresa.com/vchat/webhook"},"events":{"type":"array","items":{"type":"string"},"description":"Eventos assinados; vazio = todos.","example":["message.received","ticket.closed"]},"active":{"type":"boolean"},"lastStatus":{"type":"string","nullable":true,"description":"Resultado da última entrega (ok / http_500 / timeout / disabled)."},"lastAt":{"type":"string","format":"date-time","nullable":true},"createdAt":{"type":"string","format":"date-time"},"hasSecret":{"type":"boolean","description":"Se há um segredo definido (o valor em si nunca é retornado em leitura)."},"secretMasked":{"type":"string","example":"••••"}}},"Department":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome do setor.","example":"Suporte"},"description":{"description":"Descrição opcional do setor.","anyOf":[{"type":"string"},{"type":"null"}]},"active":{"description":"Ativo? (default true). No update, false desativa.","example":true,"type":"boolean"}},"required":["name"],"id":"Department","description":"Setor de atendimento (account-scoped, soft-delete)."},"Tag":{"type":"object","properties":{"type":{"type":"string","minLength":1,"description":"Categoria/tipo da tag (agrupa tags afins).","example":"motivo"},"name":{"type":"string","minLength":1,"description":"Nome da tag (único por type na conta).","example":"Reclamação"},"color":{"description":"Cor em hex p/ exibição.","example":"#e11d48","anyOf":[{"type":"string"},{"type":"null"}]},"description":{"description":"Descrição opcional.","anyOf":[{"type":"string"},{"type":"null"}]},"active":{"description":"Ativa? (default true).","example":true,"type":"boolean"}},"required":["type","name"],"id":"Tag","description":"Tag de ticket (account-scoped, único por type+name, soft-delete)."},"ResolutionReason":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome do motivo de resolução (único entre irmãos do mesmo pai na conta).","example":"Resolvido na 1ª resposta"},"parentId":{"description":"ID do motivo-pai (null/ausente = raiz/nível 1). Permite hierarquia de níveis (categoria → subcategoria → …). A seleção válida no fechamento é sempre uma folha (nó sem filhos).","example":12,"ref":"ResolutionReason","refEndpoint":"GET /resolution-reasons","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"active":{"description":"Ativo? (default true).","example":true,"type":"boolean"}},"required":["name"],"id":"ResolutionReason","description":"Motivo de resolução de ticket — árvore hierárquica por conta (parentId), account-scoped, soft-delete."},"ResolutionReasonProfile":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome da regra de obrigatoriedade.","example":"Suporte sempre exige motivo"},"enabled":{"description":"Ativa? (default true).","example":true,"type":"boolean"},"priority":{"description":"Prioridade de RESOLUÇÃO — qual regra VENCE quando várias casam (canal∩setor∩tags); menor = vence; default 100.","example":100,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"channelIds":{"description":"IDs de canais-gatilho (vazio = todos). Devem pertencer à conta.","example":[],"ref":"Channel","refEndpoint":"GET /channels","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"departmentIds":{"description":"IDs de setores-gatilho (vazio = global). Devem pertencer à conta.","example":[],"ref":"Department","refEndpoint":"GET /departments","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagIds":{"description":"IDs de tags-gatilho. Devem pertencer à conta.","example":[],"ref":"Tag","refEndpoint":"GET /tags","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagMatchMode":{"description":"Casa com QUALQUER tag (any) ou TODAS (all). Default \"any\".","example":"any","type":"string","enum":["any","all"]},"required":{"description":"Quando esta regra casa, informar o motivo de resolução é obrigatório para o agente encerrar o ticket. Default true. Use false + priority baixa como exceção (ex.: \"neste canal nunca é obrigatório\").","example":true,"type":"boolean"}},"required":["name"],"id":"ResolutionReasonProfile","description":"Regra GLOBAL de obrigatoriedade de motivo de resolução por gatilhos (canal∩setor∩tags + priority). A 1ª regra que casa (priority asc) decide se o motivo é obrigatório no fechamento pelo agente."},"MessagePrefixProfile":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome da regra de prefixo de mensagem.","example":"Sem prefixo no canal VIP"},"enabled":{"description":"Ativa? (default true).","example":true,"type":"boolean"},"priority":{"description":"Prioridade de RESOLUÇÃO — qual regra VENCE quando várias casam (canal∩setor∩tags); menor = vence; default 100.","example":100,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"channelIds":{"description":"IDs de canais-gatilho (vazio = todos). Devem pertencer à conta.","example":[],"ref":"Channel","refEndpoint":"GET /channels","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"departmentIds":{"description":"IDs de setores-gatilho (vazio = global). Devem pertencer à conta.","example":[],"ref":"Department","refEndpoint":"GET /departments","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagIds":{"description":"IDs de tags-gatilho. Devem pertencer à conta.","example":[],"ref":"Tag","refEndpoint":"GET /tags","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagMatchMode":{"description":"Casa com QUALQUER tag (any) ou TODAS (all). Default \"any\".","example":"any","type":"string","enum":["any","all"]},"sendPrefix":{"description":"Quando esta regra casa, define se a mensagem do agente leva o prefixo `*Nome*` (true) ou não (false). Default true. Sem regra alguma casando, o comportamento padrão é enviar o prefixo (preserva o comportamento atual).","example":true,"type":"boolean"}},"required":["name"],"id":"MessagePrefixProfile","description":"Regra GLOBAL de envio do prefixo `*Nome do agente*` nas mensagens, por gatilhos (canal∩setor∩tags + priority). A 1ª regra que casa (priority asc) decide se o prefixo é enviado. Sem match, o padrão é enviar."},"PredefinedText":{"type":"object","properties":{"title":{"type":"string","minLength":1,"description":"Título do texto predefinido.","example":"Saudação inicial"},"shortcut":{"type":"string","minLength":1,"description":"Atalho disparado no chat com /<código>. Normalizado (trim, minúsculo, sem barra inicial).","example":"bemvindo"},"content":{"type":"string","description":"Conteúdo da mensagem. Interpola ${var}/{$var}. Obrigatório quando NÃO há mídia anexada; com mídia (mediaLibraryItemId), pode ficar vazio (vira legenda).","example":"Olá! Como posso ajudar?"},"active":{"description":"Ativo? (default true).","example":true,"type":"boolean"},"mediaLibraryItemId":{"description":"ID do item da biblioteca de mídia a anexar quando o atalho for disparado. null/ausente = atalho de texto puro. O item deve ser visível à conta (mídia compartilhada da empresa); atalhos compartilhados não podem referenciar mídia pessoal de um agente.","example":123,"ref":"MediaLibraryItem","anyOf":[{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},{"type":"null"}]}},"required":["title","shortcut","content"],"id":"PredefinedText","description":"Texto predefinido COMPARTILHADO da conta (userId forçado null — pessoais de agente não são geríveis via API). Pode anexar UMA mídia da biblioteca curada via mediaLibraryItemId; nesse caso o list devolve um bloco `media` com previewUrl assinado e disponibilidade."},"PredefinedTextMedia":{"type":"object","properties":{"itemId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"ID do MediaLibraryItem referenciado.","example":123},"label":{"type":"string","description":"Rótulo da mídia na biblioteca.","example":"Tabela de preços 2026"},"mimetype":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"MIME type do arquivo.","example":"application/pdf"},"sizeBytes":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Tamanho do arquivo em bytes.","example":84213},"previewUrl":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"URL assinada (accountId-scoped, TTL) para baixar a mídia. null quando indisponível/invisível.","example":"/api/media-assets/45/raw?exp=...&acc=1&sig=..."},"available":{"type":"boolean","description":"true se a mídia está visível para a conta (não desativada, mesmo tenant, escopo válido). false → o atalho continua existindo, mas a mídia não anexa.","example":true}},"required":["itemId","label","mimetype","sizeBytes","previewUrl","available"],"id":"PredefinedTextMedia","description":"Metadados da mídia anexada a um texto predefinido (só no list; read-only)."},"AccountVariables":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{},"description":"Mapa chave→valor das variáveis da conta (contexto persistente lido no fluxo como ${account.vars.*}). MERGE por chave: as chaves enviadas sobrescrevem/criam; as demais são preservadas (envie valor null/\"\" para limpar uma chave).","example":{"empresa":"Loja Exemplo","horario_atendimento":"9h às 18h"}}},"required":["variables"],"id":"AccountVariables","description":"Variáveis (contexto) da conta. Apenas a coluna `variables` (mapa string→valor) é editável. Atualização por MERGE de chaves."},"AccountSecretVariables":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{},"description":"Mapa chave→valor das variáveis SECRETAS da conta. WRITE-ONLY: na leitura os valores são mascarados (`••••`) — só as chaves aparecem. MERGE por chave no PATCH: as chaves enviadas sobrescrevem/criam; as demais são preservadas (valor null/\"\" remove a chave).","example":{"chave_api_erp":"erp_live_xxxx","segredo_integracao":"s3cr3t"}}},"required":["variables"],"id":"AccountSecretVariables","description":"Variáveis SECRETAS (contexto) da conta. WRITE-ONLY na API: o GET devolve valores mascarados (`••••`), o PATCH grava por MERGE de chaves. Nunca ecoa o valor real."},"QueueProfile":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome do perfil.","example":"Padrão"},"enabled":{"description":"Ativo? (default true).","example":true,"type":"boolean"},"priority":{"description":"Prioridade de RESOLUÇÃO do perfil — qual perfil VENCE quando vários casam (canal∩setor∩tags); menor = vence; default 100. ⚠️ NÃO é a ordem de atendimento da fila — essa é definida por ticket no nó `handover` (campo `priority`) ou por `set(\"priority\", n)`.","example":100,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"channelIds":{"description":"IDs de canais-gatilho (vazio = todos). Devem pertencer à conta.","example":[],"ref":"Channel","refEndpoint":"GET /channels","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"departmentIds":{"description":"IDs de setores-gatilho (vazio = global). Devem pertencer à conta.","example":[],"ref":"Department","refEndpoint":"GET /departments","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagIds":{"description":"IDs de tags-gatilho. Devem pertencer à conta.","example":[],"ref":"Tag","refEndpoint":"GET /tags","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagMatchMode":{"description":"Casa com QUALQUER tag (any) ou TODAS (all). Default \"any\".","example":"any","type":"string","enum":["any","all"]},"enableQueueInfo":{"description":"Informa posição/tempo de fila ao cliente.","type":"boolean"},"queueInfoInterval":{"description":"Intervalo (s) entre avisos de fila.","example":30,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"avgServiceTime":{"description":"Tempo médio de atendimento (min) p/ estimativa.","example":10,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"queueMaxInformMin":{"description":"Teto (min) de espera informável.","example":60,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"queueRenotifyDropPct":{"description":"Queda grande: renotifica imediatamente (bypassa o intervalo) se a espera cair ≥ X%.","example":30,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"queueMinDropPct":{"description":"Queda mínima exigida para renotificar quando o intervalo já decorreu.","example":5,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"queueNextInLineEnabled":{"description":"Avisa quando o cliente é o próximo da fila.","type":"boolean"},"queueCalcWindowMin":{"description":"Janela (min) p/ calcular a média de atendimento.","example":120,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"queueMinSampleSize":{"description":"Amostra mínima de atendimentos p/ confiar na estimativa.","example":5,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"enableAgentGreeting":{"description":"Dispara saudação do agente ao assumir.","type":"boolean"},"positionTimeTemplates":{"description":"Templates de \"posição + tempo\" (1 sorteado). Vars: {posicao}/{tempo}.","type":"array","items":{"type":"string"}},"positionOnlyTemplates":{"description":"Templates de \"só posição\".","type":"array","items":{"type":"string"}},"nextInLineTemplates":{"description":"Templates de \"você é o próximo\".","type":"array","items":{"type":"string"}},"agentGreetingTemplates":{"description":"Templates de saudação do agente.","type":"array","items":{"type":"string"}}},"required":["name"],"id":"QueueProfile","description":"Perfil de FILA global por gatilhos (posição/tempo, saudação). resolveQueueProfile escolhe por canal∩setor∩tags + priority."},"BusinessHoursProfile":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome do perfil.","example":"Padrão"},"enabled":{"description":"Ativo? (default true).","example":true,"type":"boolean"},"priority":{"description":"Prioridade de RESOLUÇÃO do perfil — qual perfil VENCE quando vários casam (canal∩setor∩tags); menor = vence; default 100. ⚠️ NÃO é a ordem de atendimento da fila — essa é definida por ticket no nó `handover` (campo `priority`) ou por `set(\"priority\", n)`.","example":100,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"channelIds":{"description":"IDs de canais-gatilho (vazio = todos). Devem pertencer à conta.","example":[],"ref":"Channel","refEndpoint":"GET /channels","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"departmentIds":{"description":"IDs de setores-gatilho (vazio = global). Devem pertencer à conta.","example":[],"ref":"Department","refEndpoint":"GET /departments","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagIds":{"description":"IDs de tags-gatilho. Devem pertencer à conta.","example":[],"ref":"Tag","refEndpoint":"GET /tags","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagMatchMode":{"description":"Casa com QUALQUER tag (any) ou TODAS (all). Default \"any\".","example":"any","type":"string","enum":["any","all"]},"schedule":{"description":"Agenda por dia da semana. Chave \"0\"=domingo … \"6\"=sábado → lista de janelas. Horários wall-clock GMT-3. Ex.: {\"1\":[{\"start\":\"08:00\",\"end\":\"18:00\",\"status\":\"working\"}]}.","example":{"1":[{"start":"08:00","end":"18:00","status":"working"}]},"type":"object","additionalProperties":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string","description":"Início \"HH:MM\" (wall-clock GMT-3).","example":"08:00"},"end":{"type":"string","description":"Fim \"HH:MM\" (> start; p/ virar o dia, divida em 2 janelas).","example":"12:00"},"status":{"description":"working=aberto, near=quase fechando, closed=fechado. Default \"working\".","example":"working","type":"string","enum":["working","near","closed"]},"text":{"description":"Mensagem da janela — VOLTA pro fluxo em {$businessHoursMessage} quando esta janela casa.","type":"string"}},"required":["start","end"]}}},"holidays":{"description":"Feriados/datas especiais que SOBREPÕEM a agenda semanal. Cada um casa por dia/mês (year ausente=recorrente todo ano; com year=só naquele ano). windows vazio = fechado o dia todo.","example":[{"month":12,"day":25,"name":"Natal","text":"Feliz Natal! Voltamos dia 26."}],"type":"array","items":{"type":"object","properties":{"month":{"type":"integer","minimum":1,"maximum":12,"description":"Mês (1=janeiro … 12=dezembro).","example":12},"day":{"type":"integer","minimum":1,"maximum":31,"description":"Dia do mês (1-31).","example":25},"year":{"description":"Ano específico (ex.: 2026). Ausente/null = RECORRENTE (casa todo ano nesse dia/mês). Precedência no match: ano-fixo > recorrente.","example":2026,"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"name":{"description":"Rótulo administrativo do feriado (não exibido ao cliente).","example":"Natal","type":"string"},"windows":{"description":"Janelas próprias do feriado (ex.: meio-expediente). Ausente/vazio = fechado o dia todo.","type":"array","items":{"type":"object","properties":{"start":{"type":"string","description":"Início \"HH:MM\" (wall-clock GMT-3).","example":"08:00"},"end":{"type":"string","description":"Fim \"HH:MM\" (> start; p/ virar o dia, divida em 2 janelas).","example":"12:00"},"status":{"description":"working=aberto, near=quase fechando, closed=fechado. Default \"working\".","example":"working","type":"string","enum":["working","near","closed"]},"text":{"description":"Mensagem da janela — VOLTA pro fluxo em {$businessHoursMessage} quando esta janela casa.","type":"string"}},"required":["start","end"]}},"text":{"description":"Mensagem quando fechado o dia todo (windows vazio) — volta pro fluxo em {$businessHoursMessage}.","example":"Feriado de Natal, voltamos amanhã.","type":"string"}},"required":["month","day"],"description":"Feriado/data especial. Sobrepõe a agenda semanal no dia em que casa."}}},"required":["name"],"id":"BusinessHoursProfile","description":"Horário de funcionamento global por gatilhos. O nó business_hours ramifica working/near/closed e injeta {$businessHoursMessage} (o text da janela casada). Feriados sobrepõem a agenda semanal. Em near/closed também injeta ${nextOpen.horario} (\"HH:MM\"), ${nextOpen.data} (\"DD/MM/AAAA\") e ${nextOpen.diaSemana} (pt-BR minúsculo) com o próximo horário aberto (até 30 dias à frente); em working ou sem profile, nextOpen é null."},"Survey":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome do perfil.","example":"Padrão"},"enabled":{"description":"Ativo? (default true).","example":true,"type":"boolean"},"priority":{"description":"Prioridade de RESOLUÇÃO do perfil — qual perfil VENCE quando vários casam (canal∩setor∩tags); menor = vence; default 100. ⚠️ NÃO é a ordem de atendimento da fila — essa é definida por ticket no nó `handover` (campo `priority`) ou por `set(\"priority\", n)`.","example":100,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"channelIds":{"description":"IDs de canais-gatilho (vazio = todos). Devem pertencer à conta.","example":[],"ref":"Channel","refEndpoint":"GET /channels","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"departmentIds":{"description":"IDs de setores-gatilho (vazio = global). Devem pertencer à conta.","example":[],"ref":"Department","refEndpoint":"GET /departments","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagIds":{"description":"IDs de tags-gatilho. Devem pertencer à conta.","example":[],"ref":"Tag","refEndpoint":"GET /tags","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagMatchMode":{"description":"Casa com QUALQUER tag (any) ou TODAS (all). Default \"any\".","example":"any","type":"string","enum":["any","all"]},"questions":{"minItems":1,"type":"array","items":{"type":"object","properties":{"id":{"description":"ID estável da pergunta (STRING — usado como chave de agregação no relatório; o painel gera UUID). Número é aceito e coagido p/ string (backward-compat).","example":"q1","type":"string"},"type":{"type":"string","enum":["rating","nps","text","choice"],"description":"rating=nota 1..N (CSAT), text=aberta, choice=opções. (`nps` é legado — mantido só por compatibilidade.)","example":"rating"},"label":{"type":"string","description":"Texto da pergunta enviado ao cliente.","example":"De 1 a 5, como avalia o atendimento?"},"options":{"description":"Opções (apenas type=choice).","type":"array","items":{"type":"string"}},"ratingLabels":{"description":"Rótulo opcional por nota (apenas type=rating): índice 0 = nota 1, índice 1 = nota 2, … (ex.: [\"Ruim\",\"Regular\",\"Ótimo\"]). O cliente vê \"1. Ruim\", \"2. Regular\", …; a resposta gravada continua sendo o número (médias inalteradas). Vazio/ausente = só o número.","example":["Ruim","Regular","Ótimo"],"type":"array","items":{"type":"string"}}},"required":["type","label"],"description":"Pergunta da pesquisa."},"description":"Perguntas da pesquisa (≥1), conduzidas uma a uma no mesmo ticket."},"closedBy":{"description":"Filtra por quem fechou o ticket (ex.: agent/flow).","type":"array","items":{"type":"string"}},"cooldownHours":{"description":"Não repesquisar o mesmo contato dentro de X horas.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"preMessage":{"description":"Mensagem antes da 1ª pergunta.","anyOf":[{"type":"string"},{"type":"null"}]},"postMessage":{"description":"Mensagem de agradecimento ao fim.","anyOf":[{"type":"string"},{"type":"null"}]},"inactivityMins":{"description":"Expira a pesquisa por inatividade (min). Default 30.","example":30,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"useInteractive":{"description":"Usa botões/listas nativas do WhatsApp Oficial nas perguntas (1–3 opções=botões, 4–10=lista, >10=texto); fallback automático para texto numerado no WhatsApp via QR Code / API. Default true.","example":true,"type":"boolean"}},"required":["name","questions"],"id":"Survey","description":"Pesquisa de satisfação (CSAT) selecionada por gatilhos no fechamento do ticket (status SURVEY)."},"ClosingMessage":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome do perfil.","example":"Padrão"},"enabled":{"description":"Ativo? (default true).","example":true,"type":"boolean"},"priority":{"description":"Prioridade de RESOLUÇÃO do perfil — qual perfil VENCE quando vários casam (canal∩setor∩tags); menor = vence; default 100. ⚠️ NÃO é a ordem de atendimento da fila — essa é definida por ticket no nó `handover` (campo `priority`) ou por `set(\"priority\", n)`.","example":100,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"channelIds":{"description":"IDs de canais-gatilho (vazio = todos). Devem pertencer à conta.","example":[],"ref":"Channel","refEndpoint":"GET /channels","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"departmentIds":{"description":"IDs de setores-gatilho (vazio = global). Devem pertencer à conta.","example":[],"ref":"Department","refEndpoint":"GET /departments","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagIds":{"description":"IDs de tags-gatilho. Devem pertencer à conta.","example":[],"ref":"Tag","refEndpoint":"GET /tags","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagMatchMode":{"description":"Casa com QUALQUER tag (any) ou TODAS (all). Default \"any\".","example":"any","type":"string","enum":["any","all"]},"message":{"type":"string","minLength":1,"description":"Mensagem de encerramento enviada ao fechar o ticket.","example":"Atendimento encerrado. Obrigado!"},"closedBy":{"description":"Filtra por quem fechou (ex.: agent/flow).","type":"array","items":{"type":"string"}}},"required":["name","message"],"id":"ClosingMessage","description":"Mensagem de encerramento global por gatilhos (priority asc)."},"ChannelUpdate":{"type":"object","properties":{"name":{"description":"Nome do canal.","example":"WhatsApp Vendas","type":"string","minLength":1},"active":{"description":"Ativo?","example":true,"type":"boolean"},"defaultFlowId":{"description":"Fluxo padrão do canal (⊆ conta).","ref":"Flow","refEndpoint":"GET /flows","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"filterRuleId":{"description":"Regra de filtro (whitelist/blacklist) do canal (⊆ conta).","ref":"FilterRule","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"id":"ChannelUpdate","description":"Atualização de metadados do canal. Segredos write-only (resposta traz só flags *Set); sessão (QR Code)/create/delete fora da API."},"ChannelVariables":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{},"description":"Mapa chave→valor das variáveis do canal (contexto persistente lido no fluxo como ${channel.vars.*}). MERGE por chave: as chaves enviadas sobrescrevem/criam; as demais são preservadas (envie valor null/\"\" para limpar uma chave).","example":{"saudacao":"Bem-vindo à loja!","fila_padrao":"vendas"}}},"required":["variables"],"id":"ChannelVariables","description":"Variáveis (contexto) do canal. Apenas a coluna `variables` (mapa string→valor) é editável. Atualização por MERGE de chaves."},"ChannelSecretVariables":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{},"description":"Mapa chave→valor das variáveis SECRETAS do canal. WRITE-ONLY: na leitura os valores são mascarados (`••••`) — só as chaves aparecem. MERGE por chave no PATCH: as chaves enviadas sobrescrevem/criam; as demais são preservadas (valor null/\"\" remove a chave).","example":{"api_token":"sk-live-xxxx","webhook_secret":"s3cr3t"}}},"required":["variables"],"id":"ChannelSecretVariables","description":"Variáveis SECRETAS (contexto) do canal. WRITE-ONLY na API: o GET devolve valores mascarados (`••••`), o PATCH grava por MERGE de chaves. Nunca ecoa o valor real."},"TicketVariables":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{},"description":"Mapa chave→valor das variáveis do ticket (contexto persistente lido no fluxo como ${ticket.vars.*}). MERGE por chave: as chaves enviadas sobrescrevem/criam; as demais são preservadas (envie valor null/\"\" para limpar uma chave).","example":{"protocolo":"AB-1234","origem":"site"}}},"required":["variables"],"id":"TicketVariables","description":"Variáveis (contexto) do ticket. Apenas a coluna `variables` (mapa string→valor) é editável; o restante do ticket não é exposto na API pública. Atualização por MERGE de chaves."},"TicketSecretVariables":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{},"description":"Mapa chave→valor das variáveis SECRETAS do ticket. WRITE-ONLY: na leitura os valores são mascarados (`••••`) — só as chaves aparecem. MERGE por chave no PATCH: as chaves enviadas sobrescrevem/criam; as demais são preservadas (valor null/\"\" remove a chave).","example":{"token_pagamento":"pay_xxxx"}}},"required":["variables"],"id":"TicketSecretVariables","description":"Variáveis SECRETAS (contexto) do ticket. WRITE-ONLY na API: o GET devolve valores mascarados (`••••`), o PATCH grava por MERGE de chaves. Nunca ecoa o valor real."},"SendContact":{"type":"object","properties":{"contacts":{"minItems":1,"type":"array","items":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome de exibição do contato (não pode ser vazio).","example":"Maria Souza"},"phones":{"minItems":1,"type":"array","items":{"type":"string"},"description":"Telefones do contato (1+). Aceita com/sem DDI/máscara; números BR são normalizados (colapso do 9º dígito).","example":["5511988887777"]}},"required":["name","phones"],"description":"Um contato do cartão (nome + telefones)."},"description":"Lista de contatos a enviar como cartão de Contato (vCard). Ao menos um contato; cada um exige nome e ao menos um telefone.","example":[{"name":"Maria Souza","phones":["5511988887777"]}]}},"required":["contacts"],"id":"SendContact","description":"Envio de mensagem de Contato (cartão vCard) a um ticket existente. Persiste 1 mensagem atribuída ao BOT, emite o evento de tempo real e despacha ao canal (Oficial inline / QR Code via fila durável / fallback texto). Não cria ticket."},"PublicTicketLive":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"ID do ticket (conversa).","example":1234},"status":{"type":"string","description":"Estado do ticket no ciclo de atendimento: BOT | WAITING | IN_PROGRESS | SURVEY | CLOSED.","example":"IN_PROGRESS"},"channelId":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"ID do canal (Channel mestre) do ticket; null em ticket legado sem canal.","example":7},"channelType":{"type":"string","description":"Tipo do canal: WHATSAPP | FACEBOOK | INSTAGRAM | API | EMAIL | TELEGRAM.","example":"WHATSAPP"},"departmentId":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"ID do setor atual do ticket (null = sem setor definido).","example":3},"contact":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"ID do contato.","example":88},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Nome do contato (null se desconhecido).","example":"Maria Souza"},"number":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Identificador do contato no canal (telefone/handle/e-mail). Null p/ canais sem número.","example":"5511988887777"}},"required":["id","name","number"],"description":"Contato (interlocutor) do ticket."},"currentOperatorId":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"ID do operador humano atualmente responsável pelo ticket (null = bot/fila/sem responsável).","example":42},"unreadCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Quantidade de mensagens não lidas do contato.","example":2},"lastMessageAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Horário (ISO, GMT-3 shifted) da última mensagem do ticket. Null se ainda sem mensagens.","example":"2026-07-30T14:03:21.000Z"},"tags":{"type":"array","items":{"type":"string"},"description":"Nomes das tags aplicadas ao ticket.","example":["vip","cobranca"]}},"required":["id","status","channelId","channelType","departmentId","contact","currentOperatorId","unreadCount","lastMessageAt","tags"],"id":"PublicTicketLive","description":"Ticket (conversa) ao vivo — visão de leitura da API pública. Espelha o que a AgentView mostra na lista/topo da conversa. Reusado como payload nos webhooks ticket.*."},"PublicMessage":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"ID da mensagem.","example":55501},"ticketId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"ID do ticket ao qual a mensagem pertence.","example":1234},"direction":{"type":"string","enum":["inbound","outbound"],"description":"Sentido da mensagem: inbound (do contato) | outbound (do sistema/operador).","example":"outbound"},"senderType":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Origem da mensagem (ex.: CONTACT | AGENT | BOT | SYSTEM | EXTERNAL). Null p/ legado.","example":"AGENT"},"operatorId":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"ID do operador humano autor da mensagem (null = bot/sistema/contato).","example":42},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Corpo de texto da mensagem (null p/ mídia sem legenda).","example":"Olá, como posso ajudar?"},"mediaType":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Tipo de mídia quando houver (ex.: image | video | audio | document | sticker). Null = texto.","example":"image"},"mediaUrl":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"URL para baixar a mídia (endpoint público autenticado). Null quando não há mídia.","example":"/api/public/v1/messages/55501/media"},"quotedMessageId":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"ID da mensagem citada (reply), quando a mensagem responde a outra. Null caso contrário.","example":55499},"createdAt":{"type":"string","description":"Horário de criação (ISO, GMT-3 shifted).","example":"2026-07-30T14:03:21.000Z"},"deliveredAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Horário de entrega ao destinatário (ISO), quando disponível. Null se não confirmado.","example":"2026-07-30T14:03:22.000Z"},"readAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Horário de leitura pelo destinatário (ISO), quando disponível. Null se não lida/confirmada.","example":null}},"required":["id","ticketId","direction","senderType","operatorId","body","mediaType","mediaUrl","quotedMessageId","createdAt","deliveredAt","readAt"],"id":"PublicMessage","description":"Mensagem de um ticket — visão de leitura da API pública. Reusado como payload nos webhooks message.received / message.sent."},"PublicOperatorStatus":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"ID do operador (User).","example":42},"name":{"type":"string","description":"Nome de exibição do operador.","example":"João Atendente"},"status":{"type":"string","description":"Status atual do operador (ex.: READY | ON_BREAK | LOGGED_OUT | LOGGED_IN).","example":"READY"},"online":{"type":"boolean","description":"true se o operador tem sessão viva (heartbeat/atividade dentro da janela de online).","example":true},"activeTickets":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Quantidade de tickets em atendimento com este operador.","example":3},"capacity":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Capacidade máxima de tickets simultâneos do operador (null = sem limite definido).","example":10}},"required":["id","name","status","online","activeTickets","capacity"],"id":"PublicOperatorStatus","description":"Status e presença de um operador — visão de leitura para o motor de roteamento do integrador (auto-assign exige READY + online)."},"PublicWebhookEndpoint":{"type":"object","properties":{"id":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"ID do endpoint de webhook.","example":5},"url":{"type":"string","description":"URL https de destino que recebe os POSTs assinados (HMAC-SHA256 em X-VChat-Signature).","example":"https://api.meu-sistema.com/vchat/webhook"},"events":{"type":"array","items":{"type":"string"},"description":"Eventos assinados por este endpoint. [] = todos os eventos. Ex.: message.received, message.sent, ticket.created, ticket.status_changed, ticket.assigned, ticket.closed.","example":["message.received","ticket.closed"]},"active":{"type":"boolean","description":"true se o endpoint está ativo (recebendo entregas).","example":true},"lastStatus":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Resultado da última tentativa de entrega (ex.: ok | http_500 | timeout | disabled). Null se nunca entregou.","example":"ok"},"lastAt":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Horário (ISO, GMT-3 shifted) da última tentativa de entrega. Null se nunca entregou.","example":"2026-07-30T14:05:00.000Z"},"createdAt":{"type":"string","description":"Horário de criação (ISO, GMT-3 shifted).","example":"2026-07-30T14:00:00.000Z"}},"required":["id","url","events","active","lastStatus","lastAt","createdAt"],"id":"PublicWebhookEndpoint","description":"Endpoint de webhook de saída — visão de leitura da API pública. O campo `secret` é WRITE-ONLY e NUNCA é retornado (só exibido na criação)."},"CustomerVariables":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{},"description":"Mapa chave→valor das variáveis do cliente (contexto persistente lido no fluxo como ${customer.vars.*}). MERGE por chave: as chaves enviadas sobrescrevem/criam; as demais são preservadas (envie valor null/\"\" para limpar uma chave).","example":{"plano":"premium","cidade":"São Paulo"}}},"required":["variables"],"id":"CustomerVariables","description":"Variáveis (contexto) do cliente. Apenas a coluna `variables` (mapa string→valor) é editável; o restante do cliente não é exposto na API pública. Atualização por MERGE de chaves."},"CustomerSecretVariables":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{},"description":"Mapa chave→valor das variáveis SECRETAS do cliente. WRITE-ONLY: na leitura os valores são mascarados (`••••`) — só as chaves aparecem. MERGE por chave no PATCH: as chaves enviadas sobrescrevem/criam; as demais são preservadas (valor null/\"\" remove a chave).","example":{"senha_portal":"s3nh4","documento_sigiloso":"XYZ"}}},"required":["variables"],"id":"CustomerSecretVariables","description":"Variáveis SECRETAS (contexto) do cliente. WRITE-ONLY na API: o GET devolve valores mascarados (`••••`), o PATCH grava por MERGE de chaves. Nunca ecoa o valor real."},"WhatsAppTemplate":{"type":"object","properties":{"channelId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"ID do canal (Master) WhatsApp Oficial. Deve pertencer à conta.","example":24,"ref":"Channel","refEndpoint":"GET /channels"},"name":{"type":"string","minLength":1,"description":"Nome/slug do template (minúsculas, a-z 0-9 _).","example":"aviso_vencimento_fatura"},"language":{"type":"string","description":"Código de idioma da Meta.","example":"pt_BR"},"category":{"type":"string","enum":["MARKETING","UTILITY","AUTHENTICATION"],"description":"Categoria HSM. Aviso de fatura = UTILITY.","example":"UTILITY"},"header":{"description":"Header opcional. Header de MÍDIA exige `handle` já resolvido (upload prévio).","type":"object","properties":{"format":{"type":"string","enum":["TEXT","IMAGE","VIDEO","DOCUMENT"],"description":"Formato do header."},"text":{"description":"Texto (só format=TEXT).","type":"string"},"handle":{"description":"Handle de mídia já carregado na Meta (só header de mídia).","type":"string"}},"required":["format"]},"bodyText":{"type":"string","minLength":1,"description":"Corpo do template. Variáveis {{1}},{{2}}… preenchidas no envio.","example":"Olá {{1}}, sua fatura de {{2}} vence em {{3}}."},"footer":{"description":"Rodapé opcional (texto estático).","example":"Financeiro","type":"string"},"buttons":{"description":"Botões/CTA (opcional). Estáticos viajam no template aprovado; botão de URL dinâmica ({{1}}) recebe o parâmetro por-destinatário no envio da campanha (campo `templateButtons` da campanha).","type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["QUICK_REPLY","URL","PHONE_NUMBER"],"description":"Tipo do botão.","example":"URL"},"text":{"type":"string","description":"Rótulo do botão.","example":"Ver 2ª via"},"url":{"description":"URL (só type=URL). URL DINÂMICA termina com {{1}} (um valor por destinatário no envio); exige `example`.","example":"https://portal.com/fatura/{{1}}","type":"string"},"example":{"description":"Exemplo(s) da URL dinâmica (obrigatório se a url tem {{1}}).","example":["https://portal.com/fatura/123"],"type":"array","items":{"type":"string"}},"phone_number":{"description":"Telefone internacional (só type=PHONE_NUMBER).","example":"+5581999998888","type":"string"}},"required":["type","text"],"description":"Botão/CTA do template. Regras Meta: ≤10 total, URL≤2, telefone≤1; não misturar Resposta Rápida (QUICK_REPLY) com URL/Telefone."}},"exampleVariables":{"description":"Um exemplo por variável {{N}} do corpo (a Meta exige na aprovação).","example":["Maria","R$ 129,90","15/07/2026"],"type":"array","items":{"type":"string"}}},"required":["channelId","name","language","category","bodyText"],"id":"WhatsAppTemplate","description":"Template HSM do WhatsApp Oficial (submetido à Meta para aprovação). Escopo: channels:write. Header de mídia via API pública exige handle pré-carregado."},"Campaign":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome da campanha.","example":"Promo Junho"},"channelId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"ID do canal (Master) de envio. Deve pertencer à conta.","example":12,"ref":"Channel","refEndpoint":"GET /channels"},"messageType":{"description":"Tipo da mensagem (default TEXT).","example":"TEXT","type":"string","enum":["TEXT","IMAGE","VIDEO","DOCUMENT","AUDIO"]},"messageTemplate":{"description":"Corpo de TEXTO LIVRE (canal WhatsApp via QR Code ou janela 24h do Oficial). Vars 2 níveis: ${contact.nome}/${chave} (por destinatário) e {{n}} posicional. ⚠️ NÃO é usado pelo corpo do template HSM Oficial — para o Oficial, as variáveis {{1}},{{2}}… vêm de `templateParams` (veja abaixo).","type":"string"},"templateId":{"description":"WhatsApp Oficial: template HSM aprovado (⊆ conta; obrigatório no launch do canal OFFICIAL). Se o template tem variáveis no corpo ({{1}}…), informe também `templateParams` (uma expressão por posição) — senão a Meta rejeita com (#132000).","ref":"WhatsAppTemplate","refEndpoint":"GET /whatsapp-templates","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"templateParams":{"description":"WhatsApp Oficial (template HSM): expressões ORDENADAS para as variáveis do CORPO — índice 0 = {{1}}, 1 = {{2}}, … (uma por posição, na quantidade exata do template). Cada expressão é renderizada POR DESTINATÁRIO com os `vars`/`${contact.*}`. Ex.: template com {{1}}=nome → `[\"${nome}\"]` (com recipient vars {\"nome\":\"...\"}) ou `[\"${1}\"]` (com vars {\"1\":\"...\"}). Ausente/vazio quando o template tem variáveis → corpo sem parâmetros → Meta (#132000). O `messageTemplate` NÃO substitui isto no Oficial.","example":["${nome}"],"type":"array","items":{"type":"string"}},"templateButtons":{"description":"WhatsApp Oficial (template HSM com botão de URL DINÂMICA): parâmetro por-botão. Só necessário se o template tem botão URL com {{1}}. Botões estáticos (quick_reply/URL fixa/telefone) NÃO precisam disto — viajam no template aprovado.","type":"array","items":{"type":"object","properties":{"index":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Posição do botão DINÂMICO dentro do componente BUTTONS do template (0-based).","example":0},"subType":{"type":"string","enum":["url"],"description":"Tipo do botão dinâmico. No v1, só `url` (URL com {{1}} no fim).","example":"url"},"value":{"type":"string","description":"Expressão renderizada POR DESTINATÁRIO que preenche o {{1}} da URL do botão (só a porção dinâmica, ex.: o id — a Meta concatena com a base do template). ⚠️ Este {{1}} é PRÓPRIO do botão (escopo do component `button`), independente do {{1}} do CORPO (`templateParams`) — a Meta os entrega em parâmetros separados.","example":"${fatura_id}"}},"required":["index","subType","value"]}},"replyFlowId":{"description":"Fluxo disparado quando o destinatário responde (⊆ conta).","ref":"Flow","refEndpoint":"GET /flows","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"media":{"type":"object","properties":{"mediaAssetId":{"description":"Referência a um MediaAsset já existente (dedup por conteúdo). Alternativa ao `base64` — se você não tem o id, envie `base64`+`mimetype` que o asset é criado/reusado automaticamente.","ref":"MediaAsset","example":123,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"base64":{"description":"Mídia inline em base64 (alternativa ao mediaAssetId).","type":"string"},"mimetype":{"description":"MIME da mídia base64.","example":"image/png","type":"string"},"filename":{"description":"Nome do arquivo (documentos).","type":"string"}},"description":"Mídia da campanha (referência OU base64)."},"recipients":{"description":"Destinatários iniciais (também via POST /campaigns/:id/recipients). Dedupe + opt-out aplicados.","type":"array","items":{"type":"object","properties":{"number":{"type":"string","description":"Número BR (normalizado server-side).","example":"5511999998888"},"name":{"description":"Nome do destinatário (p/ ${contact.nome} no template).","type":"string"},"vars":{"description":"Variáveis por destinatário p/ o template (${chave}).","type":"object","additionalProperties":{"type":"string"}}},"required":["number"],"description":"Destinatário da campanha."}},"tagIds":{"description":"IDs de tags (escopo CUSTOMER ou CONTACT) — adiciona todos os contatos que as possuem (ContactTag direto + CustomerTag → contatos atuais do cliente). Dedupe + opt-out aplicados.","example":[12,34],"ref":"Tag","refEndpoint":"GET /tags","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagMatchMode":{"description":"any = contato com QUALQUER uma das tags (união); all = com TODAS (interseção). Default any.","example":"any","type":"string","enum":["any","all"]},"scheduledStartAt":{"description":"Agendamento wall-clock GMT-3 (\"2026-12-31T09:00\") ou ISO com tz. Ausente = manual via launch.","type":"string"}},"required":["name","channelId"],"id":"Campaign","description":"Campanha de envio (criada DRAFT; dispatcher só-PRIMARY dispara no launch). HSM obrigatório no canal Oficial. `recipients`/`tagIds` opcionais adicionam destinatários iniciais (união)."},"AddRecipients":{"type":"object","properties":{"recipients":{"description":"Destinatários explícitos. Dedupe + opt-out aplicados.","type":"array","items":{"type":"object","properties":{"number":{"type":"string","description":"Número BR (normalizado server-side).","example":"5511999998888"},"name":{"description":"Nome do destinatário (p/ ${contact.nome} no template).","type":"string"},"vars":{"description":"Variáveis por destinatário p/ o template (${chave}).","type":"object","additionalProperties":{"type":"string"}}},"required":["number"],"description":"Destinatário da campanha."}},"tagIds":{"description":"IDs de tags (escopo CUSTOMER ou CONTACT) — adiciona todos os contatos que as possuem (ContactTag direto + CustomerTag → contatos atuais do cliente). Dedupe + opt-out aplicados.","example":[12,34],"ref":"Tag","refEndpoint":"GET /tags","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"tagMatchMode":{"description":"any = contato com QUALQUER uma das tags (união); all = com TODAS (interseção). Default any.","example":"any","type":"string","enum":["any","all"]}},"id":"AddRecipients","description":"Adição de destinatários a uma campanha: linhas explícitas E/OU seleção por tag (união). Informe ao menos `recipients` ou `tagIds`."},"Automation":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome da automação.","example":"Cobrança diária de faturas"},"description":{"description":"Descrição livre (opcional).","example":"Dispara o fluxo de cobrança todo dia útil às 9h.","type":"string"},"flowId":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"ID do fluxo (headless) a executar. Deve pertencer à conta.","example":42,"ref":"Flow","refEndpoint":"GET /flows"},"channelId":{"description":"Canal de contexto (opcional). Os envios reais do fluxo resolvem o canal nas funções do nó Código. Deve pertencer à conta.","example":12,"ref":"Channel","refEndpoint":"GET /channels","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"cronExpression":{"type":"string","minLength":1,"description":"Expressão CRON (5 campos) avaliada no relógio de Brasília (GMT-3). Ex.: \"0 9 * * 1-5\" = 9h nos dias úteis. Inválida → 400.","example":"0 9 * * 1-5"},"enabled":{"description":"Liga/desliga o agendamento. Default true. Quando true + cron válido, o `nextRunAt` é pré-computado server-side.","example":true,"type":"boolean"},"concurrency":{"description":"SKIP (default) = não inicia uma nova execução se já houver uma em andamento; ALLOW = permite sobreposição.","example":"SKIP","type":"string","enum":["SKIP","ALLOW"]}},"required":["name","flowId","cronExpression"],"id":"Automation","description":"Automação agendada (cron) que executa um fluxo HEADLESS via ticket-sistema sintético. O disparo real roda no servidor PRIMARY (dispatcher). O disparo manual é POST /automations/:id/run."},"AIRule":{"type":"object","properties":{"id":{"description":"ID p/ update (ausente = create).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"type":"string","minLength":1,"description":"Nome da regra.","example":"Tom formal"},"content":{"type":"string","description":"Texto da regra injetado no system prompt do agente. SEMPRE presente no prompt enquanto a regra estiver ativa (always-on) — não depende de o agente \"decidir\" usá-la (≠ AITool).","example":"Responda sempre de forma formal."},"priority":{"description":"Ordem no system prompt (menor = primeiro; default 200).","example":200,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"flowIds":{"description":"Disponibilidade por fluxo (picker do FlowBuilder).","ref":"Flow","refEndpoint":"GET /flows","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"categoryIds":{"description":"Categoria(s) compartilhada(s).","ref":"AICategory","refEndpoint":"GET /ai/categories","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"active":{"description":"Ativa? NO-DELETE: false desativa, true reativa.","example":true,"type":"boolean"}},"required":["name","content"],"id":"AIRule","description":"Regra de IA anexada ao nó react_agent. ALWAYS-ON: o `content` é sempre injetado no system prompt (ordenado por `priority`) enquanto ativa — molda o comportamento do agente em toda resposta. Para capacidades acionadas sob demanda, use AITool."},"AITool":{"type":"object","properties":{"id":{"description":"ID p/ update (ausente = create).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"type":"string","minLength":1,"description":"Nome da ferramenta (function calling).","example":"consultar_cep"},"description":{"description":"Descrição p/ o LLM saber QUANDO usar. A tool é acionada SOB DEMANDA (lazy) — só executa quando o agente decide chamá-la durante o raciocínio; descreva bem o gatilho de uso.","type":"string"},"parameters":{"description":"JSON Schema dos parâmetros que o LLM preenche ao chamar a tool.","example":{"type":"object","properties":{"cep":{"type":"string","description":"CEP a consultar"}},"required":["cep"]}},"code":{"description":"Código JS executado quando a tool é chamada (sandbox vm). Usado quando logicType=SCRIPT.","example":"return { ok: true, cep: args.cep };","type":"string"},"logicType":{"description":"Como a tool executa: `SCRIPT` roda o `code`; `TEXT` devolve uma mensagem fixa (em logicData); `REDIRECT` desvia o fluxo p/ um nó (nodeId em logicData).","example":"SCRIPT","type":"string"},"logicData":{"description":"Config conforme logicType — TEXT: `{\"text\":\"mensagem\"}`; REDIRECT: `{\"nodeId\":\"n5\"}`; SCRIPT: ignorado (a lógica está em `code`). Aceita objeto ou string JSON.","example":{"text":"Um momento…"}},"nextToolPolicy":{"description":"Visibilidade das outras tools na rodada ReAct seguinte após esta executar: `1`=todas continuam visíveis; `2`/`3`=restringe (ver painel). Default `1`.","example":"1","type":"string"},"blockAbortOnNewMessage":{"description":"Se true, quando esta tool já executou no request do react_agent, uma nova mensagem do cliente NÃO aborta/reunifica o request (protege tools com efeito colateral). Default false.","type":"boolean"},"extraAttributes":{"description":"Atributos extras específicos da tool (Json livre, avançado).","example":{}},"flowIds":{"description":"Disponibilidade por fluxo (picker do FlowBuilder).","ref":"Flow","refEndpoint":"GET /flows","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"categoryIds":{"description":"Categoria(s) compartilhada(s).","ref":"AICategory","refEndpoint":"GET /ai/categories","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"active":{"description":"Ativa? NO-DELETE: false desativa, true reativa.","example":true,"type":"boolean"}},"required":["name"],"id":"AITool","description":"Ferramenta (function calling) do nó react_agent — a \"skill\" do agente. LAZY/SOB DEMANDA: ao contrário da AIRule (sempre no prompt), a tool só é executada quando o agente decide chamá-la durante o raciocínio (ReAct), conforme a `description`. Use AIRule para comportamento sempre-ativo; AITool para ações pontuais (consultar API, calcular, etc.)."},"AIProfile":{"type":"object","properties":{"id":{"description":"ID p/ update (ausente = create).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"type":"string","minLength":1,"description":"Nome do perfil de inferência.","example":"Criativo"},"temperature":{"description":"Aleatoriedade da geração (0-2). Requer useTemperature=true.","type":"number"},"topP":{"description":"Nucleus sampling (0-1). Requer useTopP=true.","type":"number"},"topK":{"description":"Top-K sampling. Requer useTopK=true.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"minP":{"description":"Probabilidade mínima relativa. Requer useMinP=true.","type":"number"},"repetitionPenalty":{"description":"Penalidade de repetição. Requer useRepetitionPenalty=true.","type":"number"},"maxTokens":{"description":"Máx. de tokens da resposta. Requer useMaxTokens=true.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"presencePenalty":{"description":"Penalidade de presença. Requer usePresencePenalty=true.","type":"number"},"frequencyPenalty":{"description":"Penalidade de frequência. Requer useFrequencyPenalty=true.","type":"number"},"useTemperature":{"description":"Ativa o envio de temperature.","type":"boolean"},"useTopP":{"description":"Ativa o envio de topP.","type":"boolean"},"useTopK":{"description":"Ativa o envio de topK.","type":"boolean"},"useMinP":{"description":"Ativa o envio de minP.","type":"boolean"},"useRepetitionPenalty":{"description":"Ativa o envio de repetitionPenalty.","type":"boolean"},"useMaxTokens":{"description":"Ativa o envio de maxTokens.","type":"boolean"},"usePresencePenalty":{"description":"Ativa o envio de presencePenalty.","type":"boolean"},"useFrequencyPenalty":{"description":"Ativa o envio de frequencyPenalty.","type":"boolean"},"extraConfigs":{"description":"Parâmetros extras do provedor."},"priority":{"description":"Ordem de aplicação no merge de profiles do modelo (asc; menor aplicado primeiro, MAIOR vence em conflito). Default 200.","example":200,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"includeReasoning":{"description":"Controle do raciocínio (<thought>): true=liga, false=força-desliga, null/ausente=NÃO opina (não participa do merge). Entre os profiles que opinam, vence o de maior prioridade.","anyOf":[{"type":"boolean"},{"type":"null"}]},"reasoningLimit":{"description":"Limite de caracteres do raciocínio (0 = sem limite). null = não opina.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"emitContentWithToolCall":{"description":"Quando o agente escreve um texto E chama uma ferramenta no MESMO passo: true=entrega esse texto ao cliente ANTES de executar a ferramenta; false=descarta (comportamento padrão); null/ausente=NÃO opina (default do sistema = não envia). Resolvido pelo mesmo merge por prioridade dos demais profiles.","example":true,"anyOf":[{"type":"boolean"},{"type":"null"}]},"functions":{"description":"Função/papel do perfil (eixo ORTOGONAL aos parâmetros): atendimento, classificador, juiz. Filtro de organização/UI — o runtime é escopado pelo vínculo do perfil ao modelo. Array vazio/ausente = \"qualquer função\".","example":["atendimento"],"type":"array","items":{"type":"string","enum":["atendimento","classificador","juiz"]}},"active":{"description":"Ativo? NO-DELETE: false desativa, true reativa (nunca há exclusão de perfil de IA).","example":true,"type":"boolean"}},"required":["name"],"id":"AIProfile","description":"Perfil de inferência (sampling) do react_agent. use<Param>=true ativa o respectivo parâmetro. Aplicado ao modelo via vínculo (overridable/optional); ordem por priority."},"AIModel":{"type":"object","properties":{"id":{"description":"ID p/ update (ausente = create).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"type":"string","minLength":1,"description":"Nome amigável do modelo.","example":"GPT-4o (conta)"},"baseUrl":{"type":"string","description":"Host base OpenAI-compatible (só host; o path é montado por capability).","example":"https://api.openai.com"},"model":{"description":"Nome do modelo no provedor (vazio = router escolhe).","example":"gpt-4o","type":"string"},"apiKey":{"description":"Chave do provedor — WRITE-ONLY (nunca retornada; resposta traz só apiKeySet).","type":"string"},"defaultHeaders":{"description":"Headers default enviados ao provedor."},"capabilities":{"description":"Array de capabilities suportadas pelo modelo: `chat`, `transcript` (STT), `tts`, `vision` (visão/imagem no react_agent).","example":["chat","vision"]},"chatUrl":{"description":"Override PATH-ONLY do endpoint de chat.","type":"string"},"transcriptUrl":{"description":"Override PATH-ONLY de transcrição (STT).","type":"string"},"ttsUrl":{"description":"Override PATH-ONLY de TTS.","type":"string"},"functions":{"description":"Função/papel do modelo (eixo ORTOGONAL a capabilities): atendimento, classificador, juiz. Refina modelos de chat por papel — os selects de cada contexto (react_agent/classificador/juiz) filtram por função. Array vazio/ausente = \"qualquer função\" (comportamento padrão, aparece em todos os selects).","example":["atendimento"],"type":"array","items":{"type":"string","enum":["atendimento","classificador","juiz"]}},"active":{"description":"Ativo? NO-DELETE: false desativa, true reativa (modelo referenciado por nós nunca é excluído).","example":true,"type":"boolean"}},"required":["name","baseUrl"],"id":"AIModel","description":"Modelo/endpoint LLM da conta (scope ACCOUNT forçado; apiKey write-only)."},"AICategory":{"type":"object","properties":{"id":{"description":"ID p/ update (ausente = create).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"name":{"type":"string","minLength":1,"description":"Nome da categoria de IA.","example":"Vendas"},"active":{"description":"Ativo? NO-DELETE: false desativa, true reativa.","example":true,"type":"boolean"}},"required":["name"],"id":"AICategory","description":"Categoria compartilhada de rules/tools/profiles."},"AIRulePatch":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome da regra.","example":"Tom formal"},"content":{"type":"string","description":"Texto da regra injetado no system prompt do agente. SEMPRE presente no prompt enquanto a regra estiver ativa (always-on) — não depende de o agente \"decidir\" usá-la (≠ AITool).","example":"Responda sempre de forma formal."},"priority":{"description":"Ordem no system prompt (menor = primeiro; default 200).","example":200,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"flowIds":{"description":"Disponibilidade por fluxo (picker do FlowBuilder).","ref":"Flow","refEndpoint":"GET /flows","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"categoryIds":{"description":"Categoria(s) compartilhada(s).","ref":"AICategory","refEndpoint":"GET /ai/categories","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"active":{"description":"Ativa? NO-DELETE: false desativa, true reativa.","example":true,"type":"boolean"},"replace":{"description":"FULL-REPLACE se `true` (campos OMITIDOS são resetados/apagados — igual ao upsert/POST). Default `false` = MERGE parcial: só os campos enviados mudam, os omitidos ficam INTOCADOS (sem clobber). Use o merge p/ editar 1 campo sem reenviar o resto (economia de token).","example":false,"type":"boolean"},"contentEdits":{"description":"Edições incrementais por string no campo-alvo (economia de token — não reenvia o campo inteiro). Aplicadas EM ORDEM e de forma ATÔMICA: se QUALQUER uma falhar, NADA é salvo e a resposta traz `reason` + `index` (posição 0-based do edit culpado). Como reagir a cada `reason`: `NO_MATCH` = o `find`/`findRegex` não existe no texto (releia com get_* e ajuste); `NOT_UNIQUE` = o `find` aparece +1× → inclua mais contexto p/ torná-lo único, ou `replaceAll:true`; `TOO_MANY_MATCHES` = o regex casou mais que `maxMatches` → refine o padrão ou aumente o teto; `MAX_MATCHES_REQUIRED` = faltou `maxMatches` no modo regex; `REGEX_UNSAFE`/`REGEX_TIMEOUT` = padrão perigoso/lento (ReDoS) — simplifique. Mutuamente exclusivo com enviar o campo inteiro (content/code/description) → 400. Máx. 100 edições por lote.","minItems":1,"maxItems":100,"type":"array","items":{"anyOf":[{"type":"object","properties":{"find":{"type":"string","minLength":1,"maxLength":65536,"description":"Trecho LITERAL exato a procurar (sem regex). Deve ser ÚNICO no texto, salvo replaceAll:true.","example":"Responda sempre de forma formal."},"replace":{"type":"string","maxLength":65536,"description":"Texto que substitui o trecho (literal — `$` não é interpretado).","example":"Responda de forma cordial e objetiva."},"replaceAll":{"description":"true troca TODAS as ocorrências. Default false = exige que o trecho seja único (senão erro NOT_UNIQUE).","example":false,"type":"boolean"}},"required":["find","replace"]},{"type":"object","properties":{"findRegex":{"type":"string","minLength":1,"maxLength":1000,"description":"Padrão REGEX (JS). Sempre global (todas as ocorrências, limitado por maxMatches). Padrões com quantificador aninhado (X+)+ são rejeitados (ReDoS).","example":"nf_(\\w+)"},"flags":{"description":"Flags do regex (subconjunto de g i m s u y; `g` é sempre forçado).","example":"g","type":"string","maxLength":8},"replace":{"type":"string","maxLength":65536,"description":"Substituição — aceita backrefs ($1, $&).","example":"nf2_$1"},"maxMatches":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"OBRIGATÓRIO (dry-run gate): se o padrão casar MAIS que isto, o edit falha (TOO_MANY_MATCHES) e nada é salvo.","example":20}},"required":["findRegex","replace","maxMatches"]}],"description":"Uma edição por string: literal {find,replace,replaceAll?} OU regex {findRegex,flags?,replace,maxMatches}."}},"targetField":{"description":"Campo string a editar. Rule: default \"content\". Tool: \"code\" (default) ou \"description\". Deve ser um campo de texto livre e mutável (name é imutável).","example":"content","type":"string"},"baseUpdatedAt":{"description":"Guarda de lost-update (opcional): se ≠ updatedAt atual da entidade → 409 (outro editou no meio). Releia (GET) e reaplique.","example":"2026-07-27T03:51:34.611Z","type":"string"}},"id":"AIRulePatch","description":"PATCH parcial de AIRule (merge — só os campos enviados). `?view=summary` omite `content`. Suporta edição incremental por string via `contentEdits` (economia de token; alvo default `content`)."},"AIToolPatch":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome da ferramenta (function calling).","example":"consultar_cep"},"description":{"description":"Descrição p/ o LLM saber QUANDO usar. A tool é acionada SOB DEMANDA (lazy) — só executa quando o agente decide chamá-la durante o raciocínio; descreva bem o gatilho de uso.","type":"string"},"parameters":{"description":"JSON Schema dos parâmetros que o LLM preenche ao chamar a tool.","example":{"type":"object","properties":{"cep":{"type":"string","description":"CEP a consultar"}},"required":["cep"]}},"code":{"description":"Código JS executado quando a tool é chamada (sandbox vm). Usado quando logicType=SCRIPT.","example":"return { ok: true, cep: args.cep };","type":"string"},"logicType":{"description":"Como a tool executa: `SCRIPT` roda o `code`; `TEXT` devolve uma mensagem fixa (em logicData); `REDIRECT` desvia o fluxo p/ um nó (nodeId em logicData).","example":"SCRIPT","type":"string"},"logicData":{"description":"Config conforme logicType — TEXT: `{\"text\":\"mensagem\"}`; REDIRECT: `{\"nodeId\":\"n5\"}`; SCRIPT: ignorado (a lógica está em `code`). Aceita objeto ou string JSON.","example":{"text":"Um momento…"}},"nextToolPolicy":{"description":"Visibilidade das outras tools na rodada ReAct seguinte após esta executar: `1`=todas continuam visíveis; `2`/`3`=restringe (ver painel). Default `1`.","example":"1","type":"string"},"blockAbortOnNewMessage":{"description":"Se true, quando esta tool já executou no request do react_agent, uma nova mensagem do cliente NÃO aborta/reunifica o request (protege tools com efeito colateral). Default false.","type":"boolean"},"extraAttributes":{"description":"Atributos extras específicos da tool (Json livre, avançado).","example":{}},"flowIds":{"description":"Disponibilidade por fluxo (picker do FlowBuilder).","ref":"Flow","refEndpoint":"GET /flows","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"categoryIds":{"description":"Categoria(s) compartilhada(s).","ref":"AICategory","refEndpoint":"GET /ai/categories","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"active":{"description":"Ativa? NO-DELETE: false desativa, true reativa.","example":true,"type":"boolean"},"replace":{"description":"FULL-REPLACE se `true` (campos OMITIDOS são resetados/apagados — igual ao upsert/POST). Default `false` = MERGE parcial: só os campos enviados mudam, os omitidos ficam INTOCADOS (sem clobber). Use o merge p/ editar 1 campo sem reenviar o resto (economia de token).","example":false,"type":"boolean"},"contentEdits":{"description":"Edições incrementais por string no campo-alvo (economia de token — não reenvia o campo inteiro). Aplicadas EM ORDEM e de forma ATÔMICA: se QUALQUER uma falhar, NADA é salvo e a resposta traz `reason` + `index` (posição 0-based do edit culpado). Como reagir a cada `reason`: `NO_MATCH` = o `find`/`findRegex` não existe no texto (releia com get_* e ajuste); `NOT_UNIQUE` = o `find` aparece +1× → inclua mais contexto p/ torná-lo único, ou `replaceAll:true`; `TOO_MANY_MATCHES` = o regex casou mais que `maxMatches` → refine o padrão ou aumente o teto; `MAX_MATCHES_REQUIRED` = faltou `maxMatches` no modo regex; `REGEX_UNSAFE`/`REGEX_TIMEOUT` = padrão perigoso/lento (ReDoS) — simplifique. Mutuamente exclusivo com enviar o campo inteiro (content/code/description) → 400. Máx. 100 edições por lote.","minItems":1,"maxItems":100,"type":"array","items":{"anyOf":[{"type":"object","properties":{"find":{"type":"string","minLength":1,"maxLength":65536,"description":"Trecho LITERAL exato a procurar (sem regex). Deve ser ÚNICO no texto, salvo replaceAll:true.","example":"Responda sempre de forma formal."},"replace":{"type":"string","maxLength":65536,"description":"Texto que substitui o trecho (literal — `$` não é interpretado).","example":"Responda de forma cordial e objetiva."},"replaceAll":{"description":"true troca TODAS as ocorrências. Default false = exige que o trecho seja único (senão erro NOT_UNIQUE).","example":false,"type":"boolean"}},"required":["find","replace"]},{"type":"object","properties":{"findRegex":{"type":"string","minLength":1,"maxLength":1000,"description":"Padrão REGEX (JS). Sempre global (todas as ocorrências, limitado por maxMatches). Padrões com quantificador aninhado (X+)+ são rejeitados (ReDoS).","example":"nf_(\\w+)"},"flags":{"description":"Flags do regex (subconjunto de g i m s u y; `g` é sempre forçado).","example":"g","type":"string","maxLength":8},"replace":{"type":"string","maxLength":65536,"description":"Substituição — aceita backrefs ($1, $&).","example":"nf2_$1"},"maxMatches":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"OBRIGATÓRIO (dry-run gate): se o padrão casar MAIS que isto, o edit falha (TOO_MANY_MATCHES) e nada é salvo.","example":20}},"required":["findRegex","replace","maxMatches"]}],"description":"Uma edição por string: literal {find,replace,replaceAll?} OU regex {findRegex,flags?,replace,maxMatches}."}},"targetField":{"description":"Campo string a editar. Rule: default \"content\". Tool: \"code\" (default) ou \"description\". Deve ser um campo de texto livre e mutável (name é imutável).","example":"content","type":"string"},"baseUpdatedAt":{"description":"Guarda de lost-update (opcional): se ≠ updatedAt atual da entidade → 409 (outro editou no meio). Releia (GET) e reaplique.","example":"2026-07-27T03:51:34.611Z","type":"string"}},"id":"AIToolPatch","description":"PATCH parcial de AITool (merge). ⚠️ `name` é IMUTÁVEL após a criação. `?view=summary` omite `code`/`parameters`. Suporta edição incremental por string via `contentEdits` (alvo `code` default ou `description`)."},"AIProfilePatch":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome do perfil de inferência.","example":"Criativo"},"temperature":{"description":"Aleatoriedade da geração (0-2). Requer useTemperature=true.","type":"number"},"topP":{"description":"Nucleus sampling (0-1). Requer useTopP=true.","type":"number"},"topK":{"description":"Top-K sampling. Requer useTopK=true.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"minP":{"description":"Probabilidade mínima relativa. Requer useMinP=true.","type":"number"},"repetitionPenalty":{"description":"Penalidade de repetição. Requer useRepetitionPenalty=true.","type":"number"},"maxTokens":{"description":"Máx. de tokens da resposta. Requer useMaxTokens=true.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"presencePenalty":{"description":"Penalidade de presença. Requer usePresencePenalty=true.","type":"number"},"frequencyPenalty":{"description":"Penalidade de frequência. Requer useFrequencyPenalty=true.","type":"number"},"useTemperature":{"description":"Ativa o envio de temperature.","type":"boolean"},"useTopP":{"description":"Ativa o envio de topP.","type":"boolean"},"useTopK":{"description":"Ativa o envio de topK.","type":"boolean"},"useMinP":{"description":"Ativa o envio de minP.","type":"boolean"},"useRepetitionPenalty":{"description":"Ativa o envio de repetitionPenalty.","type":"boolean"},"useMaxTokens":{"description":"Ativa o envio de maxTokens.","type":"boolean"},"usePresencePenalty":{"description":"Ativa o envio de presencePenalty.","type":"boolean"},"useFrequencyPenalty":{"description":"Ativa o envio de frequencyPenalty.","type":"boolean"},"extraConfigs":{"description":"Parâmetros extras do provedor."},"priority":{"description":"Ordem de aplicação no merge de profiles do modelo (asc; menor aplicado primeiro, MAIOR vence em conflito). Default 200.","example":200,"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"includeReasoning":{"description":"Controle do raciocínio (<thought>): true=liga, false=força-desliga, null/ausente=NÃO opina (não participa do merge). Entre os profiles que opinam, vence o de maior prioridade.","anyOf":[{"type":"boolean"},{"type":"null"}]},"reasoningLimit":{"description":"Limite de caracteres do raciocínio (0 = sem limite). null = não opina.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"emitContentWithToolCall":{"description":"Quando o agente escreve um texto E chama uma ferramenta no MESMO passo: true=entrega esse texto ao cliente ANTES de executar a ferramenta; false=descarta (comportamento padrão); null/ausente=NÃO opina (default do sistema = não envia). Resolvido pelo mesmo merge por prioridade dos demais profiles.","example":true,"anyOf":[{"type":"boolean"},{"type":"null"}]},"functions":{"description":"Função/papel do perfil (eixo ORTOGONAL aos parâmetros): atendimento, classificador, juiz. Filtro de organização/UI — o runtime é escopado pelo vínculo do perfil ao modelo. Array vazio/ausente = \"qualquer função\".","example":["atendimento"],"type":"array","items":{"type":"string","enum":["atendimento","classificador","juiz"]}},"active":{"description":"Ativo? NO-DELETE: false desativa, true reativa (nunca há exclusão de perfil de IA).","example":true,"type":"boolean"},"replace":{"description":"FULL-REPLACE se `true` (campos OMITIDOS são resetados/apagados — igual ao upsert/POST). Default `false` = MERGE parcial: só os campos enviados mudam, os omitidos ficam INTOCADOS (sem clobber). Use o merge p/ editar 1 campo sem reenviar o resto (economia de token).","example":false,"type":"boolean"}},"id":"AIProfilePatch","description":"PATCH parcial de AIProfile (merge — só os campos enviados)."},"PronunciationEntry":{"type":"object","properties":{"id":{"description":"ID (cuid) p/ update (ausente = create).","example":"clx1a2b3c0000abcd","type":"string"},"fromText":{"type":"string","minLength":1,"description":"Palavra/termo de ORIGEM a corrigir (como aparece no texto). Casa por fronteira de palavra; longest-first entre regras. Ex.: um estrangeirismo ou sigla.","example":"Basic"},"toText":{"type":"string","minLength":1,"description":"Forma FALADA que substitui `fromText` só no áudio TTS. Se `toText === fromText`, funciona como passthrough (anula uma regra global só p/ o tenant). Ideal 1 palavra→1 palavra (preserva o sync do karaokê ao vivo). Guarde em minúsculo p/ o SMART-CASE agir.","example":"Bêizike"},"caseSensitive":{"description":"false (padrão) = casa IGNORANDO caixa + SMART-CASE na saída (dtel→dêtel, Dtel→Dêtel, DTEL→DÊTEL). true = casa exatamente como cadastrado e substitui literalmente.","example":false,"type":"boolean"},"aiModelId":{"description":"Escopo por voz/engine: null (padrão) = vale p/ qualquer voz TTS; setado = só quando aquele modelo de IA (capability tts) é o resolvido. Útil quando a fonética é específica de um engine.","example":null,"ref":"AIModel","refEndpoint":"GET /ai/models","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"enabled":{"description":"Regra ativa? false desliga sem excluir; a exclusão (DELETE) apenas arquiva (preserva o histórico).","example":true,"type":"boolean"}},"required":["fromText","toText"],"id":"PronunciationEntry","description":"Regra de pronúncia (respelling) do TTS: mapeia uma ORIGEM (`fromText`) para a forma FALADA (`toText`), aplicada apenas ao texto enviado ao engine de voz — o texto exibido/persistido nunca muda. Regras do tenant sobrescrevem as globais (SYSTEM) pela mesma origem. Escopo opcional por voz (`aiModelId`)."},"UserCreate":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome do usuário.","example":"Maria Silva"},"username":{"type":"string","minLength":1,"description":"Login único.","example":"maria"},"password":{"type":"string","description":"Senha forte — WRITE-ONLY (nunca retornada)."},"displayName":{"description":"Nome de exibição no chat.","anyOf":[{"type":"string"},{"type":"null"}]},"role":{"type":"string","enum":["ADMIN","AGENT"],"description":"Papel (SUPER_ADMIN proibido via API → 403).","example":"AGENT"},"profileId":{"description":"Perfil RBAC (⊆ conta). Permissões SÓ via profile (sem atribuição direta). ⚠️ Os perfis NÃO são listáveis pela API pública — obtenha o `id` do perfil no painel (Configurações → Perfis).","ref":"Profile","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxTickets":{"description":"Capacidade máxima de tickets simultâneos.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"departmentIds":{"description":"Setores vinculados (⊆ conta).","example":[],"ref":"Department","refEndpoint":"GET /departments","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}}},"required":["name","username","password"],"id":"UserCreate","description":"Criação de operador (account-scoped; senha write-only; sem SUPER_ADMIN; perms só-via-profile)."},"UserUpdate":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome do usuário.","example":"Maria Silva"},"password":{"type":"string","description":"Senha forte — WRITE-ONLY (nunca retornada)."},"displayName":{"description":"Nome de exibição no chat.","anyOf":[{"type":"string"},{"type":"null"}]},"role":{"type":"string","enum":["ADMIN","AGENT"],"description":"Papel (SUPER_ADMIN proibido via API → 403).","example":"AGENT"},"profileId":{"description":"Perfil RBAC (⊆ conta). Permissões SÓ via profile (sem atribuição direta). ⚠️ Os perfis NÃO são listáveis pela API pública — obtenha o `id` do perfil no painel (Configurações → Perfis).","ref":"Profile","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxTickets":{"description":"Capacidade máxima de tickets simultâneos.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"departmentIds":{"description":"Setores vinculados (⊆ conta).","example":[],"ref":"Department","refEndpoint":"GET /departments","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"active":{"description":"false desativa (encerra UserSession).","example":true,"type":"boolean"}},"id":"UserUpdate","description":"Atualização de operador (mesmas leis do create; username imutável)."},"FlowCreate":{"type":"object","properties":{"name":{"type":"string","minLength":1,"description":"Nome do fluxo.","example":"Atendimento Vendas"},"nodes":{"description":"Nós do fluxo (ver tipos no /schema → nodes). Na ENTRADA aceita array OU string JSON; na RESPOSTA (get/create/update) volta sempre como ARRAY.","type":"array","items":{}},"edges":{"description":"Arestas do grafo.","type":"array","items":{}},"variables":{"description":"Variáveis declaradas do fluxo.","type":"array","items":{}},"allowInvalid":{"description":"true salva mesmo inválido (rascunho); senão 422 com validation.errors.","example":false,"type":"boolean"},"classifierSystemPrompt":{"description":"System prompt do classificador de intenção do nó Opções. Vazio/null = classificador desligado (comportamento clássico só por número/rótulo).","example":"Classifique a resposta do cliente em uma das opções do menu; se nenhuma servir, responda \"nenhuma\".","type":"string"},"classifierModelId":{"description":"Modelo de IA do classificador (⊆ conta). Sem ele, usa a resolução padrão de modelo da conta.","ref":"AIModel","refEndpoint":"GET /ai/models","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"classifierProfileIds":{"description":"Perfis de parâmetros/reasoning do classificador (⊆ conta).","ref":"AIProfile","refEndpoint":"GET /ai/profiles","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"classifierTimeout":{"description":"Timeout (segundos) da chamada LLM do classificador; padrão 120.","example":120,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"judgeSystemPrompt":{"description":"System prompt do juiz de turno do react_agent. Vazio/null = usa o prompt padrão. Use {$juiz_historico} e {$juiz_resposta}; peça o JSON { enviar_mensagem, turno_encerrado, instrucao }.","example":"Avalie se a última ação do atendente está correta e se o turno acabou.","type":"string"},"judgeModelId":{"description":"Modelo de IA do juiz de turno (⊆ conta). Sem ele, o juiz não roda (fail-safe: entrega a fala).","ref":"AIModel","refEndpoint":"GET /ai/models","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"judgeProfileIds":{"description":"Perfis de parâmetros/reasoning do juiz (⊆ conta).","ref":"AIProfile","refEndpoint":"GET /ai/profiles","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"judgeTimeout":{"description":"Timeout (segundos) da chamada LLM do juiz; padrão 120.","example":120,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"visionSystemPrompt":{"description":"System prompt (persona) do LLM de visão. Vazio/null = usa o prompt padrão. Analisa a imagem e responde à solicitação/pergunta.","example":"Você analisa imagens de clientes e responde de forma detalhada e fiel ao que aparece.","type":"string"},"visionModelId":{"description":"Modelo de IA de visão (⊆ conta; deve ter capability \"vision\"). Sem ele, cai no FALLBACK por capability: usa qualquer modelo acessível com \"vision\" (igual TTS/STT). Só desliga se não houver nenhum.","ref":"AIModel","refEndpoint":"GET /ai/models","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"visionProfileIds":{"description":"Perfis de parâmetros/reasoning do LLM de visão (⊆ conta).","ref":"AIProfile","refEndpoint":"GET /ai/profiles","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"visionTimeout":{"description":"Timeout (segundos) da chamada LLM de visão; padrão 120.","example":120,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"categoryId":{"description":"Categoria (⊆ conta).","ref":"FlowCategory","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"parentFlowId":{"description":"Fluxo pai (versionamento; ⊆ conta).","ref":"Flow","refEndpoint":"GET /flows","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"updateChannelsFromId":{"description":"Clona/versiona a partir deste fluxo e migra os canais (⊆ conta).","ref":"Flow","refEndpoint":"GET /flows","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"cloneAllChildren":{"description":"Clona recursivamente os filhos.","type":"boolean"},"isDefault":{"description":"Fluxo padrão da conta.","type":"boolean"},"defaultExitMsg":{"description":"Mensagem padrão de saída.","type":"string"},"inactivityTimeout":{"description":"Timeout de inatividade (s).","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"inactivityNextNodeId":{"description":"Nó destino no timeout de inatividade.","type":"string"},"edgeType":{"description":"Tipo visual de aresta.","type":"string"},"useFloatingEdges":{"description":"Usa arestas flutuantes no editor.","type":"boolean"},"defaultTtsModelId":{"description":"Modelo TTS padrão (⊆ conta).","ref":"AIModel","refEndpoint":"GET /ai/models","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"defaultSttModelId":{"description":"Modelo STT padrão (⊆ conta).","ref":"AIModel","refEndpoint":"GET /ai/models","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"defaultTtsProfileIds":{"description":"Perfis TTS padrão.","ref":"AIProfile","refEndpoint":"GET /ai/profiles","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"defaultSttProfileIds":{"description":"Perfis STT padrão.","ref":"AIProfile","refEndpoint":"GET /ai/profiles","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"ttsDefaultEnabled":{"description":"TTS ligado por padrão no fluxo.","type":"boolean"},"ttsAutoAskPreference":{"description":"Pergunta a preferência de áudio ao cliente.","type":"boolean"},"ttsOutputMode":{"type":"string","enum":["audio_only","audio_and_text"],"description":"Modo de saída TTS.","example":"audio_and_text"}},"required":["name"],"id":"FlowCreate","description":"Criação de fluxo (paridade total com o painel). Gate de validação: inválido → 422 (salvo allowInvalid)."},"FlowUpdate":{"type":"object","properties":{"name":{"type":"string","minLength":1},"nodes":{"description":"Nós do fluxo (ver tipos no /schema → nodes). Na ENTRADA aceita array OU string JSON; na RESPOSTA (get/create/update) volta sempre como ARRAY.","type":"array","items":{}},"edges":{"description":"Arestas do grafo.","type":"array","items":{}},"variables":{"description":"Variáveis declaradas do fluxo.","type":"array","items":{}},"allowInvalid":{"description":"true salva mesmo inválido (rascunho); senão 422 com validation.errors.","example":false,"type":"boolean"},"classifierSystemPrompt":{"description":"System prompt do classificador de intenção do nó Opções. Vazio/null = classificador desligado (comportamento clássico só por número/rótulo).","example":"Classifique a resposta do cliente em uma das opções do menu; se nenhuma servir, responda \"nenhuma\".","type":"string"},"classifierModelId":{"description":"Modelo de IA do classificador (⊆ conta). Sem ele, usa a resolução padrão de modelo da conta.","ref":"AIModel","refEndpoint":"GET /ai/models","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"classifierProfileIds":{"description":"Perfis de parâmetros/reasoning do classificador (⊆ conta).","ref":"AIProfile","refEndpoint":"GET /ai/profiles","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"classifierTimeout":{"description":"Timeout (segundos) da chamada LLM do classificador; padrão 120.","example":120,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"judgeSystemPrompt":{"description":"System prompt do juiz de turno do react_agent. Vazio/null = usa o prompt padrão. Use {$juiz_historico} e {$juiz_resposta}; peça o JSON { enviar_mensagem, turno_encerrado, instrucao }.","example":"Avalie se a última ação do atendente está correta e se o turno acabou.","type":"string"},"judgeModelId":{"description":"Modelo de IA do juiz de turno (⊆ conta). Sem ele, o juiz não roda (fail-safe: entrega a fala).","ref":"AIModel","refEndpoint":"GET /ai/models","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"judgeProfileIds":{"description":"Perfis de parâmetros/reasoning do juiz (⊆ conta).","ref":"AIProfile","refEndpoint":"GET /ai/profiles","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"judgeTimeout":{"description":"Timeout (segundos) da chamada LLM do juiz; padrão 120.","example":120,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"visionSystemPrompt":{"description":"System prompt (persona) do LLM de visão. Vazio/null = usa o prompt padrão. Analisa a imagem e responde à solicitação/pergunta.","example":"Você analisa imagens de clientes e responde de forma detalhada e fiel ao que aparece.","type":"string"},"visionModelId":{"description":"Modelo de IA de visão (⊆ conta; deve ter capability \"vision\"). Sem ele, cai no FALLBACK por capability: usa qualquer modelo acessível com \"vision\" (igual TTS/STT). Só desliga se não houver nenhum.","ref":"AIModel","refEndpoint":"GET /ai/models","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"visionProfileIds":{"description":"Perfis de parâmetros/reasoning do LLM de visão (⊆ conta).","ref":"AIProfile","refEndpoint":"GET /ai/profiles","type":"array","items":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"visionTimeout":{"description":"Timeout (segundos) da chamada LLM de visão; padrão 120.","example":120,"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"active":{"description":"Ativo?","type":"boolean"}},"id":"FlowUpdate","description":"Atualização de fluxo. Mudança crítica + histórico de uso → versiona (clone, 201); senão in-place (200)."},"FlowGraphPatch":{"type":"object","properties":{"ops":{"minItems":1,"type":"array","items":{"oneOf":[{"type":"object","properties":{"op":{"type":"string","const":"set_node_data"},"nodeId":{"type":"string","description":"ID do nó a editar. Inexistente → entra em notFound (não derruba o patch).","example":"n5"},"data":{"description":"Merge SHALLOW no data do nó: troca SÓ as chaves top-level enviadas (sub-objetos são substituídos inteiros — não há deep-merge). Ex.: { text: \"novo\" } só muda text. O maior economizador de token.","example":{"text":"Olá, tudo bem?"},"type":"object","additionalProperties":{}},"unset":{"description":"Chaves de data a REMOVER (use isto p/ apagar chave — não data:{x:null}, pois null é valor legítimo).","example":["nextNodeId"],"type":"array","items":{"type":"string"}},"replaceData":{"description":"true = substitui o data INTEIRO pelo enviado (ignora o merge shallow).","example":false,"type":"boolean"}},"required":["op","nodeId"],"description":"Edita o data de um nó (merge shallow por padrão)."},{"type":"object","properties":{"op":{"type":"string","const":"upsert_node"},"node":{"type":"object","properties":{"id":{"type":"string","description":"ID único do nó no grafo.","example":"n5"},"type":{"description":"Tipo do nó (ver /schema → nodes). Ex.: message, choice, react_agent.","example":"message","type":"string"},"position":{"description":"Posição no canvas (cosmético).","type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"}},"required":["x","y"]},"data":{"description":"Configuração do nó (campos por tipo — ver /schema → nodes).","type":"object","additionalProperties":{}}},"required":["id"],"additionalProperties":{},"description":"Nó a adicionar ou substituir INTEIRO (chaveado por node.id)."}},"required":["op","node"],"description":"Adiciona ou substitui um nó por id."},{"type":"object","properties":{"op":{"type":"string","const":"remove_node"},"nodeId":{"type":"string","description":"ID do nó a remover. Inexistente → notFound (idempotente).","example":"n5"},"removeEdges":{"description":"Também remove os edges conectados ao nó. Padrão: true.","example":true,"type":"boolean"}},"required":["op","nodeId"],"description":"Remove um nó (e, por padrão, seus edges)."},{"type":"object","properties":{"op":{"type":"string","const":"move_node"},"nodeId":{"type":"string","description":"ID do nó a reposicionar.","example":"n5"},"position":{"type":"object","properties":{"x":{"type":"number"},"y":{"type":"number"}},"required":["x","y"],"description":"Nova posição no canvas (cosmético).","example":{"x":320,"y":120}}},"required":["op","nodeId","position"],"description":"Reposiciona um nó (cosmético)."},{"type":"object","properties":{"op":{"type":"string","const":"add_edge"},"edge":{"type":"object","properties":{"id":{"description":"ID do edge (upsert por id; ausente = sempre adiciona).","example":"e12","type":"string"},"source":{"description":"ID do nó de origem.","example":"n5","type":"string"},"target":{"description":"ID do nó de destino.","example":"n6","type":"string"},"sourceHandle":{"description":"Porta de saída da origem (null = padrão).","anyOf":[{"type":"string"},{"type":"null"}]}},"additionalProperties":{},"description":"Edge a adicionar (upsert por id se houver id)."}},"required":["op","edge"],"description":"Adiciona uma aresta."},{"type":"object","properties":{"op":{"type":"string","const":"upsert_edge"},"edge":{"type":"object","properties":{"id":{"description":"ID do edge (upsert por id; ausente = sempre adiciona).","example":"e12","type":"string"},"source":{"description":"ID do nó de origem.","example":"n5","type":"string"},"target":{"description":"ID do nó de destino.","example":"n6","type":"string"},"sourceHandle":{"description":"Porta de saída da origem (null = padrão).","anyOf":[{"type":"string"},{"type":"null"}]}},"additionalProperties":{},"description":"Edge a adicionar/substituir por id."}},"required":["op","edge"],"description":"Adiciona ou substitui uma aresta por id."},{"type":"object","properties":{"op":{"type":"string","const":"remove_edge"},"edgeId":{"description":"ID do edge a remover.","example":"e12","type":"string"},"source":{"description":"Match por origem (quando o id do edge é desconhecido).","example":"n5","type":"string"},"target":{"description":"Match por destino.","example":"n6","type":"string"},"sourceHandle":{"description":"Match pela porta de saída (opcional).","anyOf":[{"type":"string"},{"type":"null"}]}},"required":["op"],"description":"Remove aresta(s) por id OU por match {source,target,sourceHandle?}."},{"type":"object","properties":{"op":{"type":"string","const":"edit_node_string"},"nodeId":{"type":"string","description":"ID do nó cujo campo de texto será editado incrementalmente.","example":"n5"},"field":{"type":"string","description":"Campo de texto dentro de node.data a editar (ex.: systemPrompt). Deve ser um texto já existente (string).","example":"systemPrompt"},"edits":{"minItems":1,"maxItems":100,"type":"array","items":{"anyOf":[{"type":"object","properties":{"find":{"type":"string","minLength":1,"maxLength":65536,"description":"Trecho LITERAL exato a procurar (sem regex). Deve ser ÚNICO no texto, salvo replaceAll:true.","example":"Responda sempre de forma formal."},"replace":{"type":"string","maxLength":65536,"description":"Texto que substitui o trecho (literal — `$` não é interpretado).","example":"Responda de forma cordial e objetiva."},"replaceAll":{"description":"true troca TODAS as ocorrências. Default false = exige que o trecho seja único (senão erro NOT_UNIQUE).","example":false,"type":"boolean"}},"required":["find","replace"]},{"type":"object","properties":{"findRegex":{"type":"string","minLength":1,"maxLength":1000,"description":"Padrão REGEX (JS). Sempre global (todas as ocorrências, limitado por maxMatches). Padrões com quantificador aninhado (X+)+ são rejeitados (ReDoS).","example":"nf_(\\w+)"},"flags":{"description":"Flags do regex (subconjunto de g i m s u y; `g` é sempre forçado).","example":"g","type":"string","maxLength":8},"replace":{"type":"string","maxLength":65536,"description":"Substituição — aceita backrefs ($1, $&).","example":"nf2_$1"},"maxMatches":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"OBRIGATÓRIO (dry-run gate): se o padrão casar MAIS que isto, o edit falha (TOO_MANY_MATCHES) e nada é salvo.","example":20}},"required":["findRegex","replace","maxMatches"]}],"description":"Uma edição por string: literal {find,replace,replaceAll?} OU regex {findRegex,flags?,replace,maxMatches}."},"description":"Edições por string aplicadas EM ORDEM e de forma ATÔMICA: se qualquer uma falhar (não casou / ambígua / regex inseguro) ou o nó/campo for inválido, NADA é salvo (422). Máx. 100."}},"required":["op","nodeId","field","edits"],"description":"Edita incrementalmente um campo de texto de node.data (ex.: systemPrompt) sem reenviar o nó inteiro. FAIL-FAST: erro em qualquer edit aborta o patch todo."}],"description":"Operação de patch do grafo (discriminada por `op`)."},"description":"Lista de operações, aplicadas NA ORDEM (permite remover+recriar no mesmo patch).","example":[{"op":"set_node_data","nodeId":"n5","data":{"text":"Novo texto"}}]},"allowInvalid":{"description":"true salva mesmo se o grafo RESULTANTE for inválido (rascunho); senão 422 com validation.errors (nada é salvo).","example":false,"type":"boolean"},"returnFlow":{"description":"true inclui os arrays nodes/edges completos na resposta; padrão retorna só o resumo (economia de token).","example":false,"type":"boolean"},"baseUpdatedAt":{"description":"Guarda de lost-update (opcional): se ≠ flow.updatedAt atual → 409 Conflict (outro editou no meio). Sem ele = last-write-wins.","example":"2026-07-09T12:34:56.000Z","type":"string"}},"required":["ops"],"id":"FlowGraphPatch","description":"Patch INCREMENTAL do grafo de um fluxo (economia de token — não reenvia o grafo inteiro). ⚠️ VERSIONAMENTO: se o fluxo tem histórico de uso, uma mudança estrutural CLONA nova versão (201 + novo id, versioned:true); senão in-place (200)."}},"responses":{"Unauthorized":{"description":"API Key ausente, inválida, revogada ou expirada.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"Forbidden":{"description":"A API Key não tem o escopo necessário.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"TooManyRequests":{"description":"Rate-limit excedido (600 req/min/chave). Veja `Retry-After`.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"InvalidEncoding":{"description":"Corpo com bytes inválidos (mojibake) — não é UTF-8. Retorna `code: INVALID_ENCODING`. Reenvie em UTF-8 (arquivos de origem no Windows salvos como UTF-8, não ANSI/CP1252).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"paths":{"/schema":{"get":{"tags":["Schema"],"operationId":"getSchemaManifest","security":[],"summary":"Manifesto de schema (nodes + scripting + entities)","description":"Contrato estruturado p/ autorar headless (público, sem API Key): **`nodes`** (15 tipos de nó — campos de `data`, saídas/links e handles), **`scripting`** (funções do nó Código, variáveis e sandbox) e **`entities`** (JSON Schema de cada entidade gravável). Mesma fonte que valida o backend e alimenta as tools/resources do MCP — sem divergência.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"nodes":{"type":"array","items":{"type":"object"},"description":"Tipos de nó (type/label/dataFields/links)."},"scripting":{"type":"object","description":"functions/readContext/textVariables/liveTimeFunctions/sandboxBlocked/timeoutMs."},"entities":{"type":"object","description":"JSON Schema (por nome) de cada entidade gravável."}}}}}}}}},"/reports/tickets":{"get":{"tags":["Relatórios"],"summary":"Relatório de atendimentos","operationId":"getTicketsReport","description":"Tickets paginados da conta (exclui tickets de teste). Cada ticket traz `contact`, `user`, `department`, `channel`, `resolutionReason` (`{ id, name }` = o nível final escolhido), **`resolutionReasonPath`** (motivo completo raiz→folha, ex.: `\"Suporte → Sem conexão → Roteador reiniciado\"`; vazio se sem motivo), `currentFlow`, `survey` e **`tags`** (array de `{ id, type, name, color }` atribuídas ao atendimento). Escopo: `reports:read`.","parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":20}},{"name":"startDate","in":"query","schema":{"type":"string","format":"date"},"description":"Início do período (YYYY-MM-DD), wall-clock Brasília."},{"name":"endDate","in":"query","schema":{"type":"string","format":"date"},"description":"Fim do período (YYYY-MM-DD), wall-clock Brasília."},{"name":"id","in":"query","schema":{"type":"integer","minimum":1},"description":"Filtra pelo **ID exato** do ticket. Lookup pontual: quando enviado, o range de datas (`startDate`/`endDate`) é IGNORADO (acha o ticket independentemente da data). Os demais filtros, se enviados, ainda restringem.","example":5103},{"name":"userId","in":"query","schema":{"type":"integer"}},{"name":"departmentId","in":"query","schema":{"type":"integer"}},{"name":"status","in":"query","schema":{"type":"string"},"description":"BOT|WAITING|IN_PROGRESS|CLOSED|SURVEY"},{"name":"contactName","in":"query","schema":{"type":"string"}},{"name":"contactNumber","in":"query","schema":{"type":"string"}},{"name":"search","in":"query","schema":{"type":"string"},"description":"Busca unificada por contato: casa NOME **ou** NÚMERO (só os dígitos são usados na parte de número). Quando enviado, tem precedência sobre `contactName`/`contactNumber`.","example":"Maria"},{"name":"sortBy","in":"query","schema":{"type":"string","enum":["id","createdAt","closedAt","status","contactName","userName","departmentName"]},"description":"Coluna de ordenação. Valor fora da lista cai no padrão `createdAt`.","example":"createdAt"},{"name":"sortOrder","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"},"description":"Direção da ordenação (padrão `desc`).","example":"desc"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"tickets":{"type":"array","items":{"type":"object"}},"pagination":{"type":"object","properties":{"total":{"type":"integer"},"page":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/reports/operators":{"get":{"tags":["Relatórios"],"summary":"Relatório de operadores","operationId":"getOperatorsReport","description":"Sessões/atividade dos operadores. Escopo: `reports:read`.","parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":20}},{"name":"startDate","in":"query","schema":{"type":"string","format":"date"},"description":"Início do período (YYYY-MM-DD), wall-clock Brasília."},{"name":"endDate","in":"query","schema":{"type":"string","format":"date"},"description":"Fim do período (YYYY-MM-DD), wall-clock Brasília."},{"name":"userId","in":"query","schema":{"type":"integer"}},{"name":"status","in":"query","schema":{"type":"string","enum":["online","offline"]}},{"name":"sortBy","in":"query","schema":{"type":"string","default":"loginAt"}},{"name":"sortOrder","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"sessions":{"type":"array","items":{"type":"object"}},"pagination":{"type":"object","properties":{"total":{"type":"integer"},"page":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/reports/campaigns":{"get":{"tags":["Relatórios"],"summary":"Relatório de campanhas","operationId":"getCampaignsReport","description":"Lista de campanhas com contadores (enviados/falhas/respostas). Escopo: `reports:read`.","parameters":[{"name":"page","in":"query","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","schema":{"type":"integer","default":20}},{"name":"startDate","in":"query","schema":{"type":"string","format":"date"},"description":"Início do período (YYYY-MM-DD), wall-clock Brasília."},{"name":"endDate","in":"query","schema":{"type":"string","format":"date"},"description":"Fim do período (YYYY-MM-DD), wall-clock Brasília."},{"name":"status","in":"query","schema":{"type":"string"}},{"name":"channelId","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"campaigns":{"type":"array","items":{"type":"object"}},"pagination":{"type":"object","properties":{"total":{"type":"integer"},"page":{"type":"integer"},"limit":{"type":"integer"},"totalPages":{"type":"integer"}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/reports/campaigns/{id}":{"get":{"tags":["Relatórios"],"summary":"Detalhe de campanha","operationId":"getCampaignDetail","description":"Campanha + breakdown de destinatários por status. Escopo: `reports:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Campanha não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/reports/surveys":{"get":{"tags":["Relatórios"],"summary":"Relatório de satisfação (CSAT)","operationId":"getSurveyReport","description":"Agregados de respostas de pesquisa: totais por status, CSAT (média das perguntas `rating`), tempo médio e distribuição por pergunta. A distribuição de uma pergunta `rating` com rótulos (`ratingLabels`) é chaveada por `\"valor — rótulo\"`. Período vazio → agregados zerados (`200`). Escopo: `reports:read`.","parameters":[{"name":"startDate","in":"query","schema":{"type":"string","format":"date"},"description":"Início do período (YYYY-MM-DD), wall-clock Brasília."},{"name":"endDate","in":"query","schema":{"type":"string","format":"date"},"description":"Fim do período (YYYY-MM-DD), wall-clock Brasília."},{"name":"surveyId","in":"query","schema":{"type":"integer"}},{"name":"channelId","in":"query","schema":{"type":"integer"}},{"name":"departmentId","in":"query","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"totals":{"type":"object"},"byStatus":{"type":"object"},"bySurvey":{"type":"array","items":{"type":"object"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/campaigns":{"get":{"tags":["Campanhas"],"summary":"Listar campanhas","operationId":"listCampaigns","description":"Lista as campanhas da conta (paginado). Escopo: `campaigns:read`.","parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer"},"description":"Página (1-based)."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer"},"description":"Itens por página."},{"name":"status","in":"query","required":false,"schema":{"type":"string"},"description":"Filtra por status (DRAFT/RUNNING/PAUSED/…)."},{"name":"channelId","in":"query","required":false,"schema":{"type":"integer"},"description":"Filtra por canal."}],"responses":{"200":{"description":"`{ campaigns[], pagination }`"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Campanhas"],"summary":"Criar campanha","operationId":"createCampaign","description":"Cria uma campanha em **DRAFT**. Escopo: `campaigns:write`. Opcional: `recipients` e/ou `tagIds` (+ `tagMatchMode`) adicionam destinatários iniciais (união; dedupe + opt-out) e `media` (base64 ou `mediaAssetId`). Use o header **`Idempotency-Key`** para evitar duplicar em retries (mesma chave → `409`).","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Dedup de requisições em retry (TTL 24h)."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Campaign"}}}},"responses":{"201":{"description":"Campanha criada (DRAFT).","content":{"application/json":{"schema":{"type":"object","properties":{"campaign":{"type":"object"},"recipients":{"type":"object","nullable":true}}}}}},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Idempotency-Key já utilizada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/campaigns/{id}/recipients":{"get":{"tags":["Campanhas"],"summary":"Listar destinatários","operationId":"listCampaignRecipients","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `campaigns:read`. Paginado (page/limit/status).","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Campanha não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"post":{"tags":["Campanhas"],"summary":"Adicionar destinatários","operationId":"addCampaignRecipients","description":"Adiciona destinatários por linhas explícitas (`recipients`) E/OU seleção por tag (`tagIds` + `tagMatchMode`, escopo CUSTOMER/CONTACT — une os contatos das tags). As fontes são unidas; normalização BR, dedupe e opt-out respeitados. Informe ao menos uma das fontes. Escopo: `campaigns:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddRecipients"}}}},"responses":{"200":{"description":"Resultado (added/skippedDuplicate/skippedInvalid/skippedOptOut/totalRecipients)"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Campanha não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/campaigns/{id}/launch":{"post":{"tags":["Campanhas"],"summary":"Disparar/agendar campanha","operationId":"launchCampaign","description":"Valida (preflight: HSM/Official, mídia, canal conectado, destinatários pendentes) e agenda (**SCHEDULED**). O disparo efetivo roda no dispatcher (servidor PRIMARY). Escopo: `campaigns:write`. Idempotente via `Idempotency-Key`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Agendada","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"campaign":{"type":"object"},"recipientCount":{"type":"integer"},"reactivated":{"type":"boolean"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Campanha não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"409":{"description":"Status inválido p/ launch OU Idempotency-Key duplicada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Preflight falhou (erros em `details`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/flows":{"get":{"tags":["Fluxos"],"summary":"Listar fluxos","operationId":"listFlows","description":"Escopo: `flows:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Fluxos"],"summary":"Criar fluxo (paridade total com o painel)","operationId":"createFlow","description":"Cria fluxo. **Paridade com o painel**: nós/edges/variáveis + config (defaultExitMsg, inatividade, edgeType, useFloatingEdges, isDefault), **TTS/STT** (defaultTtsModelId/defaultSttModelId + defaultTtsProfileIds/defaultSttProfileIds + ttsDefaultEnabled/ttsAutoAskPreference/ttsOutputMode) e **clone/versão** (parentFlowId/updateChannelsFromId/cloneAllChildren). categoryId/parentFlowId/updateChannelsFromId validados ⊆ conta (→422). Retorna o fluxo + `validation`. Escopo: `flows:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowCreate"}}}},"responses":{"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Ref (categoria/pai/clone) fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/flows/validate":{"post":{"tags":["Fluxos"],"summary":"Validar fluxo (sem salvar)","operationId":"validateFlow","description":"Valida nós/edges (15 tipos, incl. business_hours e sleep) e referências. Escopo: `flows:read`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"nodes":{"type":"array","items":{"type":"object"}},"edges":{"type":"array","items":{"type":"object"}}}}}}},"responses":{"200":{"description":"{ ok, errors[], warnings[] }"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/flows/{id}":{"get":{"tags":["Fluxos"],"summary":"Obter fluxo (completo + validação)","operationId":"getFlow","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]},"description":"`summary` devolve o grafo ENXUTO (nós `{id,type,label,outputs}` + edges `{id,source,target,sourceHandle}` **sem o `data` pesado**) — ideal p/ mapear a topologia antes de um `PATCH .../graph` gastando ~10× menos token."}],"responses":{"200":{"description":"OK (completo, ou resumo com `?view=summary`)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"put":{"tags":["Fluxos"],"summary":"Atualizar fluxo (versiona se houver histórico de uso)","operationId":"updateFlow","description":"Mudança crítica + histórico → versiona (clone, **201** + `versioned:true`, antiga vira inativa); senão in-place (**200**). `nodes`/`edges`/`variables` voltam como **array**. Escopo: `flows:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowUpdate"}}}},"responses":{"200":{"description":"Atualizado in-place"},"201":{"description":"Versionado (novo fluxo clonado)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Fluxos"],"summary":"Excluir fluxo (só sem uso)","operationId":"deleteFlow","description":"Exclui o fluxo. **Recusa (400)** se vinculado a canal ATIVO ou com histórico de atendimento (auditoria — nesse caso desative via `PUT` com `active:false`). Só apaga fluxos sem uso (ex.: testes). Escopo: `flows:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Excluído"},"400":{"description":"Em uso (canal ativo/histórico) — não pode excluir","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/flows/{id}/graph":{"patch":{"tags":["Fluxos"],"summary":"Patch INCREMENTAL do grafo (economia de token)","operationId":"patchFlowGraph","description":"Edita o grafo do fluxo enviando **só as operações do que mudou** (não o grafo inteiro) — ideal p/ economizar token: combine com `GET /flows/{id}?view=summary` p/ mapear a topologia. Ops (aplicadas NA ORDEM): `set_node_data` (merge **shallow** do `data` + `unset[]` p/ remover chave + `replaceData` p/ trocar tudo), `upsert_node`, `remove_node` (`removeEdges` default true), `move_node`, `add_edge`/`upsert_edge`, `remove_edge` (por `edgeId` OU match `{source,target,sourceHandle?}`), e `edit_node_string` (**edição incremental por string** de um campo de texto do `node.data`, ex.: `systemPrompt` — `{nodeId,field,edits:[{find,replace,replaceAll?}|{findRegex,flags?,replace,maxMatches}]}`, sem reenviar o nó inteiro). id inexistente em set/move/remove → volta em `notFound[]` (não derruba). **⚠️ `edit_node_string` é FAIL-FAST** (≠ dos outros): se um edit não casa/é ambíguo/regex inseguro, ou o nó/campo é inválido, o patch INTEIRO falha (`400`/`422`) e **nada é salvo**. Resposta enxuta por padrão (`{id,versioned,applied,notFound,validation,nodeCount,edgeCount}`); `returnFlow:true` inclui `nodes`/`edges`. **⚠️ Versionamento (Lei):** se o fluxo tem histórico de atendimento, muda-estrutura → **CLONA nova versão** (`201` + novo `id`, `versioned:true`); senão in-place (`200`). Escopo: `flows:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowGraphPatch"},"examples":{"setNodeData":{"summary":"Trocar um campo do node.data (merge shallow)","value":{"ops":[{"op":"set_node_data","nodeId":"n5","data":{"text":"Novo texto"}}]}},"editNodeString":{"summary":"Editar um TRECHO do systemPrompt de um nó (sem reenviar o nó)","value":{"ops":[{"op":"edit_node_string","nodeId":"n5","field":"systemPrompt","edits":[{"find":"<trecho literal exato>","replace":"<novo texto>"}]}]}},"editNodeStringRegex":{"summary":"Renomear por regex dentro do systemPrompt (com teto)","value":{"ops":[{"op":"edit_node_string","nodeId":"n5","field":"systemPrompt","edits":[{"findRegex":"NF\\b","flags":"g","replace":"NetFibra","maxMatches":10}]}]}}}}}},"responses":{"200":{"description":"Aplicado in-place (`{id,versioned:false,applied,notFound,validation,nodeCount,edgeCount}`)"},"201":{"description":"Aplicado + **versionado** (fluxo clonado — use o novo `id`; `versioned:true`)"},"400":{"description":"`ops` vazio, id inválido, corpo com codificação inválida, ou `edit_node_string` com regex inseguro/inválido (`reason`+`index`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"409":{"description":"Conflito — `baseUpdatedAt` ≠ `flow.updatedAt` atual (outro editou no meio). Releia e reaplique.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Grafo resultante inválido (sem `allowInvalid:true`), ref (react_agent tool/rule/profile) fora da conta, ou uma edição de `edit_node_string` não casou / nó·campo inválido (`reason`+`index`) — nada foi salvo","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/flows/debug/sessions":{"get":{"tags":["Fluxos"],"summary":"Listar sessões de debug ativas","operationId":"listFlowDebugSessions","description":"Lista as sessões DEBUG abertas da conta — útil p/ recuperar o `debugSessionId` e encerrar sessões órfãs. Retorna `{ sessions: [{ debugSessionId, flowId, currentNode, createdAt, lastActivityAt }], count, max }`. Escopo: `flows:read`. (Sessões inativas >30min são encerradas automaticamente por um TTL.)","responses":{"200":{"description":"Lista de sessões ativas"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"delete":{"tags":["Fluxos"],"summary":"Encerrar TODAS as sessões de debug","operationId":"clearFlowDebugSessions","description":"Encerra e limpa todas as sessões DEBUG da conta (reset). Retorna `{ success, closed }`. Escopo: `flows:write`. Use quando sessões ficaram presas e o cap de 10/conta foi atingido.","responses":{"200":{"description":"Sessões encerradas"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/flows/{id}/debug":{"post":{"tags":["Fluxos"],"summary":"Iniciar sessão de debug","operationId":"startFlowDebug","description":"Cria sessão DEBUG (síncrona). Retorna `{ debugSessionId, currentNode, variables }`. Escopo: `flows:write`. Máx. 10 sessões/conta (sessões inativas >30min são encerradas por TTL; use `GET/DELETE /flows/debug/sessions` p/ gerenciar).","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"201":{"description":"Sessão criada"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Fluxo não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"429":{"description":"Muitas sessões abertas","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/flows/{id}/debug/{sid}/input":{"post":{"tags":["Fluxos"],"summary":"Enviar input ao debug (1 passo)","operationId":"flowDebugInput","description":"Processa um passo. SÍNCRONO (default): retorna `{ currentNode, output[], variables, history, requests, errors, finished }`. ⏱️ ASSÍNCRONO (`async:true`): p/ cadeias longas que estouram o teto ~120s do transporte MCP, o passo roda em BACKGROUND e retorna JÁ `{ ack:true, debugSessionId, state:\"running\", cursor }` — depois faça LOOP de `GET .../poll?sinceCursor=` até `state != \"running\"`. Escopo: `flows:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"sid","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","description":"Texto do input do cliente (ou número/rótulo p/ responder um menu)."},"async":{"type":"boolean","default":false,"description":"Se `true`, dispara o passo em background e retorna um ack + cursor (observe por `/poll`). Default `false` = síncrono (bloqueia até o passo acabar)."}}}}}},"responses":{"200":{"description":"Estado pós-passo (sync) ou ack+cursor (async)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Sessão não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/flows/{id}/debug/{sid}/poll":{"get":{"tags":["Fluxos"],"summary":"Observar progresso do debug (delta incremental)","operationId":"flowDebugPoll","description":"Observação READ-ONLY do progresso de um passo ASSÍNCRONO (ou de qualquer sessão). Retorna `{ state, events[], output[], currentNode, variables?, cursor, finished }`. `state`: `running` · `awaiting_input` · `finished`. `events` = DELTA desde `sinceCursor` — ENXUTOS por padrão (`{ seq, kind:\"node\"|\"tool\"|\"error\", nodeId?, toolName?, result?, text? }`, sem o system prompt); `verbose=1` traz `{ history, requests }` completos + `variables` mesmo em `running`. `output` = msgs novas do bot. Repita passando o `cursor` retornado até `state != \"running\"` (backoff ~1s). Escopo: `flows:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"sid","in":"path","required":true,"schema":{"type":"integer"}},{"name":"sinceCursor","in":"query","required":false,"schema":{"type":"string"},"description":"Cursor opaco do poll anterior (omitido = desde o início do passo)."},{"name":"verbose","in":"query","required":false,"schema":{"type":"string","enum":["0","1"]},"description":"`1` = payload completo (history/requests) + variables; default = enxuto. ⚠️ Requer o escopo `flows:write` (o payload cru pode conter segredos resolvidos + tokens de nó API/Código/react_agent — mesma proteção do input síncrono); com key só-`flows:read` o verbose é ignorado (retorna enxuto + `verboseDenied`)."}],"responses":{"200":{"description":"Delta de eventos + estado"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Sessão não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/flows/{id}/debug/{sid}":{"delete":{"tags":["Fluxos"],"summary":"Encerrar sessão de debug","operationId":"endFlowDebug","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"sid","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Encerrada"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Sessão não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/departments":{"get":{"tags":["Gestão de conta"],"summary":"Listar — Setores","description":"Escopo: `account:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Gestão de conta"],"summary":"Criar — Setores","description":"Escopo: `account:write`. Entidades \"por gatilho\" validam channel/dept/tag ⊆ conta (→422).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Department"}}}},"responses":{"200":{"description":"Criado"},"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/departments/{id}":{"put":{"tags":["Gestão de conta"],"summary":"Atualizar — Setores","description":"Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Department"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Gestão de conta"],"summary":"Remover — Setores","description":"Soft-delete. Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tags":{"get":{"tags":["Gestão de conta"],"summary":"Listar — Tags","description":"Escopo: `account:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Gestão de conta"],"summary":"Criar — Tags","description":"Escopo: `account:write`. Entidades \"por gatilho\" validam channel/dept/tag ⊆ conta (→422).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tag"}}}},"responses":{"200":{"description":"Criado"},"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tags/{id}":{"put":{"tags":["Gestão de conta"],"summary":"Atualizar — Tags","description":"Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Tag"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Gestão de conta"],"summary":"Remover — Tags","description":"Soft-delete. Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/resolution-reasons":{"get":{"tags":["Gestão de conta"],"summary":"Listar — Motivos de resolução","description":"Escopo: `account:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Gestão de conta"],"summary":"Criar — Motivos de resolução","description":"Escopo: `account:write`. Entidades \"por gatilho\" validam channel/dept/tag ⊆ conta (→422).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolutionReason"}}}},"responses":{"200":{"description":"Criado"},"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/resolution-reasons/{id}":{"put":{"tags":["Gestão de conta"],"summary":"Atualizar — Motivos de resolução","description":"Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolutionReason"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Gestão de conta"],"summary":"Remover — Motivos de resolução","description":"Soft-delete. Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/resolution-reason-profiles":{"get":{"tags":["Gestão de conta"],"summary":"Listar — Obrigatoriedade de motivo","description":"Escopo: `account:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Gestão de conta"],"summary":"Criar — Obrigatoriedade de motivo","description":"Escopo: `account:write`. Entidades \"por gatilho\" validam channel/dept/tag ⊆ conta (→422).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolutionReasonProfile"}}}},"responses":{"200":{"description":"Criado"},"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/resolution-reason-profiles/{id}":{"put":{"tags":["Gestão de conta"],"summary":"Atualizar — Obrigatoriedade de motivo","description":"Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolutionReasonProfile"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Gestão de conta"],"summary":"Remover — Obrigatoriedade de motivo","description":"Soft-delete. Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/message-prefix-profiles":{"get":{"tags":["Gestão de conta"],"summary":"Listar — Prefixo de mensagem (MessagePrefixProfile)","description":"Escopo: `account:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Gestão de conta"],"summary":"Criar — Prefixo de mensagem (MessagePrefixProfile)","description":"Escopo: `account:write`. Entidades \"por gatilho\" validam channel/dept/tag ⊆ conta (→422).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessagePrefixProfile"}}}},"responses":{"200":{"description":"Criado"},"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/message-prefix-profiles/{id}":{"put":{"tags":["Gestão de conta"],"summary":"Atualizar — Prefixo de mensagem (MessagePrefixProfile)","description":"Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MessagePrefixProfile"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Gestão de conta"],"summary":"Remover — Prefixo de mensagem (MessagePrefixProfile)","description":"Soft-delete. Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/predefined-texts":{"get":{"tags":["Gestão de conta"],"summary":"Listar — Textos predefinidos (compartilhados)","description":"Escopo: `account:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Gestão de conta"],"summary":"Criar — Textos predefinidos (compartilhados)","description":"Escopo: `account:write`. Entidades \"por gatilho\" validam channel/dept/tag ⊆ conta (→422).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PredefinedText"}}}},"responses":{"200":{"description":"Criado"},"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/predefined-texts/{id}":{"put":{"tags":["Gestão de conta"],"summary":"Atualizar — Textos predefinidos (compartilhados)","description":"Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PredefinedText"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Gestão de conta"],"summary":"Remover — Textos predefinidos (compartilhados)","description":"Soft-delete. Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/queue-profiles":{"get":{"tags":["Gestão de conta"],"summary":"Listar — Filas (QueueProfile)","description":"Escopo: `account:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Gestão de conta"],"summary":"Criar — Filas (QueueProfile)","description":"Escopo: `account:write`. Entidades \"por gatilho\" validam channel/dept/tag ⊆ conta (→422).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueueProfile"}}}},"responses":{"200":{"description":"Criado"},"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/queue-profiles/{id}":{"put":{"tags":["Gestão de conta"],"summary":"Atualizar — Filas (QueueProfile)","description":"Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueueProfile"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Gestão de conta"],"summary":"Remover — Filas (QueueProfile)","description":"Soft-delete. Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/business-hours-profiles":{"get":{"tags":["Gestão de conta"],"summary":"Listar — Horários (BusinessHoursProfile)","description":"Escopo: `account:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Gestão de conta"],"summary":"Criar — Horários (BusinessHoursProfile)","description":"Escopo: `account:write`. Entidades \"por gatilho\" validam channel/dept/tag ⊆ conta (→422).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessHoursProfile"}}}},"responses":{"200":{"description":"Criado"},"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/business-hours-profiles/{id}":{"put":{"tags":["Gestão de conta"],"summary":"Atualizar — Horários (BusinessHoursProfile)","description":"Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BusinessHoursProfile"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Gestão de conta"],"summary":"Remover — Horários (BusinessHoursProfile)","description":"Soft-delete. Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/surveys":{"get":{"tags":["Gestão de conta"],"summary":"Listar — Pesquisas de satisfação","description":"Escopo: `account:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Gestão de conta"],"summary":"Criar — Pesquisas de satisfação","description":"Escopo: `account:write`. Entidades \"por gatilho\" validam channel/dept/tag ⊆ conta (→422).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Survey"}}}},"responses":{"200":{"description":"Criado"},"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/surveys/{id}":{"put":{"tags":["Gestão de conta"],"summary":"Atualizar — Pesquisas de satisfação","description":"Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Survey"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Gestão de conta"],"summary":"Remover — Pesquisas de satisfação","description":"Soft-delete. Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/closing-messages":{"get":{"tags":["Gestão de conta"],"summary":"Listar — Mensagens de fechamento","description":"Escopo: `account:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Gestão de conta"],"summary":"Criar — Mensagens de fechamento","description":"Escopo: `account:write`. Entidades \"por gatilho\" validam channel/dept/tag ⊆ conta (→422).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClosingMessage"}}}},"responses":{"200":{"description":"Criado"},"201":{"description":"Criado"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/closing-messages/{id}":{"put":{"tags":["Gestão de conta"],"summary":"Atualizar — Mensagens de fechamento","description":"Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClosingMessage"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Gatilho fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Gestão de conta"],"summary":"Remover — Mensagens de fechamento","description":"Soft-delete. Escopo: `account:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/ai/profiles":{"get":{"tags":["IA"],"summary":"Listar — Perfis de IA","description":"Escopo: `ai:read`. Account-scoped.","parameters":[{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]},"description":"`summary` devolve a entidade ENXUTA — sem os campos pesados (tool sem `code`/`parameters`/`logicData`; rule sem `content`) — p/ mapear/listar gastando ~10× menos token antes de um PATCH."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["IA"],"summary":"Criar/atualizar (upsert) — Perfis de IA","description":"POST = upsert (id no body → update). **Full-replace** (campos omitidos resetam). Para editar só alguns campos sem apagar o resto, use o `PATCH /{id}`. Escopo: `ai:write`. Models forçam `scope=ACCOUNT`; `apiKey` write-only.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIProfile"}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação (JSON inválido em campos Json, nome duplicado, etc.)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Referência (flowIds/categoryIds/modelId) fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/ai/profiles/{id}":{"get":{"tags":["IA"],"summary":"Obter — Perfis de IA","operationId":"getAIProfile","description":"Obtém uma entidade da conta pelo id. `?view=summary` = enxuto. Escopo: `ai:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]},"description":"`summary` devolve a entidade ENXUTA — sem os campos pesados (tool sem `code`/`parameters`/`logicData`; rule sem `content`) — p/ mapear/listar gastando ~10× menos token antes de um PATCH."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["IA"],"summary":"Atualizar (parcial/merge) — Perfis de IA","operationId":"patchAIProfile","description":"PATCH **parcial (merge)**: envia SÓ os campos que mudaram — os OMITIDOS ficam INTOCADOS (sem clobber; ideal p/ editar 1 campo sem reenviar `code`/`parameters`/etc., ~10× menos token que o POST). `replace:true` volta ao comportamento **full-replace** do POST (reseta os omitidos). ⚠️ Em AITool, `name` é imutável após criação. `?view=summary` na resposta. Escopo: `ai:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]},"description":"`summary` devolve a entidade ENXUTA — sem os campos pesados (tool sem `code`/`parameters`/`logicData`; rule sem `content`) — p/ mapear/listar gastando ~10× menos token antes de um PATCH."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIProfilePatch"}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação (ex.: JSON inválido em `parameters`/`logicData`; nome de tool imutável; `contentEdits` combinado com o campo inteiro; `targetField` inválido; regex inseguro/inválido; nada para atualizar)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["IA"],"summary":"Remover — Perfis de IA","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/ai/tools":{"get":{"tags":["IA"],"summary":"Listar — Ferramentas de IA","description":"Escopo: `ai:read`. Account-scoped.","parameters":[{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]},"description":"`summary` devolve a entidade ENXUTA — sem os campos pesados (tool sem `code`/`parameters`/`logicData`; rule sem `content`) — p/ mapear/listar gastando ~10× menos token antes de um PATCH."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["IA"],"summary":"Criar/atualizar (upsert) — Ferramentas de IA","description":"POST = upsert (id no body → update). **Full-replace** (campos omitidos resetam). Para editar só alguns campos sem apagar o resto, use o `PATCH /{id}`. Escopo: `ai:write`. Models forçam `scope=ACCOUNT`; `apiKey` write-only.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AITool"}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação (JSON inválido em campos Json, nome duplicado, etc.)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Referência (flowIds/categoryIds/modelId) fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/ai/tools/{id}":{"get":{"tags":["IA"],"summary":"Obter — Ferramentas de IA","operationId":"getAITool","description":"Obtém uma entidade da conta pelo id. `?view=summary` = enxuto. Escopo: `ai:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]},"description":"`summary` devolve a entidade ENXUTA — sem os campos pesados (tool sem `code`/`parameters`/`logicData`; rule sem `content`) — p/ mapear/listar gastando ~10× menos token antes de um PATCH."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["IA"],"summary":"Atualizar (parcial/merge) — Ferramentas de IA","operationId":"patchAITool","description":"PATCH **parcial (merge)**: envia SÓ os campos que mudaram — os OMITIDOS ficam INTOCADOS (sem clobber; ideal p/ editar 1 campo sem reenviar `code`/`parameters`/etc., ~10× menos token que o POST). `replace:true` volta ao comportamento **full-replace** do POST (reseta os omitidos). ⚠️ Em AITool, `name` é imutável após criação. `?view=summary` na resposta. Escopo: `ai:write`. **Edição incremental por string** (economia de token em textos grandes): envie `contentEdits:[{find,replace,replaceAll?}|{findRegex,flags?,replace,maxMatches}]` p/ editar SÓ um trecho de `code` sem reenviar o campo inteiro (alvo default `code`, ou `targetField:\"description\"`). ATÔMICO/fail-fast: se um edit não casa (`NO_MATCH`), é ambíguo (`NOT_UNIQUE` — use `replaceAll:true` ou mais contexto), casa demais (`TOO_MANY_MATCHES`) ou o regex é inseguro/inválido, **nada é salvo**. Mutuamente exclusivo com enviar o campo inteiro. `baseUpdatedAt` opcional → `409` se a entidade mudou no meio.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]},"description":"`summary` devolve a entidade ENXUTA — sem os campos pesados (tool sem `code`/`parameters`/`logicData`; rule sem `content`) — p/ mapear/listar gastando ~10× menos token antes de um PATCH."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIToolPatch"},"examples":{"stringEditLiteral":{"summary":"Editar 1 trecho de code (literal, sem reenviar o campo inteiro)","value":{"contentEdits":[{"find":"<trecho literal exato e ÚNICO>","replace":"<novo texto>"}]}},"stringEditReplaceAll":{"summary":"Trocar TODAS as ocorrências de um termo","value":{"contentEdits":[{"find":"NF","replace":"NetFibra","replaceAll":true}]}},"stringEditRegex":{"summary":"Renomear por padrão (regex + backref) com teto de segurança","value":{"contentEdits":[{"findRegex":"nf_(\\w+)","flags":"g","replace":"nf2_$1","maxMatches":20}]}},"stringEditSafe":{"summary":"Edição segura contra concorrência (baseUpdatedAt do GET → 409 se mudou)","value":{"contentEdits":[{"find":"<trecho>","replace":"<novo>"}],"baseUpdatedAt":"2026-07-27T03:51:34.611Z"}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação (ex.: JSON inválido em `parameters`/`logicData`; nome de tool imutável; `contentEdits` combinado com o campo inteiro; `targetField` inválido; regex inseguro/inválido; nada para atualizar)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"409":{"description":"Conflito — `baseUpdatedAt` ≠ `updatedAt` atual (edição concorrente). Releia (GET) e reaplique.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Uma edição de `contentEdits` não pôde ser aplicada (não casou / ambígua / casou demais) — nada foi salvo (traz `reason` + `index`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["IA"],"summary":"Remover — Ferramentas de IA","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/ai/rules":{"get":{"tags":["IA"],"summary":"Listar — Regras de IA","description":"Escopo: `ai:read`. Account-scoped.","parameters":[{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]},"description":"`summary` devolve a entidade ENXUTA — sem os campos pesados (tool sem `code`/`parameters`/`logicData`; rule sem `content`) — p/ mapear/listar gastando ~10× menos token antes de um PATCH."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["IA"],"summary":"Criar/atualizar (upsert) — Regras de IA","description":"POST = upsert (id no body → update). **Full-replace** (campos omitidos resetam). Para editar só alguns campos sem apagar o resto, use o `PATCH /{id}`. Escopo: `ai:write`. Models forçam `scope=ACCOUNT`; `apiKey` write-only.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIRule"}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação (JSON inválido em campos Json, nome duplicado, etc.)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"Referência (flowIds/categoryIds/modelId) fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/ai/rules/{id}":{"get":{"tags":["IA"],"summary":"Obter — Regras de IA","operationId":"getAIRule","description":"Obtém uma entidade da conta pelo id. `?view=summary` = enxuto. Escopo: `ai:read`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]},"description":"`summary` devolve a entidade ENXUTA — sem os campos pesados (tool sem `code`/`parameters`/`logicData`; rule sem `content`) — p/ mapear/listar gastando ~10× menos token antes de um PATCH."}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["IA"],"summary":"Atualizar (parcial/merge) — Regras de IA","operationId":"patchAIRule","description":"PATCH **parcial (merge)**: envia SÓ os campos que mudaram — os OMITIDOS ficam INTOCADOS (sem clobber; ideal p/ editar 1 campo sem reenviar `code`/`parameters`/etc., ~10× menos token que o POST). `replace:true` volta ao comportamento **full-replace** do POST (reseta os omitidos). ⚠️ Em AITool, `name` é imutável após criação. `?view=summary` na resposta. Escopo: `ai:write`. **Edição incremental por string** (economia de token em textos grandes): envie `contentEdits:[{find,replace,replaceAll?}|{findRegex,flags?,replace,maxMatches}]` p/ editar SÓ um trecho de `content` sem reenviar o campo inteiro (alvo default `content`). ATÔMICO/fail-fast: se um edit não casa (`NO_MATCH`), é ambíguo (`NOT_UNIQUE` — use `replaceAll:true` ou mais contexto), casa demais (`TOO_MANY_MATCHES`) ou o regex é inseguro/inválido, **nada é salvo**. Mutuamente exclusivo com enviar o campo inteiro. `baseUpdatedAt` opcional → `409` se a entidade mudou no meio.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]},"description":"`summary` devolve a entidade ENXUTA — sem os campos pesados (tool sem `code`/`parameters`/`logicData`; rule sem `content`) — p/ mapear/listar gastando ~10× menos token antes de um PATCH."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIRulePatch"},"examples":{"stringEditLiteral":{"summary":"Editar 1 trecho de content (literal, sem reenviar o campo inteiro)","value":{"contentEdits":[{"find":"<trecho literal exato e ÚNICO>","replace":"<novo texto>"}]}},"stringEditReplaceAll":{"summary":"Trocar TODAS as ocorrências de um termo","value":{"contentEdits":[{"find":"NF","replace":"NetFibra","replaceAll":true}]}},"stringEditRegex":{"summary":"Renomear por padrão (regex + backref) com teto de segurança","value":{"contentEdits":[{"findRegex":"nf_(\\w+)","flags":"g","replace":"nf2_$1","maxMatches":20}]}},"stringEditSafe":{"summary":"Edição segura contra concorrência (baseUpdatedAt do GET → 409 se mudou)","value":{"contentEdits":[{"find":"<trecho>","replace":"<novo>"}],"baseUpdatedAt":"2026-07-27T03:51:34.611Z"}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação (ex.: JSON inválido em `parameters`/`logicData`; nome de tool imutável; `contentEdits` combinado com o campo inteiro; `targetField` inválido; regex inseguro/inválido; nada para atualizar)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"409":{"description":"Conflito — `baseUpdatedAt` ≠ `updatedAt` atual (edição concorrente). Releia (GET) e reaplique.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Uma edição de `contentEdits` não pôde ser aplicada (não casou / ambígua / casou demais) — nada foi salvo (traz `reason` + `index`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["IA"],"summary":"Remover — Regras de IA","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/ai/models":{"get":{"tags":["IA"],"summary":"Listar — Modelos de IA","operationId":"listAIModels","parameters":[{"name":"view","in":"query","required":false,"schema":{"type":"string","enum":["summary"]}}],"description":"Escopo: `ai:read`. Lista os modelos acessíveis à conta: **ACCOUNT** (próprios) + **SYSTEM** (globais e atribuídos à conta). Nos modelos SYSTEM a infraestrutura interna (`baseUrl`, `defaultHeaders`, `chatUrl`, `transcriptUrl`, `ttsUrl`) é **omitida** — a conta vê só `id`/`name`/`model` p/ referenciar nos fluxos. `apiKey` é write-only (resposta só traz `apiKeySet`). `?includeInactive=true` traz inativos.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["IA"],"summary":"Criar/atualizar (upsert) — Modelo de IA","operationId":"upsertAIModel","description":"POST = upsert (`id` no body → update). **Full-replace**. Escopo: `ai:write`. Força `scope=ACCOUNT` (SYSTEM inalcançável); `apiKey` write-only. **Modelos não têm PATCH parcial** — reenvie o objeto no POST.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AIModel"}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/ai/models/{id}":{"delete":{"tags":["IA"],"summary":"Remover — Modelo de IA","operationId":"deleteAIModel","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Soft-delete de um modelo ACCOUNT (nós referenciam `modelId` — nunca hard-delete). Escopo: `ai:write`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/ai/pronunciations":{"get":{"tags":["IA"],"summary":"Listar — Regras de pronúncia (TTS)","operationId":"listPronunciations","description":"Escopo: `ai:read`. Lista as regras da conta + as **SYSTEM** herdadas (read-only, `system:true`). Aplicadas SÓ ao texto enviado ao engine de voz — o texto exibido/persistido nunca muda. `?includeInactive=true` traz as desativadas.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["IA"],"summary":"Criar/atualizar (upsert) — Regra de pronúncia (TTS)","operationId":"upsertPronunciation","description":"POST = upsert (`id` cuid no body → update). Escopo: `ai:write`. Grava SÓ regras da própria conta (as SYSTEM são geridas pelo superadmin, fora da API). `caseSensitive=false` (padrão) casa ignorando caixa + SMART-CASE na saída. `aiModelId` (opcional) escopa a regra a uma voz.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PronunciationEntry"}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"aiModelId fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/ai/pronunciations/{id}":{"delete":{"tags":["IA"],"summary":"Remover — Regra de pronúncia (TTS)","operationId":"deletePronunciation","description":"Soft-delete de uma regra da própria conta. Escopo: `ai:write`.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/users":{"get":{"tags":["Usuários"],"summary":"Listar operadores","operationId":"listUsers","description":"Escopo: `users:read`. Nunca retorna senha.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Usuários"],"summary":"Criar operador","operationId":"createUser","description":"Escopo: `users:write`. **Nunca SUPER_ADMIN** (403); profileId/departmentIds ⊆ conta (422); senha forte write-only.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCreate"}}}},"responses":{"201":{"description":"Criado (sem senha)"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"SUPER_ADMIN proibido","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"409":{"description":"Username já existe","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"profile/setor fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/users/{id}":{"put":{"tags":["Usuários"],"summary":"Atualizar operador","operationId":"updateUser","description":"Escopo: `users:write`. SUPER_ADMIN intocável (403); sem promoção a SUPER_ADMIN; desativar encerra sessões.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserUpdate"}}}},"responses":{"200":{"description":"OK (sem senha)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Alvo SUPER_ADMIN","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"profile/setor fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Usuários"],"summary":"Desativar operador (soft-delete)","operationId":"deleteUser","description":"Escopo: `users:write`. Soft-delete + encerra sessões. SUPER_ADMIN intocável.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"responses":{"200":{"description":"Desativado"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Alvo SUPER_ADMIN","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/channels":{"get":{"tags":["Canais"],"summary":"Listar canais","operationId":"listChannels","description":"Escopo: `channels:read`. Segredos mascarados (`*Set: true`).","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/channels/{id}":{"get":{"tags":["Canais"],"summary":"Obter canal","operationId":"getChannel","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `channels:read`. Segredos mascarados.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"put":{"tags":["Canais"],"summary":"Atualizar metadados do canal","operationId":"updateChannel","description":"Escopo: `channels:write`. Só name/active/defaultFlowId/filterRuleId — **não toca segredos nem a sessão de conexão (QR Code)**.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelUpdate"}}}},"responses":{"200":{"description":"OK (mascarado)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"flow/filtro fora da conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/channels/{id}/variables":{"get":{"tags":["Canais"],"summary":"Obter variáveis do canal","operationId":"getChannelVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Retorna `{ variables }` — mapa chave→valor lido no fluxo como `${channel.vars.*}`. Escopo: `channels:read`.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Canal não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["Canais"],"summary":"Atualizar variáveis do canal (merge)","operationId":"updateChannelVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"MERGE por chave: as chaves de `variables` enviadas sobrescrevem/criam; as demais são preservadas (valor `null`/`\"\"` remove a chave). Não substitui o objeto inteiro. Escopo: `channels:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelVariables"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Canal não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/channels/{id}/secret-variables":{"get":{"tags":["Canais"],"summary":"Obter variáveis SECRETAS do canal (mascarado)","operationId":"getChannelSecretVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Retorna `{ variables }` com os **valores MASCARADOS** (`••••`) — apenas as CHAVES são visíveis. É **write-only**: a API pública NUNCA ecoa o valor real de um segredo. Escopo: `channels:read`.","responses":{"200":{"description":"OK (valores mascarados)","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{"type":"string","example":"••••"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Canal não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["Canais"],"summary":"Atualizar variáveis SECRETAS do canal (merge)","operationId":"updateChannelSecretVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"MERGE por chave: as chaves de `variables` enviadas sobrescrevem/criam; as demais são preservadas (valor `null`/`\"\"` remove a chave). A resposta traz os valores **MASCARADOS** (`••••`) — o valor real nunca é devolvido. Escopo: `channels:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChannelSecretVariables"}}}},"responses":{"200":{"description":"OK (valores mascarados)","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{"type":"string","example":"••••"}}}}}}},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Canal não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/account/variables":{"get":{"tags":["Gestão de conta"],"summary":"Obter variáveis da conta","operationId":"getAccountVariables","description":"Retorna `{ variables }` — mapa chave→valor lido no fluxo como `${account.vars.*}`. A conta é sempre a do API Key. Escopo: `account:read`.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Conta não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["Gestão de conta"],"summary":"Atualizar variáveis da conta (merge)","operationId":"updateAccountVariables","description":"MERGE por chave: as chaves de `variables` enviadas sobrescrevem/criam; as demais são preservadas (valor `null`/`\"\"` remove a chave). Não substitui o objeto inteiro. Escopo: `account:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountVariables"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Conta não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/account/secret-variables":{"get":{"tags":["Gestão de conta"],"summary":"Obter variáveis SECRETAS da conta (mascarado)","operationId":"getAccountSecretVariables","description":"Retorna `{ variables }` com os **valores MASCARADOS** (`••••`) — apenas as CHAVES são visíveis. A conta é sempre a do API Key. É **write-only**: a API pública NUNCA ecoa o valor real. Escopo: `account:read`.","responses":{"200":{"description":"OK (valores mascarados)","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{"type":"string","example":"••••"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Conta não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["Gestão de conta"],"summary":"Atualizar variáveis SECRETAS da conta (merge)","operationId":"updateAccountSecretVariables","description":"MERGE por chave: as chaves de `variables` enviadas sobrescrevem/criam; as demais são preservadas (valor `null`/`\"\"` remove a chave). A resposta traz os valores **MASCARADOS** (`••••`) — o valor real nunca é devolvido. Escopo: `account:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AccountSecretVariables"}}}},"responses":{"200":{"description":"OK (valores mascarados)","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{"type":"string","example":"••••"}}}}}}},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Conta não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets":{"get":{"tags":["Atendimento"],"summary":"Listar conversas (tickets) ao vivo","operationId":"listLiveTickets","description":"Lista as conversas (tickets) da conta ao vivo, paginadas e ordenadas por atividade recente (`updatedAt` desc). Espelha a lista da AgentView. SEMPRE escopado por conta (anti-leak). Escopo: `tickets:read`.","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"},"description":"Filtra por status (CSV): `BOT,WAITING,IN_PROGRESS,SURVEY,CLOSED`.","example":"WAITING,IN_PROGRESS"},{"name":"channelId","in":"query","required":false,"schema":{"type":"integer"},"description":"Filtra pelo canal (Master)."},{"name":"departmentId","in":"query","required":false,"schema":{"type":"integer"},"description":"Filtra pelo setor atual."},{"name":"operatorId","in":"query","required":false,"schema":{"type":"integer"},"description":"Filtra pelo operador responsável (userId do ticket)."},{"name":"tag","in":"query","required":false,"schema":{"type":"integer"},"description":"Filtra por ID de tag aplicada ao ticket."},{"name":"search","in":"query","required":false,"schema":{"type":"string"},"description":"Busca por nome OU número do contato (só os dígitos são usados na parte de número).","example":"Maria"},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1}},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","default":25},"description":"Itens por página (teto 100)."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PublicTicketLive"}},"page":{"type":"integer"},"pageSize":{"type":"integer"},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/tickets/{id}":{"get":{"tags":["Atendimento"],"summary":"Obter uma conversa (ticket) ao vivo","operationId":"getLiveTicket","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Estado ao vivo de um ticket (status, contato, operador atual, tags, não-lidas). Pré-check anti-leak por conta → 404 se não pertence. Escopo: `tickets:read`.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicTicketLive"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/messages":{"get":{"tags":["Atendimento"],"summary":"Histórico de mensagens de um ticket","operationId":"getLiveTicketMessages","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1}},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","default":50},"description":"Itens por página (teto 100)."}],"description":"Mensagens do ticket em ordem cronológica ascendente (estável), paginadas. Cada mensagem traz `mediaUrl` (endpoint autenticado de download) quando há mídia. Pré-check anti-leak por conta → 404. Escopo: `tickets:read`.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PublicMessage"}},"page":{"type":"integer"},"pageSize":{"type":"integer"},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"post":{"tags":["Atendimento"],"summary":"Enviar mensagem de TEXTO como operador","operationId":"sendTicketText","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Envia uma mensagem de texto ao contato do ticket, como o operador do `X-Operator-Id`. **ASSÍNCRONO**: enfileira na fila durável (mesmo caminho do painel — prefixo `*Nome*`, formatação WhatsApp, socket emit, claim e dispatch ficam no consumer PRIMARY) e responde **202** — a `Message` ainda não existe neste ponto. Escopos: `agent:act` + `messages:send`. Pré-check anti-leak por conta → 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","description":"Texto da mensagem (não-vazio). `**negrito**` é convertido para `*negrito*` no WhatsApp.","example":"Olá! Como posso ajudar?"},"quotedMessageId":{"type":"string","description":"ID (WhatsApp `messageId`) da mensagem citada (responder-citando). Opcional."},"noPrefix":{"type":"boolean","description":"true = NÃO adiciona o prefixo `*Nome do operador*`. Opcional (default false)."},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id` (use o header preferencialmente)."}}}}}},"responses":{"202":{"description":"Enfileirado","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"boolean","example":true},"ticketId":{"type":"integer"}}}}}},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/tickets/{id}/messages/media":{"post":{"tags":["Atendimento"],"summary":"Enviar MÍDIA como operador (base64/URL)","operationId":"sendTicketMedia","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Envia mídia (imagem/áudio/vídeo/documento) ao contato do ticket, como o operador. JSON-first: informe **`mediaBase64`** (data URI ou base64 puro) OU **`mediaUrl`** (http(s), baixado com anti-SSRF). Reusa o mesmo caminho do painel (conversão de áudio/vídeo, prefixo, dedup de `MediaAsset`, emit, claim) → a `Message` já é criada; responde **202** com a mensagem. Limites: vídeo 100 MB, restante 15 MB; allowlist de mimetype (image/audio/video + PDF/Office/txt/csv/zip). Escopos: `agent:act` + `messages:send`. Pré-check anti-leak → 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"mediaBase64":{"type":"string","description":"Mídia em data URI (`data:image/png;base64,...`) ou base64 puro. Se puro sem assinatura reconhecível, `mimetype` é obrigatório."},"mediaUrl":{"type":"string","format":"uri","description":"URL http(s) pública da mídia (baixada com anti-SSRF). Alternativa a `mediaBase64`.","example":"https://cdn.exemplo.com/arquivo.pdf"},"mimetype":{"type":"string","description":"Força o mimetype (útil p/ base64 puro).","example":"image/png"},"filename":{"type":"string","description":"Nome do arquivo (documentos).","example":"comprovante.pdf"},"caption":{"type":"string","description":"Legenda (image/video/document)."},"quotedMessageId":{"type":"string","description":"Responder-citando (WhatsApp `messageId`)."},"noPrefix":{"type":"boolean","description":"Não adicionar o prefixo `*Nome*`."},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id`."}}}}}},"responses":{"202":{"description":"Enviado","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"$ref":"#/components/schemas/PublicMessage"}}}}}},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"413":{"description":"Arquivo maior que o limite (vídeo 100 MB / restante 15 MB).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/tickets/{id}/mark-read":{"post":{"tags":["Atendimento"],"summary":"Marcar conversa como lida","operationId":"markTicketRead","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Marca as mensagens inbound do ticket como lidas (não despacha tráfego ao canal). Escopo: `agent:act`. Pré-check anti-leak → 404.","responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/messages/{mid}/react":{"post":{"tags":["Atendimento"],"summary":"Reagir (emoji) a uma mensagem","operationId":"reactTicketMessage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"mid","in":"path","required":true,"schema":{"type":"integer"},"description":"ID numérico (Message.id) da mensagem alvo."},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Reage com um emoji a uma mensagem do ticket (envie `emoji: \"\"` para remover a reação). Só WhatsApp (Baileys/Oficial); outros canais → 422. Escopo: `agent:act`. Pré-check anti-leak → 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"emoji":{"type":"string","description":"Emoji único (string vazia remove).","example":"👍"},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id`."}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket/mensagem não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Canal não suporta reação","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/tickets/{id}/messages/{mid}":{"patch":{"tags":["Atendimento"],"summary":"Editar o texto de uma mensagem `fromMe`","operationId":"editTicketMessage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"mid","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Edita o texto de uma mensagem enviada (`fromMe`). Só WhatsApp Baileys (Oficial → 422); a mensagem não pode estar apagada nem ser de mídia. Nunca hard-delete: grava `editedAt`/`editHistory`. Escopo: `agent:act` (+ `tickets:edit-message` no painel). Pré-check anti-leak → 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["text"],"properties":{"text":{"type":"string","description":"Novo texto da mensagem."},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id`."}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket/mensagem não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Canal não suporta edição","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}},"delete":{"tags":["Atendimento"],"summary":"Apagar (revoke) uma mensagem `fromMe`","operationId":"deleteTicketMessage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"mid","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Apaga (revoke) uma mensagem enviada (`fromMe`). Só WhatsApp Baileys (Oficial → 422). Nunca hard-delete do banco: marca `deletedAt` e mantém o selo. Escopo: `agent:act` (+ `tickets:delete-message`). Pré-check anti-leak → 404.","responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket/mensagem não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"422":{"description":"Canal não suporta remoção","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/messages/forward":{"post":{"tags":["Atendimento"],"summary":"Encaminhar N mensagens para N destinos","operationId":"forwardMessages","parameters":[{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Encaminha uma ou mais mensagens de UMA conversa de origem (ticket ou grupo) para vários destinos (ticket/grupo ativo, contato existente ou número novo). O RBAC por-destino e a posse (origem + destinos ⊆ conta) são validados pelo serviço. Best-effort por destino. Escopo: `agent:act`. Origem inexistente/de outra conta → 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["source","sourceMessageIds","targets"],"properties":{"source":{"type":"object","description":"Conversa de origem.","properties":{"kind":{"type":"string","enum":["ticket","group"]},"ticketId":{"type":"integer"},"groupId":{"type":"integer"}},"example":{"kind":"ticket","ticketId":1234}},"sourceMessageIds":{"type":"array","items":{"type":"integer"},"description":"IDs (Message.id) das mensagens a encaminhar.","example":[55501,55502]},"targets":{"type":"array","description":"Destinos. Cada item aponta para um ticket/grupo/contato/número.","items":{"type":"object"}},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id`."}}}}}},"responses":{"200":{"description":"Resultado por destino (best-effort)"},"400":{"description":"Origem inválida","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Conversa de origem não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/tickets/{id}/claim":{"post":{"tags":["Atendimento"],"summary":"Assumir (claim) o ticket","operationId":"claimTicket","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Assume o ticket para o operador do `X-Operator-Id`. Exige o operador com status READY (igual ao painel) — garanta a sessão/status via `/agent/session` + `/agent/status` antes, senão 403. Escopo: `agent:act`. Pré-check anti-leak → 404.","responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/transfer":{"post":{"tags":["Atendimento"],"summary":"Transferir o ticket (fila/agente/fluxo)","operationId":"transferTicket","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Transfere o ticket. O destino é escolhido pelo corpo: `departmentId` (fila de um setor), `targetUserId` (outro agente) ou `flowId` (de volta a um fluxo). O RBAC por-ação (`tickets:transfer-queue`/`-agent`/`-flow`) roda contra o OPERADOR real. Escopo: `agent:act`. Pré-check anti-leak → 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"departmentId":{"type":"integer","description":"Transferir para a fila deste setor."},"targetUserId":{"type":"integer","description":"Transferir diretamente a este operador."},"flowId":{"type":"integer","description":"Reinjetar no bot deste fluxo (o fluxo precisa aceitar transferência de atendente)."},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id`."}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/tickets/{id}/resolve":{"post":{"tags":["Atendimento"],"summary":"Encerrar o ticket (com motivo)","operationId":"resolveTicket","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Encerra o atendimento. Se um perfil de obrigatoriedade casar no ticket, `resolutionReasonId` (nó FOLHA da árvore ⊆ conta) é exigido — consulte antes `GET /tickets/{id}/resolution-required`. Dispara pesquisa de satisfação/mensagem de fechamento/auto-assign conforme configurado. Escopo: `agent:act`. Pré-check anti-leak → 404.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"isResolved":{"type":"boolean","description":"Marca o atendimento como resolvido/não-resolvido."},"resolutionReasonId":{"type":"integer","description":"ID do motivo de resolução (nó FOLHA). Obrigatório se o perfil exigir."},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id`."}}}}}},"responses":{"200":{"description":"Encerrado"},"400":{"description":"Motivo obrigatório ausente / inválido","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/resolution-required":{"get":{"tags":["Atendimento"],"summary":"Obrigatoriedade de motivo p/ encerrar","operationId":"getTicketResolutionRequired","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Diz se o encerramento DESTE ticket exige um motivo de resolução (e, em caso positivo, a árvore de motivos aplicável). READ-ONLY — não exige operador. Escopo: `tickets:read`. Pré-check anti-leak → 404.","responses":{"200":{"description":"OK (`{ required, reasons? }`)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/force-handover":{"post":{"tags":["Atendimento"],"summary":"Forçar transbordo (voltar ao bot)","operationId":"forceTicketHandover","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Força o transbordo do atendimento de volta ao bot/fila (WAITING). Só em ticket com status BOT (senão 404). Escopo: `agent:act`. Pré-check anti-leak → 404.","responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket não encontrado / não está em BOT","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/tags":{"get":{"tags":["Atendimento"],"summary":"Tags atuais do ticket","operationId":"getTicketTags","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Lista as tags aplicadas ao ticket. READ-ONLY — não exige operador. Escopo: `tickets:read`. Pré-check anti-leak → 404.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"post":{"tags":["Atendimento"],"summary":"Substituir as tags do ticket","operationId":"setTicketTags","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"SUBSTITUI o conjunto de tags do ticket pelo enviado. Escopo: `agent:act`. Pré-check anti-leak → 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tags"],"properties":{"tags":{"type":"array","description":"Tags a aplicar (cria as inexistentes na conta).","items":{"type":"object","properties":{"type":{"type":"string","description":"Escopo da tag (TICKET/CUSTOMER/CONTACT)."},"name":{"type":"string"}},"required":["name"]},"example":[{"type":"TICKET","name":"vip"}]},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id`."}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/tags/add":{"patch":{"tags":["Atendimento"],"summary":"Adicionar tags ao ticket","operationId":"addTicketTags","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Adiciona tags ao ticket (mantém as existentes). Escopo: `agent:act`. Pré-check anti-leak → 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tags"],"properties":{"tags":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"name":{"type":"string"}},"required":["name"]}},"operatorId":{"type":"integer"}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/tags/remove":{"patch":{"tags":["Atendimento"],"summary":"Remover tags do ticket","operationId":"removeTicketTags","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Remove tags do ticket. Escopo: `agent:act`. Pré-check anti-leak → 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["tags"],"properties":{"tags":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"name":{"type":"string"}},"required":["name"]}},"operatorId":{"type":"integer"}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/tag-history":{"get":{"tags":["Atendimento"],"summary":"Histórico de tags do ticket","operationId":"getTicketTagHistory","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Histórico append-only de aplicação/remoção de tags do ticket. READ-ONLY. Escopo: `tickets:read`. Pré-check anti-leak → 404.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/bulk-action":{"post":{"tags":["Atendimento"],"summary":"Ação em massa sobre N tickets","operationId":"ticketsBulkAction","parameters":[{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Executa uma ação sobre vários tickets (best-effort, teto 200). `action`: `transfer-flow` / `transfer-queue` / `transfer-agent` / `close`. Só tickets abertos elegíveis; o RBAC por-ação roda contra o OPERADOR real (`close` exige `tickets:manage`). Escopo: `agent:act`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ticketIds","action"],"properties":{"ticketIds":{"type":"array","items":{"type":"integer"},"description":"IDs dos tickets alvo (⊆ conta).","example":[101,102,103]},"action":{"type":"string","description":"transfer-flow | transfer-queue | transfer-agent | close.","example":"transfer-queue"},"flowId":{"type":"integer"},"departmentId":{"type":"integer"},"targetUserId":{"type":"integer"},"isResolved":{"type":"boolean"},"resolutionReasonId":{"type":"integer"},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id`."}}}}}},"responses":{"200":{"description":"Resultado por ticket (`{ processed, failed[] }`)"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"404":{"description":"Nenhum ticket elegível","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/initiate/channels":{"get":{"tags":["Atendimento"],"summary":"Canais iniciáveis (início proativo)","operationId":"listInitiableChannels","parameters":[{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Lista os canais/setores que o operador pode usar para INICIAR uma conversa proativa (RBAC dinâmico por tipo de canal roda contra o operador). Escopo: `agent:act`.","responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/initiate/contacts":{"get":{"tags":["Atendimento"],"summary":"Buscar contatos para iniciar","operationId":"searchInitiableContacts","parameters":[{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"},{"name":"search","in":"query","required":false,"schema":{"type":"string"},"description":"Termo de busca por nome/número."}],"description":"Busca contatos da conta para iniciar uma conversa proativa. Escopo: `agent:act`.","responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/initiate/templates":{"get":{"tags":["Atendimento"],"summary":"Templates HSM para iniciar (Oficial)","operationId":"listInitiableTemplates","parameters":[{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"},{"name":"channelId","in":"query","required":false,"schema":{"type":"integer"},"description":"ID do canal WhatsApp Oficial."}],"description":"Lista os templates HSM aprovados de um canal WhatsApp Oficial — necessários para iniciar fora da janela de 24h. Escopo: `agent:act`.","responses":{"200":{"description":"OK"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/initiate":{"post":{"tags":["Atendimento"],"summary":"Iniciar conversa proativa","operationId":"initiateTicket","parameters":[{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Inicia um atendimento proativo (o operador é o iniciador; RBAC por tipo de canal + resolução de setor rodam contra ele). Em canal Oficial fora da janela de 24h, use `templateId` + `templateParams`. Escopo: `agent:act`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["channelId","phone"],"properties":{"channelId":{"type":"integer","description":"Canal (Master) de saída."},"phone":{"type":"string","description":"Número do destinatário (E.164/BR; normalizado).","example":"5581999990000"},"name":{"type":"string","description":"Nome do contato (se novo)."},"text":{"type":"string","description":"Texto inicial (janela de 24h / QR Code)."},"departmentId":{"type":"integer","description":"Setor de destino."},"templateId":{"type":"integer","description":"Template HSM aprovado (canal Oficial fora da janela)."},"templateParams":{"type":"array","items":{"type":"string"},"description":"Parâmetros ordenados do corpo do HSM (índice 0 = {{1}})."},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id`."}}}}}},"responses":{"200":{"description":"Iniciado"},"201":{"description":"Iniciado"},"400":{"description":"Validação","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/agent/session":{"post":{"tags":["Atendimento"],"summary":"Abrir/garantir sessão do operador (online)","operationId":"openAgentSession","parameters":[{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Abre (ou reusa/renova) a sessão-API do operador, marcando-o **online** — pré-requisito para auto-assign/roteamento sem o painel. Renova `lastActivity`; se não estiver em pausa, força status `LOGGED_IN`. Idempotente. Escopo: `agent:act`.","responses":{"200":{"description":"OK (`{ sessionId, online: true }`)"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/agent/session/heartbeat":{"post":{"tags":["Atendimento"],"summary":"Heartbeat da sessão do operador","operationId":"agentSessionHeartbeat","parameters":[{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Renova `lastActivity` da sessão-API viva (mantém online). Cadência recomendada ≤ 2 min (janela de online = 5 min). Sem sessão viva, cria uma. Escopo: `agent:act`.","responses":{"200":{"description":"OK (`{ online: true, lastActivity }`)"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/agent/session/logout":{"post":{"tags":["Atendimento"],"summary":"Encerrar sessão do operador (offline)","operationId":"agentSessionLogout","parameters":[{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Encerra todas as sessões vivas do operador e define status `LOGGED_OUT` (fica offline). Escopo: `agent:act`.","responses":{"200":{"description":"OK (`{ online: false }`)"},"400":{"description":"`X-Operator-Id`/`operatorId` ausente ou inválido, ou corpo inválido.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/agent/status":{"post":{"tags":["Atendimento"],"summary":"Definir status do operador","operationId":"setAgentStatus","parameters":[{"name":"X-Operator-Id","in":"header","required":true,"schema":{"type":"integer"},"description":"ID do operador (User ⊆ conta) em cujo nome a ação é executada — carimba `senderId`/`lastAgentId`/auditoria. Fallback: `operatorId` no corpo/query. Operador de outra conta, inexistente ou SUPER_ADMIN → 403; ausente → 400.","example":42},{"name":"X-VChat-Auth-Mode","in":"header","required":false,"schema":{"type":"string","enum":["operator","token"],"default":"operator"},"description":"Modo de autorização. `operator` (default): a ação respeita as permissões RBAC do operador (`X-Operator-Id`). `token`: ignora o RBAC do operador e usa o teto desta API Key — exige o escopo `agent:rbac-override` (sem ele → 403). Fallback: `authMode` no corpo/query.","example":"operator"}],"description":"Define o status do operador: `READY` (disponível), `LOGGED_IN`, `LOGGED_OUT`, `PRE_PAUSE` ou `ON_BREAK` (pausa — exige `pauseTypeId` ⊆ conta). Escopo: `agent:act`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["READY","LOGGED_IN","LOGGED_OUT","PRE_PAUSE","ON_BREAK"],"example":"READY"},"pauseTypeId":{"type":"integer","description":"Obrigatório quando `status=ON_BREAK` (tipo de pausa ⊆ conta)."},"operatorId":{"type":"integer","description":"Fallback do `X-Operator-Id`."}}}}}},"responses":{"200":{"description":"OK (`{ status, pauseTypeId }`)"},"400":{"description":"status inválido / pauseTypeId ausente/ inválido","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Escopo ausente, OU operador inválido para esta conta (cross-tenant/SUPER_ADMIN), OU o operador não tem a permissão de painel exigida pela ação (ex.: edit/delete-message, tags, transfer, close) — nesse caso use `X-VChat-Auth-Mode: token` com uma key de escopo `agent:rbac-override`, OU o header `token` foi enviado sem esse escopo.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/webhooks":{"get":{"tags":["Webhooks"],"summary":"Listar endpoints de webhook","operationId":"listWebhooks","description":"Lista os endpoints de webhook da conta. O `secret` NUNCA é retornado — cada item traz apenas `hasSecret`/`secretMasked`. Escopo: `webhooks:manage`.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PublicWebhookEndpoint"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Webhooks"],"summary":"Criar endpoint de webhook","operationId":"createWebhook","description":"Cria um endpoint. A `url` deve ser **https** e pública (anti-SSRF: rede interna/loopback bloqueadas). Se `secret` não for enviado, um forte é gerado. O `secret` é retornado em CLARO **apenas nesta resposta** (guarde-o: é usado para verificar a assinatura `X-VChat-Signature`). Escopo: `webhooks:manage`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","description":"URL https pública que receberá os POSTs.","example":"https://api.suaempresa.com/vchat/webhook"},"events":{"type":"array","items":{"type":"string","enum":["message.received","message.sent","ticket.created","ticket.status_changed","ticket.assigned","ticket.closed"]},"description":"Eventos assinados. Vazio/omitido = TODOS.","example":["message.received","ticket.closed"]},"active":{"type":"boolean","description":"Ativa/pausa a entrega (default true)."},"secret":{"type":"string","description":"Segredo HMAC (opcional; gerado se ausente). Write-only."}}}}}},"responses":{"201":{"description":"Criado (inclui `secret` em claro — única vez)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/PublicWebhookEndpoint"},{"type":"object","properties":{"secret":{"type":"string","description":"Segredo em claro — exibido só aqui."}}}]}}}}}},"400":{"description":"url inválida (não https / rede interna) ou evento desconhecido","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/webhooks/dlq":{"get":{"tags":["Webhooks"],"summary":"Listar a DLQ (entregas mortas)","operationId":"listWebhookDeadLetters","description":"Lista as entregas que **esgotaram** a janela de retry (backoff denso 15s×6 → exponencial 1min→6h, por até 24h desde a 1ª falha). Itens **sanitizados**: `deliveryId`, `event`, `attempt`, `failedAt`, `endpointIds` — **sem** segredo nem o payload do evento. Filtrada pela sua conta. Reentregue com `POST /webhooks/dlq/redrive`. Escopo: `webhooks:manage`.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100},"description":"Máximo de itens (1..500)."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"deliveryId":{"type":"string"},"event":{"type":"string"},"attempt":{"type":"integer"},"failedAt":{"type":"string","format":"date-time","nullable":true},"endpointIds":{"type":"array","items":{"type":"integer"}}}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/webhooks/dlq/redrive":{"post":{"tags":["Webhooks"],"summary":"Reentregar a DLQ","operationId":"redriveWebhookDeliveries","description":"Reenfileira **todas** as entregas mortas (DLQ) da sua conta, reiniciando o schedule de retry e **preservando o mesmo `X-VChat-Delivery-Id`** (deduplique por ele no seu receptor). Use após restaurar o seu backend de uma janela de manutenção. Escopo: `webhooks:manage`.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"redriven":{"type":"integer","description":"Quantidade de entregas reenfileiradas."}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/webhooks/{id}":{"put":{"tags":["Webhooks"],"summary":"Atualizar endpoint de webhook","operationId":"updateWebhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Atualiza url/events/active e, se `secret` for enviado, **rotaciona** o segredo (retornado em claro apenas nesta resposta). Escopo: `webhooks:manage`. Anti-leak por conta → 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string"}},"active":{"type":"boolean"},"secret":{"type":"string","description":"Envie para rotacionar (write-only)."}}}}}},"responses":{"200":{"description":"OK (inclui `secret` só se rotacionado)","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PublicWebhookEndpoint"}}}}}},"400":{"description":"url inválida ou evento desconhecido","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Webhook não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Webhooks"],"summary":"Remover endpoint de webhook (soft-delete)","operationId":"deleteWebhook","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Soft-delete do endpoint (para de entregar). Escopo: `webhooks:manage`. Anti-leak por conta → 404.","responses":{"204":{"description":"Removido"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Webhook não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/messages/{id}/media":{"get":{"tags":["Atendimento"],"summary":"Baixar a mídia de uma mensagem","operationId":"getMessageMedia","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Baixa o binário da mídia (imagem/áudio/vídeo/documento) de uma mensagem. Autenticação por **API Key** (Bearer) — a mesma URL do painel também aceita assinatura HMAC/`?token=`. O corpo é o **binário cru** (não JSON) com o `Content-Type` do arquivo. Gate de tenant: a mensagem precisa pertencer à conta da chave (senão 404). Escopo: `tickets:read`.","responses":{"200":{"description":"Binário da mídia.","content":{"application/octet-stream":{"schema":{"type":"string","format":"binary"}},"image/*":{"schema":{"type":"string","format":"binary"}},"audio/*":{"schema":{"type":"string","format":"binary"}},"video/*":{"schema":{"type":"string","format":"binary"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"description":"Mensagem/mídia não encontrada (ou de outra conta)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/agent/{id}/status":{"get":{"tags":["Atendimento"],"summary":"Status e presença de um operador","operationId":"getAgentStatus","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Status atual + presença de um operador (User) da conta: `status` (READY/ON_BREAK/…), `online` (heartbeat/atividade na janela de online), `activeTickets` e `capacity`. Base do roteamento do integrador (auto-assign exige READY + online). Pré-check anti-leak por conta → 404. Escopo: `users:read`.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicOperatorStatus"}}}},"400":{"description":"ID de operador inválido","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Operador não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/variables":{"get":{"tags":["Tickets"],"summary":"Obter variáveis do ticket","operationId":"getTicketVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Retorna `{ variables }` — mapa chave→valor lido no fluxo como `${ticket.vars.*}`. Escopo: `tickets:read`.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["Tickets"],"summary":"Atualizar variáveis do ticket (merge)","operationId":"updateTicketVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"MERGE por chave: as chaves de `variables` enviadas sobrescrevem/criam; as demais são preservadas (valor `null`/`\"\"` remove a chave). Não substitui o objeto inteiro. Escopo: `tickets:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketVariables"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/secret-variables":{"get":{"tags":["Tickets"],"summary":"Obter variáveis SECRETAS do ticket (mascarado)","operationId":"getTicketSecretVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Retorna `{ variables }` com os **valores MASCARADOS** (`••••`) — apenas as CHAVES são visíveis. É **write-only**: a API pública NUNCA ecoa o valor real. Escopo: `tickets:read`.","responses":{"200":{"description":"OK (valores mascarados)","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{"type":"string","example":"••••"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["Tickets"],"summary":"Atualizar variáveis SECRETAS do ticket (merge)","operationId":"updateTicketSecretVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"MERGE por chave: as chaves de `variables` enviadas sobrescrevem/criam; as demais são preservadas (valor `null`/`\"\"` remove a chave). A resposta traz os valores **MASCARADOS** (`••••`) — o valor real nunca é devolvido. Escopo: `tickets:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TicketSecretVariables"}}}},"responses":{"200":{"description":"OK (valores mascarados)","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{"type":"string","example":"••••"}}}}}}},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/tickets/{id}/send-contact":{"post":{"tags":["Tickets"],"summary":"Enviar Contato (vCard) ao ticket","operationId":"sendTicketContact","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Envia uma mensagem de **Contato** (cartão vCard) a um ticket existente. Persiste 1 mensagem (atribuída ao BOT), emite o evento em tempo real e despacha ao canal (Oficial inline / QR Code via fila durável / fallback texto). Não cria ticket. Escopo: `tickets:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendContact"}}}},"responses":{"200":{"description":"Enviado","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"sent":{"type":"integer","description":"Quantidade de contatos enviados (após saneamento)."}}}}}},"400":{"description":"Validação / contato sem nome+telefone após saneamento","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Ticket não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"503":{"description":"Serviço de envio indisponível","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/customers/{id}/variables":{"get":{"tags":["Clientes"],"summary":"Obter variáveis do cliente","operationId":"getCustomerVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Retorna `{ variables }` — mapa chave→valor lido no fluxo como `${customer.vars.*}`. Escopo: `customers:read`.","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":true}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Cliente não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["Clientes"],"summary":"Atualizar variáveis do cliente (merge)","operationId":"updateCustomerVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"MERGE por chave: as chaves de `variables` enviadas sobrescrevem/criam; as demais são preservadas (valor `null`/`\"\"` remove a chave). Não substitui o objeto inteiro. Escopo: `customers:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerVariables"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Cliente não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/customers/{id}/secret-variables":{"get":{"tags":["Clientes"],"summary":"Obter variáveis SECRETAS do cliente (mascarado)","operationId":"getCustomerSecretVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Retorna `{ variables }` com os **valores MASCARADOS** (`••••`) — apenas as CHAVES são visíveis. É **write-only**: a API pública NUNCA ecoa o valor real. Escopo: `customers:read`.","responses":{"200":{"description":"OK (valores mascarados)","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{"type":"string","example":"••••"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Cliente não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"patch":{"tags":["Clientes"],"summary":"Atualizar variáveis SECRETAS do cliente (merge)","operationId":"updateCustomerSecretVariables","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"MERGE por chave: as chaves de `variables` enviadas sobrescrevem/criam; as demais são preservadas (valor `null`/`\"\"` remove a chave). A resposta traz os valores **MASCARADOS** (`••••`) — o valor real nunca é devolvido. Escopo: `customers:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomerSecretVariables"}}}},"responses":{"200":{"description":"OK (valores mascarados)","content":{"application/json":{"schema":{"type":"object","properties":{"variables":{"type":"object","additionalProperties":{"type":"string","example":"••••"}}}}}}},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Cliente não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/whatsapp-templates":{"get":{"tags":["Canais"],"summary":"Listar templates HSM","operationId":"listWhatsAppTemplates","description":"Lista os templates HSM (locais) de um canal WhatsApp Oficial. `?channelId=` obrigatório (canal ⊆ conta, senão 404). `?refresh=true` sincroniza com a Meta antes. Escopo: `channels:read`.","parameters":[{"name":"channelId","in":"query","required":true,"schema":{"type":"integer"},"description":"ID do canal (Master) WhatsApp Oficial."},{"name":"refresh","in":"query","required":false,"schema":{"type":"boolean"},"description":"true = puxa a lista fresca da Meta antes de retornar."}],"responses":{"200":{"description":"OK"},"400":{"description":"channelId inválido","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Canal WhatsApp Oficial não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"post":{"tags":["Canais"],"summary":"Criar template HSM","operationId":"createWhatsAppTemplate","description":"Cria e submete à Meta um template HSM a partir de campos AMIGÁVEIS (header/body/footer/buttons → `components`). O status final (APPROVED/REJECTED) chega async por webhook. ⚠️ Header de **MÍDIA** exige um `handle` já resolvido (upload prévio) — via API pública só TEXT/none. NUNCA envie token/segredo no corpo. Escopo: `channels:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhatsAppTemplate"}}}},"responses":{"201":{"description":"Criado (PENDING)"},"400":{"description":"Validação (campos obrigatórios / regras de botões da Meta)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Canal WhatsApp Oficial não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"502":{"description":"A Meta recusou o template (retorna `detail`/`metaResponse`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/whatsapp-templates/sync":{"post":{"tags":["Canais"],"summary":"Sincronizar templates HSM com a Meta","operationId":"syncWhatsAppTemplates","description":"Reconcilia os templates locais com a lista atual da Meta (upsert + marca como DISABLED os removidos remotamente). Corpo: `{ channelId }`. Escopo: `channels:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"channelId":{"type":"integer","description":"ID do canal (Master) WhatsApp Oficial."}},"required":["channelId"]}}}},"responses":{"200":{"description":"OK (`{ success, upserted, markedDisabled }`)"},"400":{"description":"channelId inválido","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Canal WhatsApp Oficial não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/whatsapp-templates/{channelId}/{name}/{language}":{"delete":{"tags":["Canais"],"summary":"Remover template HSM","operationId":"deleteWhatsAppTemplate","description":"Soft-delete do template local + delete na Meta (best-effort). Escopo: `channels:write`.","parameters":[{"name":"channelId","in":"path","required":true,"schema":{"type":"integer"}},{"name":"name","in":"path","required":true,"schema":{"type":"string"}},{"name":"language","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK (`{ success: true }`)"},"400":{"description":"name/language ausentes","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Canal ou template não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/campaigns/{id}":{"get":{"tags":["Campanhas"],"summary":"Detalhe da campanha","operationId":"getCampaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `campaigns:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"put":{"tags":["Campanhas"],"summary":"Atualizar campanha","operationId":"updateCampaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `campaigns:write`. Editável em DRAFT/SCHEDULED (e RUNNING/PAUSED p/ não-enviados). Aceita os mesmos campos do POST (parcial).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Campaign"}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/campaigns/{id}/recipients/{rid}":{"patch":{"tags":["Campanhas"],"summary":"Editar destinatário","operationId":"updateCampaignRecipient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"rid","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `campaigns:write`. Edita nome/variáveis de um destinatário (antes do envio).","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Nome do destinatário."},"vars":{"type":"object","description":"Variáveis por-destinatário usadas no template (${chave}).","example":{"nome":"Ana","valor":"99,90"}}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Campanhas"],"summary":"Remover destinatário","operationId":"removeCampaignRecipient","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"rid","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `campaigns:write`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/campaigns/{id}/validate":{"post":{"tags":["Campanhas"],"summary":"Validar (preflight, sem disparar)","operationId":"validateCampaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `campaigns:read`. Retorna `{ ok, errors[], recipientCount }` (HSM/Official, mídia, canal, destinatários).","responses":{"200":{"description":"Resultado do preflight"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/campaigns/{id}/pause":{"post":{"tags":["Campanhas"],"summary":"Pausar (RUNNING→PAUSED)","operationId":"pauseCampaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `campaigns:write`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Transição inválida","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/campaigns/{id}/resume":{"post":{"tags":["Campanhas"],"summary":"Retomar (PAUSED→RUNNING)","operationId":"resumeCampaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `campaigns:write`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Transição inválida","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/campaigns/{id}/cancel":{"post":{"tags":["Campanhas"],"summary":"Cancelar","operationId":"cancelCampaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `campaigns:write`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Transição inválida","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/campaigns/{id}/test":{"post":{"tags":["Campanhas"],"summary":"Envio de teste (1 número)","operationId":"testCampaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `campaigns:write`. Body `{ number }`. Renderiza preview + envia ao número (delegado ao PRIMARY). Não afeta contadores.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"number":{"type":"string"}}}}}},"responses":{"200":{"description":"{ preview, missing, delegated }"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/automations":{"get":{"tags":["Automações"],"summary":"Listar automações","operationId":"listAutomations","description":"Lista as automações agendadas (não-excluídas) da conta, com `flow` (`{ id, name }`). Escopo: `automations:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["Automações"],"summary":"Criar automação","operationId":"createAutomation","description":"Cria uma automação agendada (cron) que executa um fluxo HEADLESS. `cronExpression` é avaliada no relógio de Brasília (GMT-3); o próximo disparo (`nextRunAt`) é pré-computado quando `enabled` + cron válido. `flowId`/`channelId` são validados ⊆ conta (→404). Escopo: `automations:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Automation"}}}},"responses":{"201":{"description":"Criada"},"400":{"description":"Validação (nome/cron inválidos) ou codificação inválida (`code: INVALID_ENCODING`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Fluxo/canal não encontrado na conta","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/automations/{id}":{"get":{"tags":["Automações"],"summary":"Detalhe da automação","operationId":"getAutomation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `automations:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"put":{"tags":["Automações"],"summary":"Atualizar automação","operationId":"updateAutomation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Edição parcial. Recalcula o próximo disparo (`nextRunAt`) se `cronExpression` ou `enabled` mudarem. Escopo: `automations:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Automation"}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação (nome/cron inválidos) ou codificação inválida (`code: INVALID_ENCODING`)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrada (ou fluxo/canal fora da conta)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}},"delete":{"tags":["Automações"],"summary":"Excluir automação (soft-delete)","operationId":"deleteAutomation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Soft-delete: marca `deletedAt`, desabilita o agendamento e limpa `nextRunAt`. Escopo: `automations:write`.","responses":{"200":{"description":"OK (`{ ok: true }`)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/automations/{id}/run":{"post":{"tags":["Automações"],"summary":"Disparar manualmente","operationId":"runAutomation","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Disparo MANUAL imediato (fire-and-forget): publica no bus; o servidor PRIMARY (dispatcher) cria o run e executa o fluxo. Não bloqueia. Escopo: `automations:execute`.","responses":{"200":{"description":"Disparo publicado (`{ ok: true }`)"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/automations/{id}/runs":{"get":{"tags":["Automações"],"summary":"Histórico de execuções","operationId":"listAutomationRuns","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Lista as últimas 50 execuções (append-only) da automação: `trigger` (SCHEDULED|MANUAL), `status` (RUNNING|SUCCESS|FAILED), `startedAt`/`finishedAt`, `ticketId` (ticket-sistema sintético), `error`/`log`. Escopo: `automations:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/ai/models/{id}/test":{"post":{"tags":["IA"],"summary":"Testar modelo (probe)","operationId":"testAIModel","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `ai:write`. Faz uma chamada de teste ao endpoint do modelo.","responses":{"200":{"description":"Resultado do teste"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/ai/models/{id}/profiles":{"get":{"tags":["IA"],"summary":"Profiles vinculáveis ao modelo","operationId":"listAIModelProfiles","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `ai:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/ai/models/{id}/profile-links":{"put":{"tags":["IA"],"summary":"Definir vínculos profile↔modelo","operationId":"setAIModelProfileLinks","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Define quais perfis de inferência (AIProfile) o modelo aplica e como. Escopo: `ai:write`. A conta nunca cria vínculo `fixed` (só `overridable`/`optional`). Substitui o conjunto de vínculos do modelo.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["links"],"properties":{"links":{"type":"array","description":"Lista de vínculos a aplicar (substitui os atuais).","items":{"type":"object","properties":{"profileId":{"type":"integer","description":"ID do AIProfile (⊆ conta)."},"kind":{"type":"string","description":"overridable | optional (a conta não cria fixed).","example":"overridable"},"priority":{"type":"integer","description":"Ordem de aplicação no merge (asc; maior vence conflito).","example":200}},"required":["profileId","kind"]}}},"example":{"links":[{"profileId":3,"kind":"overridable","priority":200}]}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Modelo não encontrado","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}},"/ai/categories":{"get":{"tags":["IA"],"summary":"Listar categorias de IA","operationId":"listAICategories","description":"Escopo: `ai:read`. `?includeInactive=true` traz inativas.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"tags":["IA"],"summary":"Criar/atualizar categoria (upsert)","operationId":"upsertAICategory","description":"Escopo: `ai:write`.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AICategory"}}}},"responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/ai/rules/{id}/usage":{"get":{"tags":["IA"],"summary":"Onde a regra é usada (fluxos)","operationId":"ruleUsage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `ai:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/ai/tools/{id}/usage":{"get":{"tags":["IA"],"summary":"Onde a ferramenta é usada","operationId":"toolUsage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `ai:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/ai/profiles/{id}/usage":{"get":{"tags":["IA"],"summary":"Onde o perfil é usado","operationId":"profileUsage","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}}],"description":"Escopo: `ai:read`.","responses":{"200":{"description":"OK"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/tags/merge":{"post":{"tags":["Gestão de conta"],"summary":"Fundir tags","operationId":"mergeTags","description":"Escopo: `account:write`. Funde `sourceIds[]` em `targetId` (reatribui tickets). Origem/destino ⊆ conta.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["sourceIds","targetId"],"properties":{"sourceIds":{"type":"array","items":{"type":"integer"}},"targetId":{"type":"integer"}}}}}},"responses":{"200":{"description":"OK"},"400":{"description":"Validação, ou corpo com codificação inválida (`code: INVALID_ENCODING`) — reenvie em UTF-8","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Tag destino não encontrada","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"array","items":{"type":"string"}},"code":{"type":"string","description":"Código legível por máquina do erro, quando aplicável (ex.: `INVALID_ENCODING`)."}},"required":["error"]}}}}}}}}}