@lionchat/mcp-server 1.12.0 → 1.13.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.
@@ -18,7 +18,9 @@ Comprovado em produção (24/07): a sessão que dizia "não há função de list
18
18
  - **Famílias principais pra pesquisar:** conversations, conversations_messages, contacts,
19
19
  kanban_items, funnels, flows, flow_tools, campaigns, macros, automation_rules, agents, teams,
20
20
  inboxes, labels, canned_responses, reports, sla, csat, captain_assistants, captain_documents,
21
- scheduled_messages, custom_attributes, webhooks, eclinica_integrations, tasks, notifications.
21
+ scheduled_messages, custom_attributes, webhooks, eclinica_integrations, tasks, notifications,
22
+ lead_forms (Formulários públicos de captação), guru_subscription (gestão da assinatura da PRÓPRIA
23
+ conta no LionChat — feature liberada conta a conta).
22
24
  - Só depois de pesquisar e não encontrar é que se pode dizer que a função não existe.
23
25
  - **Trabalho volumoso** (relatório de muitas conversas/cards): as listas são paginadas (25-100 por
24
26
  página) — faça em ETAPAS ao longo de várias respostas, acumulando os resultados; nunca desista
@@ -47,6 +49,8 @@ Account (conta/empresa)
47
49
  ├─ Captain::Assistant (AI Agente / IA de atendimento)
48
50
  │ └─ Captain::Scenario (instrução situacional; pode FIXAR parâmetro de tool em tool_bindings)
49
51
  ├─ AccountTask (agenda/tarefas)
52
+ ├─ LeadForm (formulário público de captação)
53
+ │ └─ LeadFormResponse (preenchimento: in_progress / completed / abandoned)
50
54
  └─ Booking (agendamento de demonstração/reunião)
51
55
  ```
52
56
 
@@ -360,6 +364,7 @@ Antes de escolher a ferramenta, cheque se a intenção bate com a coluna da dire
360
364
  | Só a contagem / números | `lionchat_conversations_meta` / `lionchat_reports_summary` | Não liste o conteúdo completo |
361
365
  | Criar contato | `lionchat_contacts_create` base (path `/contacts`) | NÃO use as variantes `_1` … `_9` |
362
366
  | Pausar a IA agora (botão de pânico) | `lionchat_captain_assistants_update` (`paused: true`, top-level) | — |
367
+ | Criar/editar **formulário público de captação** (página de captura própria) | `lionchat_lead_forms_create` / `_update` | Leia `lionchat://docs/formularios-publicos` ANTES — o desenho vai em `form_data` e só entra no ar depois do `publish` (que valida o desenho e pode voltar 422 com a lista de pendências). Escrita é só de administrador |
363
368
  | Anexar arquivo a uma mensagem | `lionchat_upload_create` primeiro, depois usar o retorno em `lionchat_conversations_messages_create` | Confira `lionchat_upload_limits` se o arquivo for grande |
364
369
  | Marcar conversa como **lida** | `lionchat_conversations_update_last_seen` | NÃO use `unread` (esse marca como NÃO-lida) |
365
370
  | Criar/gerenciar **campanha** (disparo WhatsApp/SMS) | `lionchat_campaigns_*` (criar, estatísticas) | SEMPRE rode `campaigns_estimate_audience` antes de criar e mostre a contagem. Disparo em massa = confirmação obrigatória (seção 7b) |
@@ -442,6 +442,13 @@ mantém o comportamento antigo (só as colunas pedidas).
442
442
  `GET /upload_limits` retorna o teto em MB por tipo (`image`, `video`, `audio`, `document`,
443
443
  `fallback`). Consulte 1x antes de subir anexo grande — evita 422 no meio do fluxo.
444
444
 
445
+ **Desde 15/08/2026 o TIPO não rejeita mais anexo de mensagem** — a validação passou a ser só de
446
+ tamanho. Tipo sem balde próprio (nem imagem, nem vídeo, nem áudio) cai no balde de **documento**,
447
+ que é o maior. Ou seja: `upload_limits` continua sendo a única fonte dos tetos, mas a resposta
448
+ "esse formato não é aceito" deixou de existir nesse caminho. Os caminhos que **continuam** com
449
+ lista fechada de tipos são o upload por URL (`/upload` com `#validate`, usado no teste de mídia do
450
+ FlowBuilder) e o chat interno da equipe.
451
+
445
452
  ### Webhooks de saída — evento novo (2026-06)
446
453
 
447
454
  `inbox_updated` entrou na lista de eventos assináveis (`webhooks_create/update`): dispara quando
@@ -330,7 +330,7 @@ Como a coleta de dados/cenários funciona hoje (importante pra diagnosticar "IA
330
330
 
331
331
  - O admin pode FIXAR o parâmetro de uma ferramenta no cenário (campo scenario.tool_bindings): quais mídias enviar, qual funil+etapa do card, quais agendas no agendamento. "Deixar a IA decidir" = binding vazio.
332
332
  - Quando UM cenário fixa a tool, a execução vira determinística (a IA não escolhe): mídia e kanban são aplicados pós-turno pelo BindingResolver (ordem texto→ação, sem duplicar, idempotente); o agendamento força/restringe a agenda mas a IA ainda define data/hora.
333
- - Via MCP: scenarios_create/update persiste binding de send_media_asset e create_kanban_item/move_kanban_item (re-escopados por conta). O binding de create_booking NÃO é aceito pela API — pra limitar agendas via MCP, use config.booking_event_type_ids no assistente (captain_assistants_update).
333
+ - Via MCP: scenarios_create/update persiste binding de send_media_asset e create_kanban_item/move_kanban_item (re-escopados por conta). O binding de create_booking NÃO é aceito pela API — pra limitar agendas via MCP, use config.booking_event_type_ids no assistente (captain_assistants_update). **Atualização 15/08:** o modo direto/avulso do create_booking foi REMOVIDO — agenda de Booking é o ÚNICO caminho de agendamento da IA (ver seção "O que mudou no agendamento DA IA").
334
334
  - Receita pra criar/editar cenário com binding via MCP (scenarios_create / scenarios_update): (1) o corpo vai SEMPRE embrulhado em `scenario`; (2) a instrução PRECISA mencionar a ferramenta como link markdown `[Rótulo](tool://slug)` — menção crua `(tool://slug)` NÃO conta e o binding é descartado no salvamento; (3) envie `tool_bindings` com o shape: `send_media_asset` → `{ "asset_ids": [Int] }`, `create_kanban_item`/`move_kanban_item` → `{ "funnel_id": Int, "stage": "<chave da etapa>" }`. Não precisa mandar `tools` (é auto-extraído da instrução e sobrescrito). `create_booking` NÃO entra em tool_bindings pela API.
335
335
  - Pra APONTAR uma página da base de conhecimento num cenário via MCP, escreva `[@Nome](document://ID)` (o ID numérico do documento) DENTRO do texto da `instruction` — igual ao link de tool, o motor deriva daí o campo `document_ids`. NÃO existe param `document_ids` na API/MCP (mandar seria descartado em silêncio). Teto de 3 documentos; a página apontada ganha PRIORIDADE na busca RAG, NÃO exclusividade (o resto da base continua pesquisável).
336
336
 
@@ -445,6 +445,32 @@ criar a campanha e mostre a contagem ao usuário — estimativa e disparo usam o
445
445
  - Rate limit da reserva pública: 10/5min por IP + 20/min por conta (429 ao estourar).
446
446
  - Todo agendamento aparece vinculado a uma conversa (cria/reusa a conversa do contato).
447
447
 
448
+ ### O que mudou no agendamento DA IA (2026-08-15)
449
+
450
+ **1. A IA só agenda por AGENDA de Booking.** `view_booking_option` virou a ÚNICA fonte de horário
451
+ — o modo direto/avulso do `create_booking` foi REMOVIDO (era o único caminho que criava
452
+ compromisso fora do motor de reservas). Consequência prática: a IA passou a respeitar os dias em
453
+ que a agenda NUNCA abre (parou de oferecer domingo) e não inventa mais identificador de agenda —
454
+ com uma única agenda liberada ela assume essa; com duas ou mais, recusa nomeando as válidas. Para
455
+ restringir quais agendas a IA oferece, continua valendo `captain_assistants_update` com
456
+ `config.booking_event_type_ids`.
457
+
458
+ **2. A IA entrega o link do Meet sozinha.** O link do Google Meet não existe no instante em que o
459
+ agendamento é criado (nasce depois da ida ao Google), e antes a IA prometia o link e nunca voltava.
460
+ Agora o sistema arma uma volta de ~25s; sem link ainda, ele remarca sozinho (+40s) sem gastar turno
461
+ de IA; com link, a IA manda o endereço literal. Se na segunda tentativa o link não veio, ela avisa
462
+ o cliente e aciona um humano (nota privada com menção). Requisitos: o tipo de agendamento precisa
463
+ gerar Meet e o dono da agenda precisa ter o Google conectado. `{{conversation.custom_attribute.meet_link}}`
464
+ e o `{{meet_link}}` da mensagem de confirmação do Booking passaram a vir preenchidos (antes saíam
465
+ vazios) — isso vale inclusive para cliente que não usa IA.
466
+
467
+ **3. A IA não confirma reunião que não existe.** No último instante antes da resposta virar
468
+ mensagem: se ela afirma agendamento concluído, a agenda está liberada e não há compromisso real na
469
+ conversa, o sistema roda UMA tentativa guiada que agenda de verdade com a data/hora prometidas.
470
+ Agendou, sai a confirmação verdadeira; não agendou, a frase é trocada por uma honesta — nunca
471
+ silêncio. Diagnóstico: se o cliente disser "a IA confirmou e não tem nada na agenda", esse é o
472
+ comportamento ANTIGO (medido: 39 afirmações falsas em 30 dias antes do conserto).
473
+
448
474
  ## Variáveis de conta (account_variables)
449
475
 
450
476
  Pra dados fixos que se repetem (slogans, endereços, horários):
@@ -685,6 +711,9 @@ automática. Ver a seção de variáveis de conta acima.
685
711
  ```
686
712
  Cada item de `attachments` é o `blob_id` retornado pelo upload. Pode mandar vários.
687
713
  3. O mesmo padrão vale pra anexar mídia num card do Kanban (use o `blob_id` no campo de anexo do card).
714
+ 4. **Anexo de MENSAGEM aceita QUALQUER tipo de arquivo (novo 15/08)** — só o TAMANHO reprova. Se
715
+ houver dúvida sobre um arquivo grande, confira antes com `lionchat_upload_limits_show` (segue
716
+ sendo a fonte dos tetos).
688
717
 
689
718
  ## Cuidados com tools de criação
690
719
 
@@ -471,6 +471,37 @@ pode ser disparado por **webhook externo próprio**: cria-se a integração embu
471
471
  resolve contato/conversa (mesmas regras do Webhook Universal) e dispara o flow.
472
472
  Receita completa no resource **FlowBuilder — Guia de Design** (seção do trigger `webhook`).
473
473
 
474
+ ### Disparo de flow por FORMULÁRIO público de captação (2026-08)
475
+
476
+ Um flow pode ser disparado pelo que o lead faz num **Formulário público** (resource
477
+ `lionchat://docs/formularios-publicos`). São **três gatilhos**, todos no node `start`:
478
+
479
+ | Gatilho | Quando dispara |
480
+ |---|---|
481
+ | `lead_form_completed` | O lead **concluiu** o formulário |
482
+ | `lead_form_milestone` | O lead **atingiu um marco** dentro do formulário (sem precisar concluir) |
483
+ | `lead_form_abandoned` | O lead **parou no meio** e a janela de abandono do formulário venceu |
484
+
485
+ Como montar o item do gatilho:
486
+ - **`lead_form_id` é obrigatório** — o gatilho só casa com o formulário apontado; sem ele o flow
487
+ fica MUDO (nenhum erro, nenhum disparo).
488
+ - Pelo caminho **API/MCP**, `lead_form_id` (e `milestone_node_id`) vão no **topo do item**; a
489
+ **tela** grava os mesmos campos dentro de `config`. O motor lê os dois formatos — não precisa
490
+ duplicar.
491
+ - `milestone_node_id` é **opcional** e só existe no `lead_form_milestone`: preenchido, o flow
492
+ dispara só naquele marco; **vazio = qualquer marco** daquele formulário.
493
+ - Só vale em **flow individual** (`conversation_mode: 'individual'`). Flow de grupo e ferramenta
494
+ de IA (`ai_tool`) nunca são disparados por formulário.
495
+
496
+ Comportamento no disparo:
497
+ - Cada gatilho dispara **uma vez por resposta** (`completed`, `milestone:<node_id>` e `abandoned`
498
+ são chaves independentes) — retentativa não dobra o flow.
499
+ - Exige **contato** na resposta: lead que não deixou nome+telefone não dispara nada. Em caixa
500
+ WhatsApp (oficial ou QR Code), contato sem telefone também é pulado.
501
+ - O disparo **cria ou reabre uma conversa** na primeira caixa vinculada ao flow — ou seja, acorda
502
+ os ouvintes de `conversation_created` (automação, outros flows, webhook de saída, Kanban, SLA,
503
+ notificação). Conte com isso ao desenhar a cadeia.
504
+
474
505
  ### Proteção anti-loop entre motores (2026-06)
475
506
 
476
507
  Automações e flows podem se encadear (automação dispara flow, flow dispara automação...).
@@ -492,6 +523,11 @@ ficar muda (2026-06-05).
492
523
  | `message_created` | Nova mensagem (incoming ou outgoing) |
493
524
  | `first_reply_created` | Primeira resposta humana |
494
525
 
526
+ **Os gatilhos de Formulário público (`lead_form_completed`, `lead_form_milestone`,
527
+ `lead_form_abandoned`) NÃO entram nessa tabela** — são gatilhos de **flow**, não eventos de
528
+ automação. Não adianta criar `automation_rule` com esses nomes em `event_name`; o caminho é o node
529
+ `start` de um flow (ver a seção acima).
530
+
495
531
  ### Subtipos de `conversation_updated` (campo `action_types`)
496
532
 
497
533
  Ao criar/atualizar uma `automation_rule` com `event_name: 'conversation_updated'`, envie tambem
@@ -502,6 +502,20 @@ MetaLeadIntegration (Facebook Lead Ads)
502
502
  ├── status (active/token_expired/paused)
503
503
  └── meta_lead_forms (has_many)
504
504
 
505
+ LeadForm (Formulário público de captação — feature flag lead_forms, por conta)
506
+ ├── account_id (FK)
507
+ ├── name / slug (8 chars, gerado na criação e imutável) / display_mode (cards|chat) / active
508
+ ├── form_data (jsonb: o desenho do construtor) — published_data (a FOTO publicada, que a página usa)
509
+ ├── settings (jsonb: primary_color, theme, abandon_minutes, button_label) / published_at
510
+ └── counts: views / responses / completed (contadores) + lead_form_responses (has_many)
511
+
512
+ LeadFormResponse (um preenchimento do formulário)
513
+ ├── lead_form_id (FK) / account_id (FK)
514
+ ├── contact_id (FK opcional — o contato nasce assim que nome+telefone entram, mesmo sem concluir)
515
+ ├── status (in_progress / completed / abandoned)
516
+ └── answers (respostas) / milestones (marcos atingidos) / utm_params / started_at / completed_at /
517
+ abandoned_at
518
+
505
519
  CustomWebhookIntegration (Webhook Universal — entrada)
506
520
  ├── account_id (FK)
507
521
  ├── name / token (URL única) / active
@@ -463,6 +463,12 @@ o botão "Usar variável" ao lado do campo alterna lista fixa ↔ variável.
463
463
 
464
464
  **Campos NATIVOS do contato (2026-07-29):** com `attr_source: 'contact'`, `attr_key` também aceita os paths canônicos `contact.name`, `contact.email`, `contact.cpf`, `contact.cnpj`, `contact.rg`, `contact.passport`, `contact.date_of_birth`, `contact.gender`, `contact.marital_status`, `contact.profession` e `contact.address.{cep,street,number,complement,neighborhood,city,state,country}` — gravam na ficha nativa do contato (não em custom_attributes). Regras: telefone/identifier/etiquetas NÃO são aceitos (mapa fechado — chave fora dele vira atributo personalizado comum); documentos, nascimento, gênero e **e-mail** só preenchem campo VAZIO; nome/profissão/estado civil/endereço sobrescrevem; valor em branco nunca grava; gênero aceita `m|f|o|na` e estado civil `solteiro|casado|uniao_estavel|divorciado|separado|viuvo`; data de nascimento em `dd/mm/aaaa` ou `aaaa-mm-dd`. Recusas aparecem no histórico de execução (passo 'skipped'/'error' com mensagem).
465
465
 
466
+ **Campos NATIVOS do contato em CONDIÇÃO (2026-08-13):** o bloco de Condições e o filtro do gatilho de data passaram a LER os mesmos paths nativos (`contact.name`, `contact.cpf`, `contact.marital_status`, `contact.address.city`, ...) na fonte `contact` — antes só liam atributo personalizado e o campo da ficha voltava vazio, jogando tudo na saída padrão em silêncio. Use com `is_empty`/`is_not_empty` para montar "o CPF está preenchido?". Aceita a forma curta (`contact.cpf`) e a longa (`contact.cadastral.cpf`). **`contact.label` NÃO é aceito** em condição (consulta cara em caminho reativo). **Gênero e estado civil são listas fechadas**: compare com o valor canônico (`m|f|o|na`, `solteiro|casado|uniao_estavel|divorciado|separado|viuvo`) — comparar com "Masculino" nunca casa.
467
+
468
+ **NÃO use campo nativo na CONDIÇÃO DE SAÍDA nem no gatilho "quando o atributo do contato mudar"** — o sistema só emite o evento de mudança para atributo PERSONALIZADO, então a regra nunca dispararia. Nesses dois lugares o seletor continua oferecendo apenas atributos personalizados, de propósito.
469
+
470
+ **Parâmetro `always_ask` no node inicial de `ai_tool` (2026-08-13):** `true` faz a IA perguntar o dado ao cliente mesmo que o campo já tenha valor salvo, e regravar com a resposta. Use quando o valor guardado não for confiável (o nome do WhatsApp costuma ser só o primeiro nome). O campo volta a contar como resolvido depois que a IA salvar naquela conversa. Omitir = comportamento de sempre (campo preenchido é pulado).
471
+
466
472
  **Somar/subtrair NÃO é operação dedicada** — é filtro Liquid no próprio `attr_value`. O valor é resolvido como template antes de salvar. Exemplos:
467
473
 
468
474
  ```json
@@ -0,0 +1,410 @@
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
+ desenho dos blocos, publicação, respostas, funil e integração com fluxos.
5
+
6
+ ## O que é
7
+
8
+ Um formulário público é uma página de captação hospedada pelo próprio LionChat. O visitante abre um
9
+ link, responde bloco a bloco, e o LionChat cria o contato, guarda as respostas e (opcionalmente)
10
+ dispara um fluxo do FlowBuilder no fim.
11
+
12
+ - **Link público**: `{FRONTEND_URL}/forms/<referência da conta>/<slug>` — a referência da conta é o
13
+ `booking_public_slug` (o mesmo usado na agenda pública) e o `slug` do formulário tem 8 caracteres
14
+ alfanuméricos minúsculos. O campo `public_url` já vem pronto na resposta das tools.
15
+ - **Dois formatos de exibição** (`display_mode`): `cards` (um bloco por tela, estilo Typeform) ou
16
+ `chat` (bolhas de conversa, estilo Typebot).
17
+ - **Feature flag `lead_forms`, por conta, nasce desligada.** Com a flag desligada a **página pública
18
+ responde 404** — ninguém consegue preencher. As tools de dashboard (listar, criar, editar,
19
+ publicar, ver respostas) **funcionam mesmo assim**: não há gate de feature no backend do
20
+ dashboard. Ou seja: dá pra montar o formulário inteiro antes de ligar a flag, mas não adianta
21
+ divulgar o link antes.
22
+ - **Escrita é só de administrador.** `create`, `update`, `destroy`, `publish` e `duplicate` passam
23
+ por `check_admin_authorization?`; usuário não-admin recebe **401**. Leitura (`list`, `show`,
24
+ `stats`, `responses`) é liberada a qualquer agente da conta.
25
+
26
+ ### Não confundir
27
+
28
+ | Nome parecido | O que é de verdade |
29
+ |---|---|
30
+ | **Formulário público** (este documento) | Página de captação hospedada pelo LionChat, com blocos e link próprio |
31
+ | **MetaLeadForm** | Formulário de anúncio da Meta (Facebook/Instagram Lead Ads) — outro módulo |
32
+ | **Flow / FlowBuilder** | Motor de automação de conversa. O formulário *dispara* fluxos, mas não é um fluxo |
33
+
34
+ ## Ferramentas disponíveis
35
+
36
+ | Ferramenta | Endpoint | O que faz |
37
+ |---|---|---|
38
+ | `lionchat_lead_forms_list` | `GET /lead_forms` | Lista os formulários da conta (mais novos primeiro). Não traz `media_urls` |
39
+ | `lionchat_lead_forms_create` | `POST /lead_forms` | Cria. Admin-only |
40
+ | `lionchat_lead_forms_show` | `GET /lead_forms/:id` | Detalhe completo, único lugar que traz `media_urls` |
41
+ | `lionchat_lead_forms_update` | `PATCH /lead_forms/:id` | Edita. Admin-only |
42
+ | `lionchat_lead_forms_destroy` | `DELETE /lead_forms/:id` | Exclui (204 sem corpo). Admin-only |
43
+ | `lionchat_lead_forms_publish` | `POST /lead_forms/:id/publish` | Publica: tira a foto do desenho. Admin-only |
44
+ | `lionchat_lead_forms_duplicate` | `POST /lead_forms/:id/duplicate` | Cópia completa com slug novo. Admin-only |
45
+ | `lionchat_lead_forms_stats` | `GET /lead_forms/:id/stats` | Números e funil por bloco |
46
+ | `lionchat_lead_forms_responses_list` | `GET /lead_forms/:id/responses` | Lista de respostas (leve, sem as respostas em si) |
47
+ | `lionchat_lead_forms_responses_show` | `GET /lead_forms/:id/responses/:rid` | Detalhe de uma resposta, com o que a pessoa respondeu |
48
+
49
+ **Imagens não têm ferramenta MCP.** O upload de imagem de bloco (`POST /lead_forms/:id/media`,
50
+ máximo 5 MB, só `image/*`) existe no painel. Pela API dá pra referenciar um `image` que já foi
51
+ subido, nunca subir um novo.
52
+
53
+ ## Campos do formulário
54
+
55
+ | Campo | Tipo | Observação |
56
+ |---|---|---|
57
+ | `name` | string | **Obrigatório** |
58
+ | `display_mode` | string | `cards` (padrão) ou `chat` |
59
+ | `active` | boolean | Formulário inativo devolve 404 na página pública |
60
+ | `form_data` | jsonb | O desenho (rascunho). Teto de 2 MB serializado |
61
+ | `settings` | jsonb | Ver abaixo |
62
+ | `slug` | string | **Gerado no create e imutável** — nunca aceita valor do cliente |
63
+ | `published_at` | timestamp | Quando a foto foi tirada. Nulo = nunca publicado |
64
+ | `public_url` | string | Montado pelo servidor |
65
+ | `counts` | objeto | `{views, responses, completed}` — contadores do banco |
66
+
67
+ ### `settings`
68
+
69
+ | Chave | Valor | Padrão |
70
+ |---|---|---|
71
+ | `primary_color` | hex | `#7C3AED` |
72
+ | `theme` | `light` ou `dark` (valor fora da lista cai em `light`) | `light` |
73
+ | `abandon_minutes` | inteiro, limitado a 1..1440 | 15 |
74
+ | `button_label` | string — rótulo do botão da página pública | — |
75
+
76
+ ## Ciclo de vida
77
+
78
+ 1. **Criar** com `lionchat_lead_forms_create` (nome + `form_data`, mesmo que mínimo).
79
+ 2. **Montar o desenho** em `form_data` (contrato na próxima seção).
80
+ 3. **Publicar** com `lionchat_lead_forms_publish` — copia `form_data` para `published_data` e carimba
81
+ `published_at`.
82
+ 4. **Ativar** (`active: true`) e ligar a feature flag `lead_forms` da conta.
83
+ 5. **Divulgar** o `public_url`.
84
+
85
+ **Quem preenche usa SEMPRE a foto publicada, nunca o rascunho.** Editar `form_data` não muda nada
86
+ para quem está preenchendo até um novo `publish`. Isso é de propósito: dá pra mexer no formulário
87
+ com gente respondendo sem quebrar ninguém no meio.
88
+
89
+ **O contato nasce no MEIO do preenchimento**, não no fim: assim que existem respostas mapeadas para
90
+ `contact.name` E `contact.phone`, o LionChat cria (ou reaproveita) o contato, carimba a origem do
91
+ lead e aplica os atributos personalizados já respondidos. Quem começa e abandona depois disso
92
+ **continua virando contato com as respostas que deu** — é a diferença mais valiosa em relação a um
93
+ formulário tradicional.
94
+
95
+ ## O contrato do `form_data`
96
+
97
+ Esta é a parte que permite construir formulários pela API. O `form_data` é um desenho de nós e
98
+ arestas:
99
+
100
+ ```json
101
+ {
102
+ "nodes": [
103
+ { "id": "n1", "type": "question", "position": { "x": 0, "y": 200 }, "data": { } }
104
+ ],
105
+ "edges": [
106
+ { "id": "e1", "source": "n1", "target": "n2", "sourceHandle": "success" }
107
+ ]
108
+ }
109
+ ```
110
+
111
+ - `id` de nó e de aresta: qualquer string única dentro do desenho.
112
+ - `position`: `{x, y}` em pixels do canvas. Só afeta a visualização no editor; o motor ignora.
113
+ - `sourceHandle`: **qual saída do nó** a aresta usa. É o que decide o caminho (ver tabela).
114
+
115
+ ### Tipos de bloco
116
+
117
+ | `type` | Chaves de `data` permitidas | Saídas (`sourceHandle`) | Aparece pro lead? |
118
+ |---|---|---|---|
119
+ | `start` | `title, description, button_label, image` | `success` | sim |
120
+ | `question` | `label, description, field_type, placeholder, required, map_to, scale, image, accept, invalid_message` | `success` | sim |
121
+ | `choice` | `label, description, options, multiple, map_to` | `option_<id da opção>` (escolha única) / `success` (múltipla) | sim |
122
+ | `message` | `title, description, button_label, image` | `success` | sim |
123
+ | `condition` | `branches` | `cond_<id do ramo>` + `else` | não |
124
+ | `action` | `action_type, labels, name, event_name` | `success` | não |
125
+ | `send_message` | `inbox_id, content, template_name, template_params` | `success`, `skipped` | não |
126
+ | `end` | `title, description, redirect_url, button_label, image` | — | sim |
127
+ | `api_request` | `label, url, headers, timeout` | `success`, `error` | não (mostra bloco de espera) |
128
+ | `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) |
129
+
130
+ **Chave fora da lista não é lida pelo motor** e o editor a **remove no próximo salvamento pela
131
+ tela**. Escrever um campo inventado pela API não gera erro nenhum — ele simplesmente não faz nada e
132
+ depois some. A lista é fonte única (`LeadForm::NODE_DATA_KEYS` no servidor, espelhada em
133
+ `nodeSchema.js` no editor, com spec de contrato comparando as duas).
134
+
135
+ ### Detalhes por chave
136
+
137
+ **`field_type`** (do `question`): `short_text`, `long_text`, `email`, `phone`, `date`, `number`,
138
+ `rating` (usa `scale`), `url`, `file_upload` (usa `accept`).
139
+
140
+ **`accept`** (só em `file_upload`): `'any'` ou um array de categorias entre `image`, `video`,
141
+ `audio`, `pdf`, `spreadsheet`, `document`, `xml`. Arquivo executável é recusado sempre, antes de
142
+ qualquer categoria.
143
+
144
+ **`map_to`**: `''` (não guarda em lugar nenhum), `contact.name`, `contact.email`, `contact.phone`
145
+ ou `custom.<chave de atributo personalizado de contato>`.
146
+
147
+ **`options`** (do `choice`): `[{ "id": "abc123", "label": "Sim", "image": "" }]`. O `image` por
148
+ opção é o que faz a "escolha com fotos".
149
+
150
+ **`branches`** (do `condition`): lista avaliada em ordem; nenhum ramo casando cai no handle `else`.
151
+
152
+ ```json
153
+ [{ "id": "b1", "source_node_id": "q_cidade", "operator": "equals", "value": "São Paulo" }]
154
+ ```
155
+
156
+ Operadores: `equals`, `not_equals`, `contains`, `filled`, `not_filled`, `greater_than`,
157
+ `less_than`. Quando a fonte (`source_node_id`) é um `api_request`, o ramo **precisa** de `path` —
158
+ o caminho dentro da resposta da consulta (ex.: `"path": "localidade"`).
159
+
160
+ **`action_type`**: `add_label` (usa `labels: []`), `milestone` (usa `name` — é o marco que aparece
161
+ no funil e pode disparar fluxo), `meta_pixel_event` e `ga4_event` (usam `event_name`).
162
+
163
+ **`send_message`**: caixa WhatsApp QR Code usa `content` (texto com variáveis); caixa WhatsApp
164
+ oficial usa `template_name` + `template_params`. Um formulário com `send_message` **obriga** existir
165
+ uma pergunta com `map_to: contact.phone` — sem telefone não há para quem mandar.
166
+
167
+ **`api_request`**: **só GET**, sem corpo. A `url` precisa ter **domínio literal** (nada de variável
168
+ no host). Variáveis só entram no caminho e nos valores de query. `headers` aceita valor literal ou
169
+ `{{conta.<variável>}}` (o cofre de variáveis da conta) — variável derivada do lead em cabeçalho é
170
+ recusada na publicação. `timeout` em segundos: padrão 8, teto 15.
171
+
172
+ **`ai`**: `mode` é `generate` (devolve texto em `{{ia.<node_id>}}`) ou `classify` (roteia por
173
+ categoria). `categories` é obrigatório no modo `classify`. `include_answers` (booleano) manda as
174
+ respostas já dadas como contexto. `model` é validado contra a lista suportada
175
+ (`gpt-4.1-nano`, `gpt-4o-mini`, `gpt-4.1-mini`, `gpt-5.4-nano`, `gpt-5.4-mini`, `gpt-4o`, `gpt-4.1`,
176
+ `gpt-5-mini`, `gpt-5.4`, `o3-mini`, `o4-mini`, `gpt-5`, `gpt-5.2`, `gpt-5.5`, `o1`, `o3`);
177
+ `''` = usa o modelo padrão da conta. A IA usa a **chave da própria conta** — conta sem chave faz o
178
+ bloco sair pela saída de erro.
179
+
180
+ ### Variáveis
181
+
182
+ Usáveis no texto de `send_message`, no `prompt` da IA e na `url` do `api_request`:
183
+
184
+ | Variável | O que traz | Onde vale |
185
+ |---|---|---|
186
+ | `{{answer.<node_id>}}` | O que a pessoa respondeu naquele bloco (rótulo da opção ou texto) | mensagem, prompt, caminho/query da URL |
187
+ | `{{contact.name}}` `{{contact.phone}}` `{{contact.email}}` | Dados do contato já identificado | mensagem, prompt, caminho/query da URL |
188
+ | `{{api.<node_id>.<caminho>}}` | Um pedaço da resposta de uma consulta | mensagem, prompt, caminho/query da URL |
189
+ | `{{ia.<node_id>}}` | O texto que a IA gerou naquele bloco | mensagem, prompt, caminho/query da URL |
190
+ | `{{conta.<variável>}}` | Variável da conta (cofre, aceita segredo) | **só em cabeçalho de `api_request`** |
191
+
192
+ Variável desconhecida vira string vazia, em silêncio. `{{conta.*}}` fora do cabeçalho sai vazia
193
+ mesmo que a variável exista — é proteção, não defeito.
194
+
195
+ ## Regras de publicação
196
+
197
+ `lionchat_lead_forms_publish` devolve **422 com `{errors: [chaves]}`** quando o desenho não passa.
198
+ Cada chave e o que ela quer dizer:
199
+
200
+ | Chave | Significado |
201
+ |---|---|
202
+ | `no_start` | Não existe bloco `start` no desenho |
203
+ | `start_without_exit` | O `start` não tem aresta saindo dele |
204
+ | `no_reachable_end` | Nenhum bloco `end` é alcançável a partir do `start` |
205
+ | `question_without_label` | Uma pergunta está sem o texto da pergunta |
206
+ | `question_without_exit` | Uma pergunta não tem para onde ir depois |
207
+ | `choice_without_options` | Um bloco de escolha está sem nenhuma opção |
208
+ | `choice_without_exit` | Um bloco de escolha não tem nenhuma saída ligada |
209
+ | `choice_option_without_edge` | Uma opção específica da escolha não leva a lugar nenhum |
210
+ | `condition_without_branches` | Uma condição está sem nenhum ramo configurado |
211
+ | `condition_without_else` | Uma condição não tem a saída "senão" ligada |
212
+ | `action_without_config` | Um bloco de ação está sem a configuração que o tipo dele exige |
213
+ | `send_message_without_inbox` | Um bloco de mensagem está sem caixa de entrada escolhida |
214
+ | `send_message_requires_phone_question` | Há mensagem no formulário mas nenhuma pergunta guarda o telefone |
215
+ | `api_without_url` | Uma consulta está sem endereço |
216
+ | `api_without_error_exit` | Uma consulta está sem a saída de erro ligada |
217
+ | `api_url_host_not_literal` | O domínio da consulta tem variável (só caminho e valores aceitam) |
218
+ | `unknown_variable_in_url` | A URL usa uma variável de família desconhecida |
219
+ | `account_variable_in_url` | A URL usa `{{conta.*}}` (variável da conta só vale em cabeçalho) |
220
+ | `lead_variable_in_header` | Um cabeçalho usa variável vinda do lead (`answer`, `contact`, `api`, `ia`) |
221
+ | `ai_without_prompt` | Um bloco de IA está sem instrução |
222
+ | `ai_without_categories` | Um bloco de IA em modo classificar está sem categorias |
223
+ | `ai_without_error_exit` | Um bloco de IA (modo gerar) está sem a saída de erro ligada |
224
+ | `ai_without_other_exit` | Um bloco de IA (modo classificar) está sem a saída "outros" ligada |
225
+
226
+ ## Tetos e limites
227
+
228
+ | O quê | Limite |
229
+ |---|---|
230
+ | `form_data` serializado | 2 MB |
231
+ | Valor de uma resposta | 4 KB |
232
+ | Passos por requisição (anti-loop) | 50 |
233
+ | `days` em `stats` e `responses` | máximo 90 (valor ≤ 0 vira 30) |
234
+ | Página de `responses` | 25 por página, fixo; `page` até 10.000 |
235
+ | Chamadas de IA | 300 por formulário/dia, 1.000 por conta/dia, 10 por resposta |
236
+ | Consultas (`api_request`) | 2.000 por formulário/dia, 10.000 por conta/dia |
237
+ | Arquivo enviado pelo lead | 10 MB por arquivo, 10 arquivos por resposta |
238
+ | Imagem de bloco (só pelo painel) | 5 MB, apenas `image/*` |
239
+ | Funil em `stats` | 50.000 respostas (acima disso o funil sai parcial) |
240
+
241
+ ## Respostas e funil
242
+
243
+ **`lionchat_lead_forms_stats`** (`days` opcional) devolve:
244
+
245
+ ```json
246
+ { "views": 320, "starts": 180, "completed": 96, "abandoned": 40, "completion_rate": 53,
247
+ "funnel": [{ "node_id": "q_nome", "type": "question", "label": "Qual é o seu nome?",
248
+ "reached": 175, "percent": 97 }],
249
+ "milestones": [{ "node_id": "n7", "name": "Qualificado", "reached": 62 }] }
250
+ ```
251
+
252
+ O funil só lista os blocos que a pessoa vê (`start`, `question`, `choice`, `message`, `end`), na
253
+ ordem do desenho **publicado**.
254
+
255
+ **`lionchat_lead_forms_responses_list`** aceita `status` (`in_progress`, `completed` ou `abandoned`;
256
+ valor fora da lista é ignorado), `days` e `page`. A lista **nunca** traz as respostas em si — só
257
+ `{id, status, contact, current_node_label, answered_count, created_at, completed_at}`.
258
+
259
+ **`lionchat_lead_forms_responses_show`** traz o conteúdo:
260
+
261
+ - `answers`: `[{label, value_label, at, download_url?}]` — o `download_url` só existe em resposta de
262
+ envio de arquivo (e vem nulo se o arquivo já foi apagado).
263
+ - `milestones`: os marcos que a pessoa atingiu.
264
+ - `effects`: o resultado dos blocos de consulta e de IA — `{node_id, label, state, error, status,
265
+ url_short, at}`. `state` é `pending`, `done` ou `error`. **`url_short` é esquema + host + caminho,
266
+ sem a query**; o valor enviado, os cabeçalhos e o prompt **nunca** saem pela API.
267
+ - `utm_params`: de onde o lead veio.
268
+
269
+ Motivos de erro que aparecem em `effects`: `http_<código>`, `timeout`, `ssrf_blocked`,
270
+ `invalid_url`, `result_too_large`, `stale`, `busy`, `budget_exceeded`, `no_ai_key`, `ai_error`,
271
+ `unresolved_variable`.
272
+
273
+ ## Receitas
274
+
275
+ ### 1. Formulário simples que já publica
276
+
277
+ Desenho mínimo que passa na publicação: início, nome, telefone, fim.
278
+
279
+ ```json
280
+ {
281
+ "nodes": [
282
+ { "id": "start", "type": "start", "position": { "x": 0, "y": 0 },
283
+ "data": { "title": "Fale com a gente", "description": "Leva 30 segundos.",
284
+ "button_label": "Começar" } },
285
+ { "id": "q_nome", "type": "question", "position": { "x": 0, "y": 200 },
286
+ "data": { "label": "Qual é o seu nome?", "field_type": "short_text",
287
+ "required": true, "map_to": "contact.name" } },
288
+ { "id": "q_fone", "type": "question", "position": { "x": 0, "y": 400 },
289
+ "data": { "label": "Qual é o seu WhatsApp?", "field_type": "phone",
290
+ "required": true, "map_to": "contact.phone" } },
291
+ { "id": "fim", "type": "end", "position": { "x": 0, "y": 600 },
292
+ "data": { "title": "Obrigado!", "description": "Já falamos com você." } }
293
+ ],
294
+ "edges": [
295
+ { "id": "e1", "source": "start", "target": "q_nome", "sourceHandle": "success" },
296
+ { "id": "e2", "source": "q_nome", "target": "q_fone", "sourceHandle": "success" },
297
+ { "id": "e3", "source": "q_fone", "target": "fim", "sourceHandle": "success" }
298
+ ]
299
+ }
300
+ ```
301
+
302
+ Mande isso em `lionchat_lead_forms_create` junto com `name`, depois
303
+ `lionchat_lead_forms_publish` e `lionchat_lead_forms_update` com `active: true`.
304
+
305
+ ### 2. Consultar o CEP e ramificar pela cidade
306
+
307
+ Pergunta o CEP, consulta o ViaCEP e manda quem é de São Paulo para um caminho diferente.
308
+ Fragmento — os blocos `start`, `msg_sp`, `msg_outros`, `fim_erro` e os fins ficam de fora por
309
+ brevidade; no envio real todos precisam existir e alcançar um bloco de fim.
310
+
311
+ ```json
312
+ {
313
+ "nodes": [
314
+ { "id": "q_cep", "type": "question", "position": { "x": 0, "y": 200 },
315
+ "data": { "label": "Qual é o seu CEP?", "field_type": "short_text", "required": true } },
316
+ { "id": "api_cep", "type": "api_request", "position": { "x": 0, "y": 400 },
317
+ "data": { "label": "Consulta de CEP",
318
+ "url": "https://viacep.com.br/ws/{{answer.q_cep}}/json/",
319
+ "headers": [], "timeout": 8 } },
320
+ { "id": "cond_sp", "type": "condition", "position": { "x": 0, "y": 600 },
321
+ "data": { "branches": [ { "id": "b1", "source_node_id": "api_cep",
322
+ "path": "localidade", "operator": "equals",
323
+ "value": "São Paulo" } ] } }
324
+ ],
325
+ "edges": [
326
+ { "id": "e1", "source": "q_cep", "target": "api_cep", "sourceHandle": "success" },
327
+ { "id": "e2", "source": "api_cep", "target": "cond_sp", "sourceHandle": "success" },
328
+ { "id": "e3", "source": "api_cep", "target": "fim_erro", "sourceHandle": "error" },
329
+ { "id": "e4", "source": "cond_sp", "target": "msg_sp", "sourceHandle": "cond_b1" },
330
+ { "id": "e5", "source": "cond_sp", "target": "msg_outros", "sourceHandle": "else" }
331
+ ]
332
+ }
333
+ ```
334
+
335
+ Três coisas obrigatórias aqui: a saída `error` ligada, o `else` ligado, e o `path` no ramo (porque a
336
+ fonte é uma consulta). Faltando qualquer uma, a publicação recusa.
337
+
338
+ ### 3. Triagem por IA
339
+
340
+ Classifica o que a pessoa escreveu e manda cada caso para um lugar.
341
+
342
+ ```json
343
+ {
344
+ "nodes": [
345
+ { "id": "q_motivo", "type": "question", "position": { "x": 0, "y": 200 },
346
+ "data": { "label": "Conte rapidinho o que você precisa",
347
+ "field_type": "long_text", "required": true } },
348
+ { "id": "ia_triagem", "type": "ai", "position": { "x": 0, "y": 400 },
349
+ "data": { "label": "Triagem", "mode": "classify",
350
+ "prompt": "Classifique o pedido do cliente em uma das categorias.",
351
+ "categories": [ { "id": "c1", "label": "Orçamento" },
352
+ { "id": "c2", "label": "Suporte" } ],
353
+ "include_answers": true, "model": "" } }
354
+ ],
355
+ "edges": [
356
+ { "id": "e1", "source": "q_motivo", "target": "ia_triagem", "sourceHandle": "success" },
357
+ { "id": "e2", "source": "ia_triagem", "target": "fim_orcamento", "sourceHandle": "cat_c1" },
358
+ { "id": "e3", "source": "ia_triagem", "target": "fim_suporte", "sourceHandle": "cat_c2" },
359
+ { "id": "e4", "source": "ia_triagem", "target": "fim_geral", "sourceHandle": "other" }
360
+ ]
361
+ }
362
+ ```
363
+
364
+ Enquanto a consulta ou a IA roda, o lead vê um bloco de espera — o formulário não trava nem perde a
365
+ sessão.
366
+
367
+ ### 4. Disparar um fluxo quando o formulário for concluído
368
+
369
+ O gatilho mora no **fluxo**, não no formulário. Use `lionchat_flows_create` ou
370
+ `lionchat_flows_update` e coloque o item no bloco de início:
371
+
372
+ ```json
373
+ { "key": "lead_form_completed", "lead_form_id": 12 }
374
+ ```
375
+
376
+ Três gatilhos existem:
377
+
378
+ | `key` | Quando dispara |
379
+ |---|---|
380
+ | `lead_form_completed` | A pessoa chegou ao bloco de fim |
381
+ | `lead_form_milestone` | A pessoa passou por um bloco de ação do tipo `milestone` |
382
+ | `lead_form_abandoned` | A pessoa parou de responder e passou o tempo de `abandon_minutes` |
383
+
384
+ No `lead_form_milestone` dá pra acrescentar `milestone_node_id` para mirar um marco específico; sem
385
+ ele, qualquer marco daquele formulário dispara. O `lead_form_id` (e o `milestone_node_id`) podem ir
386
+ no topo do item ou dentro de `config` — o motor lê os dois.
387
+
388
+ **O gatilho só dispara se a resposta já tiver contato**, ou seja, se nome e telefone foram
389
+ respondidos. E dispara uma vez só por resposta e por tipo. O fluxo recebe as respostas como
390
+ variáveis: `form_name`, `form_kind`, `form_milestone` e uma variável `form_<pergunta>` por resposta
391
+ dada.
392
+
393
+ ## Armadilhas
394
+
395
+ - **O `slug` é gerado e imutável.** Mandar `slug` no create ou no update não faz nada. Link
396
+ publicado nunca muda de endereço.
397
+ - **`published_data` nunca sai pela API.** Para saber o que está no ar, olhe `published_at` e
398
+ compare mentalmente com o `form_data` — não existe endpoint que devolva a foto.
399
+ - **`form_data` no update SUBSTITUI o desenho inteiro.** Não há merge. Leia com
400
+ `lionchat_lead_forms_show`, altere o que precisa e mande o desenho completo de volta. Mandar só os
401
+ nós novos apaga todo o resto.
402
+ - **A cópia nasce desligada e não publicada.** `duplicate` cria com `active: false`, sem
403
+ `published_at`, com `" (cópia)"` no nome e slug novo. As imagens são duplicadas de verdade (blobs
404
+ novos), então mexer nelas na cópia não afeta o original.
405
+ - **Publicar não ativa.** São dois passos: `publish` (tira a foto) e `update` com `active: true`.
406
+ - **Flag desligada = página 404.** Se o cliente diz que o link não abre e as tools funcionam
407
+ normalmente, o suspeito número um é a feature `lead_forms` da conta.
408
+ - **`media_urls` só existe no `show`.** A listagem não traz, de propósito (payload).
409
+ - **Nunca escreva segredo em cabeçalho literal** de `api_request` — use `{{conta.<variável>}}`, que
410
+ resolve só no servidor e nunca aparece na tela nem no log.