@lionchat/mcp-server 1.13.0 → 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.
@@ -1,27 +1,31 @@
1
1
  # Formulários Públicos — Guia Profundo
2
2
 
3
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.
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).
5
6
 
6
7
  ## O que é
7
8
 
8
9
  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.
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.
11
12
 
12
13
  - **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.
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.
15
19
  - **Dois formatos de exibição** (`display_mode`): `cards` (um bloco por tela, estilo Typeform) ou
16
20
  `chat` (bolhas de conversa, estilo Typebot).
17
21
  - **Feature flag `lead_forms`, por conta, nasce desligada.** Com a flag desligada a **página pública
18
22
  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.
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.
25
29
 
26
30
  ### Não confundir
27
31
 
@@ -38,13 +42,15 @@ dispara um fluxo do FlowBuilder no fim.
38
42
  | `lionchat_lead_forms_list` | `GET /lead_forms` | Lista os formulários da conta (mais novos primeiro). Não traz `media_urls` |
39
43
  | `lionchat_lead_forms_create` | `POST /lead_forms` | Cria. Admin-only |
40
44
  | `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 |
45
+ | `lionchat_lead_forms_update` | `PATCH /lead_forms/:id` | Edita (inclusive `inbox_id` e `slug`). Admin-only |
42
46
  | `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 |
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 |
45
49
  | `lionchat_lead_forms_stats` | `GET /lead_forms/:id/stats` | Números e funil por bloco |
46
50
  | `lionchat_lead_forms_responses_list` | `GET /lead_forms/:id/responses` | Lista de respostas (leve, sem as respostas em si) |
47
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 |
48
54
 
49
55
  **Imagens não têm ferramenta MCP.** O upload de imagem de bloco (`POST /lead_forms/:id/media`,
50
56
  máximo 5 MB, só `image/*`) existe no painel. Pela API dá pra referenciar um `image` que já foi
@@ -57,45 +63,75 @@ subido, nunca subir um novo.
57
63
  | `name` | string | **Obrigatório** |
58
64
  | `display_mode` | string | `cards` (padrão) ou `chat` |
59
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 |
60
67
  | `form_data` | jsonb | O desenho (rascunho). Teto de 2 MB serializado |
61
68
  | `settings` | jsonb | Ver abaixo |
62
- | `slug` | string | **Gerado no create e imutável** — nunca aceita valor do cliente |
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 |
63
70
  | `published_at` | timestamp | Quando a foto foi tirada. Nulo = nunca publicado |
64
71
  | `public_url` | string | Montado pelo servidor |
65
72
  | `counts` | objeto | `{views, responses, completed}` — contadores do banco |
66
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
+
67
87
  ### `settings`
68
88
 
69
89
  | Chave | Valor | Padrão |
70
90
  |---|---|---|
71
91
  | `primary_color` | hex | `#7C3AED` |
72
92
  | `theme` | `light` ou `dark` (valor fora da lista cai em `light`) | `light` |
73
- | `abandon_minutes` | inteiro, limitado a 1..1440 | 15 |
93
+ | `abandon_minutes` | inteiro, limitado a 1..15 (minutos) | 15 |
74
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) | — |
75
97
 
76
98
  ## Ciclo de vida
77
99
 
78
100
  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`.
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`.
84
109
 
85
110
  **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.
111
+ para quem está preenchendo até um novo `publish`.
88
112
 
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.
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.
94
121
 
95
- ## O contrato do `form_data`
122
+ ### Link identificado (pula perguntas já conhecidas)
96
123
 
97
- Esta é a parte que permite construir formulários pela API. O `form_data` é um desenho de nós e
98
- arestas:
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`
99
135
 
100
136
  ```json
101
137
  {
@@ -116,15 +152,18 @@ arestas:
116
152
 
117
153
  | `type` | Chaves de `data` permitidas | Saídas (`sourceHandle`) | Aparece pro lead? |
118
154
  |---|---|---|---|
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 |
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 |
121
157
  | `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 |
158
+ | `message` | `title, description, button_label, image, video_url` | `success` | sim |
123
159
  | `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) |
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) |
128
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) |
129
168
 
130
169
  **Chave fora da lista não é lida pelo motor** e o editor a **remove no próximo salvamento pela
@@ -134,94 +173,171 @@ depois some. A lista é fonte única (`LeadForm::NODE_DATA_KEYS` no servidor, es
134
173
 
135
174
  ### Detalhes por chave
136
175
 
137
- **`field_type`** (do `question`): `short_text`, `long_text`, `email`, `phone`, `date`, `number`,
138
- `rating` (usa `scale`), `url`, `file_upload` (usa `accept`).
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`).
139
183
 
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>`.
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.
146
187
 
147
- **`options`** (do `choice`): `[{ "id": "abc123", "label": "Sim", "image": "" }]`. O `image` por
148
- opção é o que faz a "escolha com fotos".
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": "" }]`.
149
209
 
150
210
  **`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
211
  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`).
212
+ `less_than`. Fonte `api_request` exige `path` (caminho dentro da resposta).
162
213
 
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.
214
+ **`items`** (do `action`) — o formato novo, uma LISTA de ações `[{key, config}]`:
166
215
 
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.
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.
171
277
 
172
278
  **`ai`**: `mode` é `generate` (devolve texto em `{{ia.<node_id>}}`) ou `classify` (roteia por
173
279
  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.
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.
179
283
 
180
284
  ### Variáveis
181
285
 
182
- Usáveis no texto de `send_message`, no `prompt` da IA e na `url` do `api_request`:
183
-
184
286
  | Variável | O que traz | Onde vale |
185
287
  |---|---|---|
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`** |
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 |
191
294
 
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.
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.
194
297
 
195
298
  ## Regras de publicação
196
299
 
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:
300
+ `lionchat_lead_forms_publish` devolve **422 com `{errors: [chaves]}`**. As 34 chaves atuais:
199
301
 
200
302
  | Chave | Significado |
201
303
  |---|---|
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 |
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`.
225
341
 
226
342
  ## Tetos e limites
227
343
 
@@ -233,48 +349,76 @@ Cada chave e o que ela quer dizer:
233
349
  | `days` em `stats` e `responses` | máximo 90 (valor ≤ 0 vira 30) |
234
350
  | Página de `responses` | 25 por página, fixo; `page` até 10.000 |
235
351
  | 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 |
352
+ | Consultas (`api_request`) | 2.000 por formulário/dia, 10.000 por conta/dia; corpo até 64 KB |
237
353
  | Arquivo enviado pelo lead | 10 MB por arquivo, 10 arquivos por resposta |
238
354
  | Imagem de bloco (só pelo painel) | 5 MB, apenas `image/*` |
239
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}`.
240
375
 
241
376
  ## Respostas e funil
242
377
 
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
- ```
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**.
251
382
 
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}`.
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.
258
388
 
259
389
  **`lionchat_lead_forms_responses_show`** traz o conteúdo:
260
390
 
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.
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.
267
397
  - `utm_params`: de onde o lead veio.
268
398
 
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`,
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`,
271
401
  `unresolved_variable`.
272
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
+
273
417
  ## Receitas
274
418
 
275
419
  ### 1. Formulário simples que já publica
276
420
 
277
- Desenho mínimo que passa na publicação: início, nome, telefone, fim.
421
+ Início, nome, telefone, fim — E a caixa vinculada:
278
422
 
279
423
  ```json
280
424
  {
@@ -299,112 +443,57 @@ Desenho mínimo que passa na publicação: início, nome, telefone, fim.
299
443
  }
300
444
  ```
301
445
 
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`.
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`.
304
448
 
305
449
  ### 2. Consultar o CEP e ramificar pela cidade
306
450
 
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.
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`.
337
454
 
338
455
  ### 3. Triagem por IA
339
456
 
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.
457
+ `mode: "classify"` + `categories` + arestas `cat_<id>` e `other`. Inalterado.
366
458
 
367
459
  ### 4. Disparar um fluxo quando o formulário for concluído
368
460
 
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:
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
371
468
 
372
469
  ```json
373
- { "key": "lead_form_completed", "lead_form_id": 12 }
470
+ { "id": "agenda", "type": "booking", "position": { "x": 0, "y": 500 },
471
+ "data": { "label": "Escolha um horário", "event_type_id": "34", "required": false } }
374
472
  ```
375
473
 
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.
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`.
392
477
 
393
478
  ## Armadilhas
394
479
 
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.
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.
397
484
  - **`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.
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.