@lionchat/mcp-server 1.12.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,499 @@
1
+ # Formulários Públicos — Guia Profundo
2
+
3
+ Tudo sobre os Formulários Públicos do LionChat (captação estilo Typeform/Typebot): link público,
4
+ caixa de WhatsApp vinculada, desenho dos blocos, publicação, respostas, funil, simulador e
5
+ integração com fluxos. Atualizado em 18/08/2026 (v4.10.226).
6
+
7
+ ## O que é
8
+
9
+ Um formulário público é uma página de captação hospedada pelo próprio LionChat. O visitante abre um
10
+ link, responde bloco a bloco, e o LionChat cria o contato, guarda as respostas, abre a conversa na
11
+ caixa de WhatsApp vinculada e (opcionalmente) dispara um fluxo do FlowBuilder no fim.
12
+
13
+ - **Link público**: `{FRONTEND_URL}/forms/<referência da conta>/<slug>` — a referência da conta é o
14
+ `booking_public_slug` (o mesmo da agenda pública). O `slug` do formulário nasce sorteado (8
15
+ caracteres) e **pode ser trocado** (ver Campos). O campo `public_url` já vem pronto na resposta.
16
+ - **Caixa de WhatsApp vinculada é OBRIGATÓRIA para publicar** (`inbox_id`). Só canais
17
+ `Channel::Whatsapp` (oficial) e `Channel::Waha` (QR Code). É por ela que o bloco Enviar mensagem
18
+ dispara e que a conversa do lead nasce.
19
+ - **Dois formatos de exibição** (`display_mode`): `cards` (um bloco por tela, estilo Typeform) ou
20
+ `chat` (bolhas de conversa, estilo Typebot).
21
+ - **Feature flag `lead_forms`, por conta, nasce desligada.** Com a flag desligada a **página pública
22
+ responde 404** — ninguém consegue preencher. As tools de dashboard (listar, criar, editar,
23
+ publicar, ver respostas, simular) **funcionam mesmo assim**. Dá pra montar tudo antes de ligar a
24
+ flag; não adianta divulgar o link antes.
25
+ - **Escrita é só de administrador.** `create`, `update`, `destroy`, `publish`, `duplicate` e
26
+ `check_embed` passam por `check_admin_authorization?`; usuário não-admin recebe **401**. Leitura
27
+ (`list`, `show`, `stats`, `responses`) e o **simulador** (`test_run`) são liberados a qualquer
28
+ agente da conta.
29
+
30
+ ### Não confundir
31
+
32
+ | Nome parecido | O que é de verdade |
33
+ |---|---|
34
+ | **Formulário público** (este documento) | Página de captação hospedada pelo LionChat, com blocos e link próprio |
35
+ | **MetaLeadForm** | Formulário de anúncio da Meta (Facebook/Instagram Lead Ads) — outro módulo |
36
+ | **Flow / FlowBuilder** | Motor de automação de conversa. O formulário *dispara* fluxos, mas não é um fluxo |
37
+
38
+ ## Ferramentas disponíveis
39
+
40
+ | Ferramenta | Endpoint | O que faz |
41
+ |---|---|---|
42
+ | `lionchat_lead_forms_list` | `GET /lead_forms` | Lista os formulários da conta (mais novos primeiro). Não traz `media_urls` |
43
+ | `lionchat_lead_forms_create` | `POST /lead_forms` | Cria. Admin-only |
44
+ | `lionchat_lead_forms_show` | `GET /lead_forms/:id` | Detalhe completo, único lugar que traz `media_urls` |
45
+ | `lionchat_lead_forms_update` | `PATCH /lead_forms/:id` | Edita (inclusive `inbox_id` e `slug`). Admin-only |
46
+ | `lionchat_lead_forms_destroy` | `DELETE /lead_forms/:id` | Exclui (204 sem corpo). Admin-only |
47
+ | `lionchat_lead_forms_publish` | `POST /lead_forms/:id/publish` | Publica: tira a foto do desenho (com a caixa dentro). Admin-only |
48
+ | `lionchat_lead_forms_duplicate` | `POST /lead_forms/:id/duplicate` | Cópia completa com slug novo (copia o `inbox_id`). Admin-only |
49
+ | `lionchat_lead_forms_stats` | `GET /lead_forms/:id/stats` | Números e funil por bloco |
50
+ | `lionchat_lead_forms_responses_list` | `GET /lead_forms/:id/responses` | Lista de respostas (leve, sem as respostas em si) |
51
+ | `lionchat_lead_forms_responses_show` | `GET /lead_forms/:id/responses/:rid` | Detalhe de uma resposta, com o que a pessoa respondeu |
52
+ | `lionchat_lead_forms_test_run` | `POST /lead_forms/:lead_form_id/test_runs` | Simulador: roda o RASCUNHO no motor real, sem deixar rastro. Qualquer agente da conta |
53
+ | `lionchat_lead_forms_check_embed` | `POST /lead_forms/check_embed` | Confere se um site aceita ser exibido em iframe (bloco Página externa). Admin-only |
54
+
55
+ **Imagens não têm ferramenta MCP.** O upload de imagem de bloco (`POST /lead_forms/:id/media`,
56
+ máximo 5 MB, só `image/*`) existe no painel. Pela API dá pra referenciar um `image` que já foi
57
+ subido, nunca subir um novo.
58
+
59
+ ## Campos do formulário
60
+
61
+ | Campo | Tipo | Observação |
62
+ |---|---|---|
63
+ | `name` | string | **Obrigatório** |
64
+ | `display_mode` | string | `cards` (padrão) ou `chat` |
65
+ | `active` | boolean | Formulário inativo devolve 404 na página pública |
66
+ | `inbox_id` | integer | Caixa de WhatsApp vinculada. Obrigatória para publicar. Ver regras abaixo |
67
+ | `form_data` | jsonb | O desenho (rascunho). Teto de 2 MB serializado |
68
+ | `settings` | jsonb | Ver abaixo |
69
+ | `slug` | string | **Editável** em create e update. Normalizado no servidor (acento vira letra, resto vira hífen), 3 a 60 caracteres, minúsculas + números + hífen, único dentro da conta. Vazio/só espaços = mantém o atual (não é erro). Duplicado = 422. Ausente no create = sorteia 8 caracteres |
70
+ | `published_at` | timestamp | Quando a foto foi tirada. Nulo = nunca publicado |
71
+ | `public_url` | string | Montado pelo servidor |
72
+ | `counts` | objeto | `{views, responses, completed}` — contadores do banco |
73
+
74
+ **A resposta de `show`/`list`/`create`/`update` traz também:** `inbox` (`{id, name, channel_type}`
75
+ ou `null` — a caixa resolvida), e `published_inbox_id` (a caixa que está NA FOTO publicada).
76
+
77
+ ### Regras do `inbox_id`
78
+
79
+ - Caixa que não é da conta → 422 `{errors: {inbox_id: ["not found in this account"]}}`.
80
+ - Canal fora de `Channel::Whatsapp`/`Channel::Waha` → `["channel not supported"]`.
81
+ - **Trocar de caixa pode; trocar de TIPO de canal não** → `["channel type cannot change"]`.
82
+ Os blocos foram montados pro canal atual (oficial exige modelo aprovado; QR aceita texto livre e
83
+ mídia). Formulário que ainda não tinha caixa escolhe livremente.
84
+ - **A troca só vale depois de republicar**: `publish` grava o `inbox_id` DENTRO da foto
85
+ (`published_data`), e o motor lê sempre a foto.
86
+
87
+ ### `settings`
88
+
89
+ | Chave | Valor | Padrão |
90
+ |---|---|---|
91
+ | `primary_color` | hex | `#7C3AED` |
92
+ | `theme` | `light` ou `dark` (valor fora da lista cai em `light`) | `light` |
93
+ | `abandon_minutes` | inteiro, limitado a 1..15 (minutos) | 15 |
94
+ | `button_label` | string — rótulo do botão da página pública | — |
95
+ | `resume` | booleano — continuar de onde parou entre visitas | `true` |
96
+ | `chat_avatar` | id numérico de blob já subido (`media`) — foto redonda ao lado dos balões, só no formato `chat`. O público recebe `chat_avatar_url` resolvida (nunca o id) | — |
97
+
98
+ ## Ciclo de vida
99
+
100
+ 1. **Criar** com `lionchat_lead_forms_create` (nome + `form_data`, mesmo que mínimo).
101
+ 2. **Vincular a caixa de WhatsApp** (`inbox_id`) — sem ela a publicação recusa (`form_without_inbox`).
102
+ 3. **Montar o desenho** em `form_data` (contrato na próxima seção). Todo formulário precisa de uma
103
+ pergunta com `map_to: contact.phone` (`form_requires_phone_question`).
104
+ 4. **Testar** com `lionchat_lead_forms_test_run` — roda o rascunho de verdade, sem sujar nada.
105
+ 5. **Publicar** com `lionchat_lead_forms_publish` — copia `form_data` (+ `inbox_id`) para
106
+ `published_data` e carimba `published_at`.
107
+ 6. **Ativar** (`active: true`) e ligar a feature flag `lead_forms` da conta.
108
+ 7. **Divulgar** o `public_url`.
109
+
110
+ **Quem preenche usa SEMPRE a foto publicada, nunca o rascunho.** Editar `form_data` não muda nada
111
+ para quem está preenchendo até um novo `publish`.
112
+
113
+ **O contato nasce em TRÊS momentos**: quando a pessoa conclui, quando a resposta é marcada como
114
+ abandonada (padrão e teto de 15 minutos sem atividade), ou logo antes de um bloco de Enviar
115
+ WhatsApp. Enquanto isso nada é criado — nem contato, nem conversa, nem card. Efeitos que dependem de
116
+ contato (evento de pixel, gatilho de marco) ficam guardados e disparam no nascimento, com a hora
117
+ original de cada etapa. Quando o contato nasce ali, a conversa começa com uma pílula dizendo qual
118
+ formulário o originou. Contato apagado no painel solta o vínculo — a resposta pode renascer no
119
+ próximo momento. Os textos `{{contact.name}}`/`{{contact.phone}}`/`{{contact.email}}` saem do que a
120
+ pessoa RESPONDEU, não da ficha.
121
+
122
+ ### Link identificado (pula perguntas já conhecidas)
123
+
124
+ Toda mensagem de TEXTO enviada numa conversa que contenha um link `/forms/` sai **carimbada
125
+ automaticamente** com `?lt_form=<código>` (sem depender de LionTrack; vale pra mensagem de flow e
126
+ de IA também; nunca em grupo). Quem abre o link carimbado tem a resposta ligada ao contato desde o
127
+ início, e o formulário **PULA as perguntas de identidade que já têm valor** — só as três nativas:
128
+ nome, telefone e e-mail. A resposta pulada é gravada com o valor da ficha e o selo `prefilled`.
129
+
130
+ Seguranças: o valor pulado **nunca aparece na tela nem no payload público**, e o botão Voltar **não
131
+ alcança** a pergunta pulada. Código de outra conta, contato apagado ou qualquer pane = o formulário
132
+ roda anônimo normal. Link copiado do editor e colado fora de uma conversa NÃO tem carimbo.
133
+
134
+ ## O contrato do `form_data`
135
+
136
+ ```json
137
+ {
138
+ "nodes": [
139
+ { "id": "n1", "type": "question", "position": { "x": 0, "y": 200 }, "data": { } }
140
+ ],
141
+ "edges": [
142
+ { "id": "e1", "source": "n1", "target": "n2", "sourceHandle": "success" }
143
+ ]
144
+ }
145
+ ```
146
+
147
+ - `id` de nó e de aresta: qualquer string única dentro do desenho.
148
+ - `position`: `{x, y}` em pixels do canvas. Só afeta a visualização no editor; o motor ignora.
149
+ - `sourceHandle`: **qual saída do nó** a aresta usa. É o que decide o caminho (ver tabela).
150
+
151
+ ### Tipos de bloco
152
+
153
+ | `type` | Chaves de `data` permitidas | Saídas (`sourceHandle`) | Aparece pro lead? |
154
+ |---|---|---|---|
155
+ | `start` | `title, description, button_label, image, video_url` | `success` | sim |
156
+ | `question` | `label, description, field_type, placeholder, required, map_to, scale, image, video_url, accept, invalid_message` | `success` | sim |
157
+ | `choice` | `label, description, options, multiple, map_to` | `option_<id da opção>` (escolha única) / `success` (múltipla) | sim |
158
+ | `message` | `title, description, button_label, image, video_url` | `success` | sim |
159
+ | `condition` | `branches` | `cond_<id do ramo>` + `else` | não |
160
+ | `action` | `items, label` (+ formato antigo `action_type, labels, name, event_name`) | `success` | não |
161
+ | `send_message` | `messageItems` (+ formato antigo `content, template_name, template_params`) | `success`, `skipped` | não |
162
+ | `booking` | `label, description, event_type_id, required` | `success` (agendou) + `no_booking` (seguiu sem agendar; some quando `required`) | sim |
163
+ | `set_variable` | `label, assignments` | `success` | não |
164
+ | `embed` | `title, description, url, height, button_label, embed_allowed, show_open_external` | `success` | sim |
165
+ | `end` | `title, description, redirect_url, button_label, image, video_url` | — | sim |
166
+ | `api_request` | `label, method, url, headers, body, timeout` | `success`, `error` | não (mostra bloco de espera) |
167
+ | `ai` | `label, mode, prompt, categories, include_answers, model` | modo `generate`: `success`, `error` / modo `classify`: um `cat_<id>` por categoria + `other` | não (mostra bloco de espera) |
168
+
169
+ **Chave fora da lista não é lida pelo motor** e o editor a **remove no próximo salvamento pela
170
+ tela**. Escrever um campo inventado pela API não gera erro nenhum — ele simplesmente não faz nada e
171
+ depois some. A lista é fonte única (`LeadForm::NODE_DATA_KEYS` no servidor, espelhada em
172
+ `nodeSchema.js` no editor, com spec de contrato comparando as duas).
173
+
174
+ ### Detalhes por chave
175
+
176
+ **`field_type`** (do `question`): `short_text`, `long_text`, `email`, `phone`, `date`, `time`,
177
+ `datetime`, `number`, `rating` (usa `scale`), `url`, `file_upload` (usa `accept`), e os documentos
178
+ com validação de verdade: `cpf`, `cnpj`, `cep` (dígito verificador/8 dígitos; gravam só números;
179
+ erros `invalid_cpf`/`invalid_cnpj`/`invalid_cep`).
180
+ - `time` grava `HH:MM` (24h; erro `invalid_time`).
181
+ - `datetime` grava `AAAA-MM-DDTHH:MM` — hora local de quem preencheu, SEM fuso; o fuso de leitura é
182
+ o da definição do atributo de destino (erro `invalid_datetime`).
183
+
184
+ **`video_url`** (em `start`, `question`, `message`, `end`): link de YouTube, Vimeo ou arquivo de
185
+ vídeo direto. Com vídeo válido, ele ocupa o lugar da imagem. Link fora do formato conhecido não
186
+ renderiza e não dá erro.
187
+
188
+ **`accept`** (só em `file_upload`): `'any'` ou um array de categorias entre `image`, `video`,
189
+ `audio`, `pdf`, `spreadsheet`, `document`, `xml`. Arquivo executável é recusado sempre.
190
+
191
+ **`map_to`** — para onde vai a resposta:
192
+ - `''` — não guarda em lugar nenhum (só em `answers`).
193
+ - `contact.name`, `contact.email`, `contact.phone` — os três nativos.
194
+ - `contact.cadastral.<campo>` — `cpf`, `rg`, `cnpj`, `passport`, `profession`, `marital_status`,
195
+ `gender`, `date_of_birth` (os dois últimos sem tela; funcionam por API e são normalizados:
196
+ "solteira" vira `solteiro`, "masculino" vira `m`).
197
+ - `contact.address.<campo>` — `cep`, `street`, `number`, `complement`, `neighborhood`, `city`,
198
+ `state`, `country`.
199
+ - `custom.<chave>` — atributo personalizado de CONTATO.
200
+ - `conversation_custom.<chave>` — atributo personalizado da CONVERSA (só chave definida na conta;
201
+ coerção de tipo; teto 1500 caracteres).
202
+ - **Lista de recusa no servidor** (o valor é pulado de forma visível, sem erro): `name`, `email`,
203
+ `phone_number`, `identifier`, `card` e chaves começando com `captain_`, `eclinica_`, `origin_`,
204
+ `lt_`, `booking_`, `waha_`, `whatsapp_`, `ctwa_`, `meta_lead_`.
205
+ - Contato pré-existente recebe valor **só em campo vazio** — exceto o que ESTA resposta gravou e
206
+ ninguém mudou depois (correção via Voltar).
207
+
208
+ **`options`** (do `choice`): `[{ "id": "abc123", "label": "Sim", "image": "" }]`.
209
+
210
+ **`branches`** (do `condition`): lista avaliada em ordem; nenhum ramo casando cai no handle `else`.
211
+ Operadores: `equals`, `not_equals`, `contains`, `filled`, `not_filled`, `greater_than`,
212
+ `less_than`. Fonte `api_request` exige `path` (caminho dentro da resposta).
213
+
214
+ **`items`** (do `action`) — o formato novo, uma LISTA de ações `[{key, config}]`:
215
+
216
+ | `key` | `config` mínimo para publicar | O que faz |
217
+ |---|---|---|
218
+ | `update_contact_attribute` | `attr_key` | Grava atributo personalizado do CONTATO |
219
+ | `update_conversation_attribute` | `attr_key` | Grava atributo personalizado da CONVERSA |
220
+ | `add_label` | `labels` com ≥1 título | Etiqueta o contato |
221
+ | `milestone` | `name` | Marco do funil (pode disparar fluxo) |
222
+ | `meta_pixel_event` | `event_name` | Evento Meta CAPI (`[a-zA-Z0-9_]`, até 40) |
223
+ | `ga4_event` | `event_name` | Evento GA4 |
224
+
225
+ O `config` das ações de atributo aceita também `attr_value` (texto com variáveis) e `attr_op`:
226
+ `set` (sobrescrever, padrão), `plus`, `minus`, `times`, `divided_by`. **A conta é feita no
227
+ servidor** sobre o valor atual, com trava de repetição (roda UMA vez por preenchimento); valor que
228
+ não vira número não grava; divisão por zero não grava. A operação aritmética é a única que passa
229
+ por cima do "só grava em campo vazio". Identidade (nome/telefone/e-mail) NÃO se grava por aqui.
230
+ Formato antigo (`action_type` + campos soltos) continua sendo lido.
231
+
232
+ **`messageItems`** (do `send_message`) — LISTA de mensagens, enviadas na ordem:
233
+ - `{type: "text", content: "..."}` — texto com variáveis (caixa QR).
234
+ - `{type: "whatsapp_template" | "template", template_params: {name, category, language, namespace,
235
+ processed_params}}` — modelo aprovado (caixa oficial; `language` padrão `pt_BR`).
236
+ - `{type: "image" | "video" | "audio" | "file" | "document" | "attachment" | "url_media",
237
+ url | file_url | blob_signed_id, caption}` — mídia.
238
+ Item que falha **não derruba os outros** — vira `skipped_item {index, reason}` em `answers`.
239
+ A caixa é a DO FORMULÁRIO (`inbox_id`) — o bloco não tem caixa própria. Formato antigo
240
+ (`content`/`template_name`/`template_params` soltos) continua sendo lido.
241
+
242
+ **`booking`** — calendário DENTRO do formulário:
243
+ - `event_type_id` = id de um tipo de evento de Agendamento da conta (ativo).
244
+ - `required: true` = só avança depois de agendar (a saída `no_booking` some do desenho).
245
+ - O payload público leva `booking` (slug público, título, duração, `task_type`, `ask_email`,
246
+ `ask_description`), `prefill` (nome/telefone/e-mail já respondidos) e `missing` (o que o tipo
247
+ exige e o formulário não perguntou — e-mail só quando `task_type` é `video_call`). O id interno
248
+ do tipo NUNCA sai. Tipo apagado/desativado depois de publicado: a tela mostra "indisponível" com
249
+ botão de continuar (o `required` é liberado — ninguém fica preso).
250
+ - O valor gravado na resposta é o id do agendamento criado (validado contra a conta).
251
+
252
+ **`set_variable`** — cria variáveis temporárias da resposta (invisível pro lead):
253
+ - `assignments: [{key, value}]` — até 20 linhas, 1000 caracteres por valor; `key` vira
254
+ `{{var.<key>}}` nos blocos seguintes. Linha com `key` vazia é ignorada.
255
+ - SÓ variável. Gravar em contato/conversa é papel do bloco de Ações (um `target` em um item é
256
+ ignorado pelo motor e descartado pela tela).
257
+
258
+ **`embed`** — "Página externa", um site de fora exibido DENTRO do formulário (checkout, simulador,
259
+ agenda externa):
260
+ - `url` precisa começar com `https://` LITERAL (variável só do meio pra frente). Na montagem, as
261
+ variáveis resolvem por posição, com codificação de URL, e o https é reconferido — URL que não
262
+ for https sai VAZIA do payload (proteção contra script via variável).
263
+ - `embed_allowed` NÃO é configuração sua: é o veredito de `lionchat_lead_forms_check_embed`
264
+ gravado no desenho (`true`/`false`/`null`). Com `false`, a página mostra o aviso educado + botão
265
+ "Abrir em nova página" em vez de um quadrado vazio. Escrever `true` na mão mente pro renderizador.
266
+ - `height` em pixels (padrão 480, limitado 240..1200). `show_open_external` (padrão `true`) mostra
267
+ o botão de abrir fora; com o site recusando o iframe, o botão aparece mesmo desligado — é a
268
+ única saída da pessoa.
269
+ - O bloco avança por botão (a página embutida é caixa fechada — não dá pra saber quando um
270
+ pagamento terminou lá dentro). Checkout é justamente o tipo de página que MAIS recusa iframe.
271
+
272
+ **`api_request`**: `method` aceita `GET`, `POST`, `PUT`, `PATCH`, `DELETE` (padrão `GET`). `body`
273
+ (até 64 KB) aceita variáveis — inclusive `{{conta.<variável>}}`; valor do lead entra escapado em
274
+ corpo JSON. A `url` precisa ter **domínio literal** (variáveis só no caminho e nos valores de
275
+ query; `{{conta.*}}` NÃO vale na URL). `headers` aceita literal ou `{{conta.<variável>}}` —
276
+ variável do lead em cabeçalho é recusada na publicação. `timeout` em segundos: padrão 8, teto 15.
277
+
278
+ **`ai`**: `mode` é `generate` (devolve texto em `{{ia.<node_id>}}`) ou `classify` (roteia por
279
+ categoria). `categories` é obrigatório no modo `classify`. `include_answers` (booleano) manda as
280
+ respostas já dadas como contexto. `model` é validado contra a lista suportada; `''` = modelo padrão
281
+ da conta. A IA usa a **chave da própria conta** — conta sem chave faz o bloco sair pela saída de
282
+ erro.
283
+
284
+ ### Variáveis
285
+
286
+ | Variável | O que traz | Onde vale |
287
+ |---|---|---|
288
+ | `{{answer.<node_id>}}` | O que a pessoa respondeu naquele bloco (rótulo da opção ou texto) | mensagem, prompt, valores do bloco, URL/corpo de consulta |
289
+ | `{{contact.name}}` `{{contact.phone}}` `{{contact.email}}` | O que a pessoa respondeu (a ficha é reserva) | idem |
290
+ | `{{api.<node_id>.<caminho>}}` | Um pedaço da resposta de uma consulta | idem |
291
+ | `{{ia.<node_id>}}` | O texto que a IA gerou naquele bloco | idem |
292
+ | `{{var.<nome>}}` | Variável criada pelo bloco Definir variável | idem |
293
+ | `{{conta.<variável>}}` | Variável da conta (cofre) | cabeçalho e CORPO de `api_request` (com segredo); em `set_variable` e no valor das Ações só as NÃO-secretas |
294
+
295
+ Variável desconhecida vira string vazia, em silêncio. `{{conta.*}}` em texto público sai vazia
296
+ mesmo que exista — é proteção, não defeito.
297
+
298
+ ## Regras de publicação
299
+
300
+ `lionchat_lead_forms_publish` devolve **422 com `{errors: [chaves]}`**. As 34 chaves atuais:
301
+
302
+ | Chave | Significado |
303
+ |---|---|
304
+ | `no_start` | Não existe bloco `start` |
305
+ | `start_without_exit` | O `start` não tem aresta saindo |
306
+ | `no_reachable_end` | Nenhum `end` alcançável a partir do `start` |
307
+ | `form_without_inbox` | O formulário não tem caixa de WhatsApp válida vinculada |
308
+ | `form_requires_phone_question` | Nenhuma pergunta guarda o telefone (`map_to: contact.phone`) — obrigatória em TODO formulário |
309
+ | `question_without_label` | Pergunta sem o texto |
310
+ | `question_without_exit` | Pergunta sem saída |
311
+ | `choice_without_options` | Escolha sem opções |
312
+ | `choice_without_exit` | Escolha sem nenhuma saída ligada |
313
+ | `choice_option_without_edge` | Uma opção específica não leva a lugar nenhum |
314
+ | `condition_without_branches` | Condição sem ramos |
315
+ | `condition_without_else` | Condição sem o "senão" ligado |
316
+ | `action_without_config` | Bloco de ação sem nenhum item completo |
317
+ | `send_message_official_without_template` | Caixa OFICIAL vinculada e o bloco de mensagem sem nenhum modelo |
318
+ | `send_message_qr_without_content` | Caixa QR vinculada e o bloco de mensagem sem nenhum item que entregue algo |
319
+ | `api_without_url` | Consulta sem endereço |
320
+ | `api_without_error_exit` | Consulta sem a saída de erro ligada |
321
+ | `api_url_host_not_literal` | O domínio da consulta tem variável |
322
+ | `api_method_invalid` | `method` fora de GET/POST/PUT/PATCH/DELETE |
323
+ | `unknown_variable_in_url` | A URL usa variável de família desconhecida |
324
+ | `unknown_variable_in_body` | O corpo usa variável de família desconhecida (ou `{{` malformado) |
325
+ | `account_variable_in_url` | A URL usa `{{conta.*}}` (só cabeçalho e corpo aceitam) |
326
+ | `lead_variable_in_header` | Cabeçalho usa variável vinda do lead |
327
+ | `ai_without_prompt` | IA sem instrução |
328
+ | `ai_without_categories` | IA em modo classificar sem categorias |
329
+ | `ai_without_error_exit` | IA (gerar) sem a saída de erro |
330
+ | `ai_without_other_exit` | IA (classificar) sem a saída "outros" |
331
+ | `booking_without_event_type` | Agendamento sem tipo de evento escolhido |
332
+ | `booking_event_type_invalid` | O tipo não é da conta ou está desativado |
333
+ | `booking_without_exit` | Agendamento sem a saída "agendou" ligada |
334
+ | `set_variable_without_assignments` | Definir variável sem nenhuma linha com nome |
335
+ | `set_variable_without_exit` | Definir variável sem saída |
336
+ | `embed_without_url` | Página externa sem endereço |
337
+ | `embed_url_not_https` | O endereço da Página externa não começa com `https://` literal |
338
+
339
+ **Não existem mais** (renomeadas em 16/08): `send_message_without_inbox` →
340
+ `form_without_inbox`; `send_message_requires_phone_question` → `form_requires_phone_question`.
341
+
342
+ ## Tetos e limites
343
+
344
+ | O quê | Limite |
345
+ |---|---|
346
+ | `form_data` serializado | 2 MB |
347
+ | Valor de uma resposta | 4 KB |
348
+ | Passos por requisição (anti-loop) | 50 |
349
+ | `days` em `stats` e `responses` | máximo 90 (valor ≤ 0 vira 30) |
350
+ | Página de `responses` | 25 por página, fixo; `page` até 10.000 |
351
+ | Chamadas de IA | 300 por formulário/dia, 1.000 por conta/dia, 10 por resposta |
352
+ | Consultas (`api_request`) | 2.000 por formulário/dia, 10.000 por conta/dia; corpo até 64 KB |
353
+ | Arquivo enviado pelo lead | 10 MB por arquivo, 10 arquivos por resposta |
354
+ | Imagem de bloco (só pelo painel) | 5 MB, apenas `image/*` |
355
+ | Funil em `stats` | 50.000 respostas (acima disso o funil sai parcial) |
356
+ | Itens do bloco de Ações / linhas do Definir variável | 20 cada; 1000 caracteres por valor |
357
+ | Respostas por chamada do simulador | 60 |
358
+ | Altura da Página externa | 240..1200 px |
359
+ | Conferência de embed | 5 s por salto, até 3 redirecionamentos |
360
+
361
+ ## Simulador (`lionchat_lead_forms_test_run`)
362
+
363
+ Roda o **rascunho** (`form_data`, não a foto) no motor de verdade e desfaz tudo no fim — nada fica
364
+ no banco: sem contato, sem conversa, sem WhatsApp, sem pixel, sem resposta nos relatórios. Não
365
+ exige admin. O estado do teste vive em quem chama: cada chamada reproduz o teste inteiro a partir
366
+ da lista de respostas.
367
+
368
+ - Body: `{answers: [{node_id, value}]}` — até 60 itens; vazio = começa do início. Item cujo
369
+ `node_id` não é o bloco atual PARA a reprodução ali (devolve o bloco onde parou).
370
+ - Resposta 200: `{block, log, answers, variables, form}` — `block` é o mesmo envelope da página
371
+ pública; `log` é o caminho percorrido (`[{node_id, type, label, answer, detail}]` — o `detail`
372
+ mostra variável criada, ações executadas, resultado de consulta/IA e o rumo das condições).
373
+ - 422: `{error: "invalid_value", reason}` (uma resposta reprovada na validação) ou
374
+ `{error: "test_run_failed", reason}`.
375
+
376
+ ## Respostas e funil
377
+
378
+ **`lionchat_lead_forms_stats`** (`days` opcional) devolve views/starts/completed/abandoned/
379
+ completion_rate + `funnel` por bloco + `milestones`. O funil lista os blocos que a pessoa vê
380
+ (`start`, `question`, `choice`, `message`, `booking`, `embed`, `end`), na ordem do desenho
381
+ **publicado**.
382
+
383
+ **`lionchat_lead_forms_responses_list`** aceita `status` (`in_progress`, `completed`,
384
+ `abandoned`), `days` e `page`. A lista nunca traz as respostas em si — só `{id, status, contact,
385
+ current_node_label, answered_count, created_at, completed_at}`. **`contact` pode vir
386
+ `{id: null, name, phone, pending: true}`** — é quem ainda está preenchendo (o contato só nasce nos
387
+ três momentos); nome e telefone saem do que a pessoa já digitou.
388
+
389
+ **`lionchat_lead_forms_responses_show`** traz o conteúdo:
390
+
391
+ - `answers`: `[{label, value_label, at, download_url?}]`. **Atenção:** o bloco de Ações grava
392
+ entradas SINTÉTICAS com chave `"<node_id>::<n>"` — nem toda chave de `answers` é id de bloco.
393
+ - `conversation`: `{id, display_id}` ou `null` — a conversa aberta na caixa vinculada.
394
+ - `milestones`: os marcos atingidos.
395
+ - `effects`: resultado de consulta/IA — `{node_id, label, state, error, status, url_short, at}`.
396
+ `url_short` é esquema + host + caminho, sem a query; corpo, cabeçalhos e prompt nunca saem.
397
+ - `utm_params`: de onde o lead veio.
398
+
399
+ Motivos de erro em `effects`: `http_<código>`, `timeout`, `ssrf_blocked`, `invalid_url`,
400
+ `result_too_large`, `stale`, `busy`, `budget_exceeded`, `no_ai_key`, `ai_error`,
401
+ `unresolved_variable`.
402
+
403
+ ## Conferência de iframe (`lionchat_lead_forms_check_embed`)
404
+
405
+ Body `{url}`. Resposta `{status, reason}`:
406
+
407
+ | `status` | `reason` | Significado |
408
+ |---|---|---|
409
+ | `allowed` | — | O site aceita ser exibido dentro do formulário |
410
+ | `blocked` | `not_https` | URL não começa com https:// |
411
+ | `blocked` | `x_frame_options` / `frame_ancestors` | O site declara que recusa iframe |
412
+ | `blocked` | `url_blocked` | Endereço bloqueado por segurança (rede interna etc.) |
413
+ | `unknown` | `unreachable` / `http_<código>` | Não deu pra conferir — a página tenta o iframe e o botão de abrir fora cobre |
414
+
415
+ O veredito deve ser gravado em `embed_allowed` do bloco. Sites de checkout são os que mais recusam.
416
+
417
+ ## Receitas
418
+
419
+ ### 1. Formulário simples que já publica
420
+
421
+ Início, nome, telefone, fim — E a caixa vinculada:
422
+
423
+ ```json
424
+ {
425
+ "nodes": [
426
+ { "id": "start", "type": "start", "position": { "x": 0, "y": 0 },
427
+ "data": { "title": "Fale com a gente", "description": "Leva 30 segundos.",
428
+ "button_label": "Começar" } },
429
+ { "id": "q_nome", "type": "question", "position": { "x": 0, "y": 200 },
430
+ "data": { "label": "Qual é o seu nome?", "field_type": "short_text",
431
+ "required": true, "map_to": "contact.name" } },
432
+ { "id": "q_fone", "type": "question", "position": { "x": 0, "y": 400 },
433
+ "data": { "label": "Qual é o seu WhatsApp?", "field_type": "phone",
434
+ "required": true, "map_to": "contact.phone" } },
435
+ { "id": "fim", "type": "end", "position": { "x": 0, "y": 600 },
436
+ "data": { "title": "Obrigado!", "description": "Já falamos com você." } }
437
+ ],
438
+ "edges": [
439
+ { "id": "e1", "source": "start", "target": "q_nome", "sourceHandle": "success" },
440
+ { "id": "e2", "source": "q_nome", "target": "q_fone", "sourceHandle": "success" },
441
+ { "id": "e3", "source": "q_fone", "target": "fim", "sourceHandle": "success" }
442
+ ]
443
+ }
444
+ ```
445
+
446
+ Mande isso em `lionchat_lead_forms_create` junto com `name` e um `inbox_id` de caixa WhatsApp,
447
+ depois `lionchat_lead_forms_publish` e `lionchat_lead_forms_update` com `active: true`.
448
+
449
+ ### 2. Consultar o CEP e ramificar pela cidade
450
+
451
+ Igual à receita clássica: pergunta o CEP, consulta o ViaCEP (`method` pode ficar de fora — GET é o
452
+ padrão) e ramifica com `path` no ramo cuja fonte é a consulta. Obrigatórios: saída `error` ligada,
453
+ `else` ligado e o `path`.
454
+
455
+ ### 3. Triagem por IA
456
+
457
+ `mode: "classify"` + `categories` + arestas `cat_<id>` e `other`. Inalterado.
458
+
459
+ ### 4. Disparar um fluxo quando o formulário for concluído
460
+
461
+ O gatilho mora no **fluxo**: `{ "key": "lead_form_completed", "lead_form_id": 12 }` no bloco de
462
+ início do flow. Três gatilhos: `lead_form_completed`, `lead_form_milestone` (aceita
463
+ `milestone_node_id`), `lead_form_abandoned`. O `lead_form_id` pode ir no topo do item ou em
464
+ `config`. **O gatilho só dispara depois que o contato nasce** — antes fica guardado. O fluxo
465
+ recebe `form_name`, `form_kind`, `form_milestone` e uma variável `form_<pergunta>` por resposta.
466
+
467
+ ### 5. Agendamento dentro do formulário
468
+
469
+ ```json
470
+ { "id": "agenda", "type": "booking", "position": { "x": 0, "y": 500 },
471
+ "data": { "label": "Escolha um horário", "event_type_id": "34", "required": false } }
472
+ ```
473
+
474
+ Ligue `success` (obrigatória) pro caminho de quem agendou e `no_booking` pro de quem seguiu sem
475
+ agendar (com `required: true` essa saída deixa de existir). O tipo de evento vem de
476
+ `lionchat_booking_event_types_list`.
477
+
478
+ ## Armadilhas
479
+
480
+ - **O `slug` é EDITÁVEL** (mudou em 16/08). Trocar o slug derruba os links já divulgados —
481
+ inclusive os já enviados em mensagens. Vazio mantém o atual; duplicado dá 422.
482
+ - **`inbox_id` só troca por caixa do MESMO tipo de canal** (`channel type cannot change`), e a
483
+ troca só vale ao republicar.
484
+ - **`published_data` nunca sai pela API.** Para saber o que está no ar, olhe `published_at` e
485
+ `published_inbox_id`.
486
+ - **`form_data` no update SUBSTITUI o desenho inteiro.** Não há merge. Leia com `show`, altere e
487
+ mande o desenho completo de volta.
488
+ - **A cópia nasce desligada e não publicada** — e copia o `inbox_id`. Imagens viram blobs novos.
489
+ - **Publicar não ativa.** `publish` (foto) e `update` com `active: true` são dois passos.
490
+ - **Flag desligada = página 404.** Tools funcionando + link 404 = feature `lead_forms` desligada.
491
+ - **`media_urls` só existe no `show`.**
492
+ - **`answers` tem chaves sintéticas** `"<node_id>::<n>"` (bloco de Ações) — não são ids de bloco.
493
+ - **`embed_allowed` é veredito do servidor** (`check_embed`) — escrever `true` na mão faz a página
494
+ tentar um iframe que pode estar bloqueado.
495
+ - **Nunca escreva segredo em cabeçalho/corpo literal** de `api_request` — use
496
+ `{{conta.<variável>}}`, que resolve só no servidor.
497
+ - **Não há tool de rewind**: o "Voltar e corrigir" é da página pública (o lead corrige a própria
498
+ resposta; telefone corrigido reencaminha a mensagem pro número certo). Pela API de dashboard,
499
+ respostas são somente leitura.
@@ -70,6 +70,10 @@ Glossário completo de termos, status codes, enums e conceitos da plataforma. Us
70
70
  - `private`: `true` = nota privada (só visível pra equipe, não pro cliente)
71
71
  - `sender_type`: `User`, `Contact`, `Captain::Assistant`, `AgentBot`
72
72
  - `processed_message_content`: texto após processamento (com markdown removido, links resolvidos)
73
+ - `content_attributes.forwarded`: `true` = a mensagem foi ENCAMINHADA pelo cliente (nos dois canais
74
+ de WhatsApp — oficial e QR Code). A bolha mostra o selo "Encaminhada"
75
+ - `content_attributes.frequently_forwarded`: `true` = encaminhada MUITAS vezes (o "encaminhada com
76
+ frequência" do WhatsApp). Útil pra responder "essa mensagem foi encaminhada?" sem chutar
73
77
 
74
78
  ## Attachment (Anexo)
75
79
 
@@ -93,6 +97,13 @@ Glossário completo de termos, status codes, enums e conceitos da plataforma. Us
93
97
  - `file_size`: bytes
94
98
  - `width`/`height`: pixels (imagens/vídeos)
95
99
 
100
+ ### Tipo de arquivo não reprova mais (15/08/2026)
101
+
102
+ No anexo de **mensagem**, QUALQUER extensão é aceita — a validação é só de TAMANHO. Tipo sem teto
103
+ próprio cai no teto de "documento" (o maior). O `file_type` `file` cobre tudo que não é
104
+ imagem/áudio/vídeo. **Dois caminhos continuam com lista fechada de tipos permitidos**: o upload por
105
+ URL (o teste de mídia do FlowBuilder) e o chat interno da equipe — não confunda os três.
106
+
96
107
  ## Contact (Contato)
97
108
 
98
109
  ### Campos importantes
@@ -300,6 +311,12 @@ comportamento intencional.
300
311
  - `start_time`, `end_time`: ISO 8601
301
312
  - `status`: `scheduled`, `cancelled`, `completed`
302
313
 
314
+ **A IA agenda EXCLUSIVAMENTE por agenda de Booking (15/08/2026).** Não existe mais agendamento
315
+ avulso/direto pela IA — ela consulta as agendas liberadas, respeita os dias em que a agenda não
316
+ abre e só então cria a reserva. Depois de agendar, ela mesma entrega o link do Google Meet quando o
317
+ tipo de agendamento gera Meet (o link nasce alguns segundos depois; o sistema espera e volta pra
318
+ mandar). Detalhes em `lionchat://docs/best-practices`.
319
+
303
320
  ## Métricas (Reports)
304
321
 
305
322
  ### Endpoints `lionchat_reports_*` retornam métricas em SEGUNDOS por padrão
@@ -406,7 +423,7 @@ Campos MUTÁVEIS (atualizáveis livremente por IA/integração): `marital_status
406
423
  |---|---|---|---|
407
424
  | **LionCalls** | Voz pelo WhatsApp em caixa **QR Code**, mas pelo **motor próprio do LionChat** (credencial global, 1 sessão por caixa; flag `lioncalls_calling`, nasce desligada e é ligada conta a conta) | **NENHUMA — sem tool no MCP** | WhatsApp QR Code |
408
425
  | **WhatsApp Calling (Cloud)** | Voz pelo WhatsApp na **API oficial** (enterprise) | `whatsapp_calls_*` + enable/disable_whatsapp_calling | WhatsApp oficial |
409
- | **VoIP (Zenvia)** | Telefonia COMUM com softphone no navegador (ramais, saldo, recarga) | `voip_*` | Telefone |
426
+ | **VoIP (Zenvia)** | Telefonia COMUM com softphone no navegador (ramais, ligações) | `voip_*` | Telefone |
410
427
  | **VTCall** | Telefonia por **PABX click-to-call**, config por conta + ramal por atendente; **sem softphone no navegador** — o atendente fala pelo app/ramal do VTCall | `vtcall_settings_*` (show/update/test_connection), `vtcall_ramals_*` (list/create/destroy) | Telefone (PABX do cliente) |
411
428
 
412
429
  Quando o usuário falar "ligação", descubra o canal ANTES de responder: caixa QR Code → **LionCalls**;
@@ -460,3 +477,34 @@ cards daquele funil).
460
477
  A plataforma opera com 6 idiomas: `en`, `es`, `fr`, `it`, `pt`, `pt_BR` (default das contas
461
478
  brasileiras: pt_BR). Conta com idioma antigo/descontinuado cai em inglês. Notas internas geradas
462
479
  pela IA saem no idioma da conta.
480
+
481
+ ## LeadForm (Formulário público de captação)
482
+
483
+ Formulário de captura próprio da conta, com página pública (link `/forms/<conta>/<slug>`), montado
484
+ num construtor de blocos. Feature liberada conta a conta.
485
+
486
+ ### `LeadFormResponse.status`
487
+ | Valor | Significado |
488
+ |---|---|
489
+ | `in_progress` | O lead começou e ainda está preenchendo |
490
+ | `completed` | Chegou ao fim do formulário |
491
+ | `abandoned` | Parou no meio e a janela de abandono do formulário venceu |
492
+
493
+ O contato nasce em TRÊS momentos (mudou em 16/08): conclusão, fechamento do abandono, ou logo
494
+ antes de um bloco de Enviar WhatsApp. Resposta `abandoned` ganha o contato no fechamento do
495
+ abandono — com as respostas que a pessoa deu. Enquanto preenche, a lista de respostas mostra
496
+ `contact: {id: null, ..., pending: true}`.
497
+
498
+ Termos do formulário (18/08):
499
+ - **Caixa vinculada** — a caixa de WhatsApp do formulário (`inbox_id`); obrigatória pra publicar.
500
+ - **Link identificado** — link de formulário enviado numa conversa sai carimbado (`lt_form`) e o
501
+ formulário pula nome/telefone/e-mail já conhecidos, sem nunca exibir o dado.
502
+ - **Simulador** — `lead_forms_test_run`: roda o rascunho no motor real sem deixar rastro.
503
+ - **Página externa (`embed`)** — bloco que exibe um site de fora dentro do formulário (checkout,
504
+ simulador, agenda), com conferência prévia de "aceita iframe?" (`check_embed`).
505
+ - **Definir variável (`set_variable`)** — bloco invisível que cria `{{var.<nome>}}`.
506
+ - **Agendamento (`booking`)** — calendário dentro do formulário, ligado a um tipo de evento.
507
+
508
+ Detalhes completos (construtor, página pública, blocos, gatilhos de flow, tetos e limites) no
509
+ resource `lionchat://docs/formularios-publicos`.
510
+