Runbook — Webhooks do Sagazchat
Manual operacional para uma IA ou pessoa criar, configurar e excluir webhooks no Sagazchat sem depender de tentativa visual cega.
Manual operacional para uma IA ou pessoa criar, configurar e excluir webhooks no Sagazchat sem depender de tentativa visual cega.
Status: validado em 2026-05-11 na conta de exemplo (
user_id=39, empresaexemplo). App emv4.6.0(sidebar mostravav4.5.7antes do deploy do mesmo dia). Webhooks pré-existentes NA CONTA — NÃO DELETAR:
teste(Ativo, hashSFNoh67QaSXJI4xuTw5XkpPaONxXIKcQnXVzrbBFvB, 8 requisições totais, mapping vazio)validpay(Ativo, 23 requisições totais)Laboratórios criados nesta validação:
LAB-WEBHOOK-TEMP(id 273) — criado e deletadoLAB-WEBHOOK-TEMP2(id 274) — criado, desativado, deletadoLAB-WEBHOOK-TEMP3(id 275, hashDFJVRJKvZDGLGLGCiSMyew0jiJZucwyvRxSLSpQHoX) — criado, alimentado via fetch JSON, deletadoLAB-WEBHOOK-FUNCIONAL(id 276, hash4XZbDr9daITK25P1TyZjX2Jeq3TZUY0yBOtX5Da1Hz) — VIVO, configurado funcional: fluxoVENDA-SAGAZCHAT(id 540), WhatsAppexemplo(id 216). Alimentado via POST urlencoded daqui (OK_SHOOT200). Mantido pra eventual reuso/inspeção.
1. Onde fica
- Sidebar: Automação › Webhooks. A seção Automação é um grupo expansível na sidebar (chevron
>); ela contém 7 itens: Webhooks, Agendamentos, Campanhas, Remarketing, Conexões, N8N, Prompts. - Lista:
https://app.sagazchat.com/webhooks - Editor:
https://app.sagazchat.com/webhook/<hash_id>(singular — semsfinal) - URL pública que recebe o webhook:
https://backend.sagazchat.com/webhook/<user_id>/<hash_id>
Nomenclatura ambígua: “Conexões” aparece DUAS vezes no produto. (a) Aqui em Automação, rota
/integrations, conceito de integrações externas. (b) No vault e nos planos, “conexão” é slot agnóstico de canal (1 conexão = 1 canal entre WhatsApp/Instagram/Email/Widget/WhatsApp Oficial). Não confundir.
2. Lista /webhooks
A lista mostra:
| Coluna | Significado |
|---|---|
| Nome | Nome do webhook. Clicar no nome abre o editor. |
| Status | Ativo ou Desativado — controlado pelo botão Desativar/Ativar dentro do editor. |
| Requisições do mês | Contador que zera mensalmente. Bate com a quota do plano (Basic 15k, Pro 30k). |
| Requisições totais | Total histórico desde a criação. |
| Ações | Kebab ⋮ com 4 itens (ver § 6). |
Topo da tela:
- Contador
N/30000(ouN/15000em Basic) + tooltip?— mostra uso vs quota do plano. - Busca por nome.
- Botão verde + Adicionar.
Endpoint da lista:
GET /webhook?pageNumber=1
Retorna array de objetos webhook (estrutura no § 3).
3. Criar webhook
Fluxo pela UI:
- Abrir Automação › Webhooks.
- Clicar + Adicionar.
- Modal Adicionar abre com um único campo
Nomee botõesCancelar/Adicionar. - Preencher Nome.
- Clicar Adicionar.
- A UI NÃO redireciona automaticamente pro editor — o webhook aparece na lista com status
Ativoe contadores zerados. Clicar no nome pra abrir o editor.
API interna:
POST /webhook
Content-Type: application/json
{ "name": "Nome do webhook" }
Resposta real (200):
{
"id": 273,
"user_id": 39,
"hash_id": "VELEYYgntEVt1B7n2bzE21UO0QCEuDO00Dt4IPzvpA",
"company_id": 39,
"name": "LAB-WEBHOOK-TEMP",
"active": true,
"config": null,
"createdAt": "2026-05-11T20:47:24.780Z",
"updatedAt": "2026-05-11T20:47:24.780Z",
"requestMonth": 0,
"requestAll": 0,
"whatsappId": null,
"waOficialId": null,
"isMultWhatsapp": false,
"multCount": null,
"multWhatsapp": null,
"multWAOficial": [],
"waOficialTemplateName": null,
"waOficialTemplateLanguage": null,
"waOficialTemplateComponents": null
}
Observações:
idé o identificador numérico interno — usado em DELETE e nos toggles.hash_idé o token público — entra na URL que o sistema externo chama.- Recém-criado:
active: true,config: null, nenhum canal vinculado.
4. Editor /webhook/<hash_id>
Cabeçalho: seta voltar < + título Configuração de Webhook + botão (no topo direito) Desativar (se ativo) ou Ativar (se desativado).
Endpoint que carrega:
GET /webhook/<hash_id>
Endpoints auxiliares chamados ao abrir:
GET /flowbuilder/all/get— lista de fluxos para o combobox Selecione o FLUXO.GET /whatsapp— canais WhatsApp não-oficiais conectados.GET /waoficial— canais WhatsApp Oficial.GET /tags/list— etiquetas pra add/remove.
4.1 Seções do editor (na ordem da tela)
| # | Seção | Tipo | Obrigatório |
|---|---|---|---|
| 1 | Este é o link do seu webhook | URL gerada + ícone de copiar | — |
| 2 | Última requisição que este webhook recebeu | Code preview JSON + botão Atualizar dados | — |
| 3 | Selecione os campos do {celular} do seu cliente | Autocomplete (caminho dentro do JSON recebido) | sim |
| 4 | Selecione os campos do {nome} do seu cliente | Autocomplete | sim |
| 5 | Selecione os campos do {email} do seu cliente | Autocomplete | sim (sem certeza — [a validar]) |
| 6 | Selecione o FLUXO que será disparado | Combobox Escolha um fluxo | sim |
| 7 | Canal de envio | Combobox (default WhatsApp) | sim |
| 8 | Selecione o WHATSAPP ou ative a distribuição | Combobox Escolha um whatsapp + checkbox distribuição | sim (1 dos 2) |
| 9 | Selecione a etiqueta que deseja adicionar | Multi-search Etiquetas (Opcional) | não |
| 10 | Selecione a etiqueta que deseja remover | Multi-search Etiquetas (Opcional) | não |
| 11 | Campos adicionais | Botão + Adicionar campo — cria pares chave/valor extras (Opcional) | não |
Botão Salvar webhook no rodapé.
O webhook só dispara o fluxo quando todos os 4 campos obrigatórios (
{celular},{nome},{email}?, FLUXO) estão preenchidos. O webhooktesteda conta temconfig: null(mapping vazio) — recebeu 8 requests mas nunca disparou nada.
4.2 Salvar mapping (PUT)
Endpoint validado:
PUT /webhook/config
Content-Type: application/json
Body real capturado (LAB-WEBHOOK-FUNCIONAL id 276, fluxo VENDA-SAGAZCHAT id 540, WhatsApp “Exemplo” id 216):
{
"config": {
"inputs": [
{ "order": 0, "keyValue": "nome", "data": "#nome" },
{ "order": 1, "keyValue": "email", "data": "#email" },
{ "order": 2, "keyValue": "celular", "data": "#telefone" }
],
"idFlow": 540,
"keysFull": ["nome", "telefone", "email", "origem"],
"tagName": null,
"tagNameRemove": null,
"version": 2
},
"webhookId": 276,
"whatsappId": 216,
"waOficialId": null,
"isMultWhatsapp": false,
"multWhatsapp": [],
"multWAOficial": [],
"waOficialTemplateName": null,
"waOficialTemplateLanguage": null,
"waOficialTemplateComponents": null
}
Resposta: "ok" (string literal, status 200).
Mapeamento do payload:
| Campo | O que significa | Observação |
|---|---|---|
config.inputs[] | Lista dos 3 mappings obrigatórios | Note a ORDEM: 0=nome, 1=email, 2=celular — diferente da ordem visual da tela |
config.inputs[].order | Posição (0–2) | Apenas 3 entradas; sempre nessas chaves |
config.inputs[].keyValue | Variável Sagazchat | nome, email, celular (string literal) |
config.inputs[].data | Chave do JSON recebido | Com prefixo # (ex: #telefone aponta pra chave telefone do payload) |
config.idFlow | ID do fluxo a disparar | Vem de GET /flowbuilder/all/get |
config.keysFull | Todas as chaves do JSON da última requisição | Backend mantém pra UI re-renderizar opções |
config.tagName | Etiqueta a aplicar | string ou null |
config.tagNameRemove | Etiqueta a remover | string ou null |
config.version | Schema version | 2 (atual em 2026-05-11) |
webhookId | ID numérico do webhook | NÃO hash_id |
whatsappId | ID do canal WhatsApp não-oficial | OU null |
waOficialId | ID do canal WhatsApp Oficial | OU null |
isMultWhatsapp | Modo distribuição | false = canal específico; true = distribuir |
multWhatsapp / multWAOficial | Listas pra modo distribuição | [] quando isMultWhatsapp: false |
waOficialTemplate* | Template WA Oficial | Só preenche se canal for WA Oficial |
Ordem correta de seleção na UI (descoberta durante auditoria): preencher os 3 mappings → escolher WhatsApp PRIMEIRO → o combobox Fluxo carrega filtrado por canal. Tentar abrir Fluxo antes do WhatsApp retorna lista vazia.
4.3 Toggle ativar/desativar
Clicar Desativar (ou Ativar) no canto superior direito do editor.
PUT /webhook/active
Content-Type: application/json
{ "status": false, "webhookId": <id> }
Resposta: "ok" (string literal, não JSON).
Quando desativado, a URL pública continua respondendo mas a requisição não é processada (presumido — [a validar]).
4.4 Atualizar dados
Botão Atualizar dados ao lado de “Última requisição que este webhook recebeu”. Recarrega o preview JSON da última chamada externa. Útil pra colar uma requisição de teste do sistema externo e mapear os caminhos depois.
5. URL pública e payload recebido
Sistemas externos (CRM, Zapier, formulário) precisam fazer POST para:
https://backend.sagazchat.com/webhook/<user_id>/<hash_id>
Onde:
<user_id>aparece nas requisições internas/users/profile/<id>.<hash_id>é o token devolvido na criação (campohash_id).
Não há autenticação adicional — quem tiver o link consegue chamar. O hash_id é o secret de fato — não vazar pra clientes finais.
Resposta do backend ao receber POST externo: OK_SHOOT (string literal, não JSON; status 200). Capturado com fetch direto no LAB-WEBHOOK-TEMP3.
Payload é livre: qualquer JSON. O mapping dentro do editor é que define quais campos viram {celular}, {nome}, {email} no contato e/ou nas variáveis do fluxo.
Exemplo capturado (preview no editor do teste):
{
"nome": "Fulano Exemplo",
"numero": "55998120XXXX",
"Motivo": "{motivo}"
}
(Note que o sistema externo enviou literal "{motivo}" — Sagazchat não substitui isso, apenas armazena como string.)
Exemplo de payload “feed” testado na auditoria (POST externo via fetch):
{
"telefone": "5511999990000",
"primeiro_nome": "Cliente Lab",
"email_contato": "lab@example.com",
"origem": "auditoria"
}
Confirmado: o backend aceita chaves arbitrárias (não obriga nome/celular/email específicos). O mapping dentro do editor é que diz qual caminho do JSON serve pra cada variável do contato.
6. Menu kebab da lista
Cada linha tem ⋮ na coluna Ações com 4 opções:
| Item | Ação | Endpoint inferido |
|---|---|---|
| Editar | Provável: renomear (modal). | PUT /webhook/<id> com body { "name": "..." } — [a validar] |
| Duplicar | Cria cópia do webhook (gera novo hash_id). | POST /webhook/clone/<id> — [a validar] |
| Configurações | Atalho equivalente a clicar no nome — abre editor. | — |
| Excluir | Remove webhook. Destrutivo. | DELETE /webhook/<id> (id numérico) |
Endpoint validado do excluir:
DELETE /webhook/273
Resposta (200) retorna o objeto deletado completo.
7. Ações seguras vs disparadoras (para IA)
Seguras (pode rodar sem confirmação):
GET /webhook?pageNumber=1— listarGET /webhook/<hash_id>— detalheGET /flowbuilder/all/get,GET /whatsapp,GET /waoficial,GET /tags/list— auxiliares- Hover na lista, scroll, abrir modal Adicionar e cancelar
Disparadoras (exigem ordem explícita do administrador):
POST /webhook(criar)PUT /webhook/<id>(salvar config)PUT /webhook/active(toggle)DELETE /webhook/<id>(excluir — irreversível)- Botão Duplicar no kebab
8. Quirks e armadilhas
- Singular vs plural: rota da lista é
/webhooks(plural) mas todos os endpoints backend são/webhook(singular). Não confundir. idvshash_id: DELETE usa id numérico. URL pública usa hash_id. Não trocar.- Modal Adicionar sem URL: a doc antiga sugere preencher URL no momento de criar — não é assim. Cria só com nome, URL é gerada depois.
config: null≠ “não recebe”: webhook com mapping vazio AINDA RECEBE chamadas externas (contador sobe) — só não dispara fluxo. Por issotestetem 8 requests sem ter fluxo configurado.- Botão Salvar webhook não dispara request com mapping vazio: validação client-side impede. Pra capturar o PUT real, precisa preencher mínimo:
{celular}+{nome}+ Fluxo + Whatsapp. - Resposta
"ok"do toggle: PUT/webhook/activeretorna string"ok", não objeto JSON. Parsing precisa tratar isso. - Resposta
OK_SHOOTdo POST externo: URL pública/webhook/<user_id>/<hash_id>retorna a string literalOK_SHOOT(não JSON). Cliente externo que faz.json()quebra. webhookIdno body do toggle, não na URL: a URL é fixa/webhook/active, ID vai no payload.- Sidebar deploy entre v4.5.7 e v4.6.0: rótulo “Assistentes IA” virou só “IA” no v4.6.0 (na seção Criação) — pode causar quebra em scripts que filtram texto.
9. Snippets — comandos Playwright via CDP
Pressupõe sessão CDP ativa em
scripts/_out/cdp.txtapontando parahttp://localhost:9242com usuário já logado.
9.1 Listar webhooks
import { connect } from './_lib/connect.mjs';
const { page, browser } = await connect();
await page.goto('https://app.sagazchat.com/webhooks', { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(2000);
// network já mostra GET /webhook?pageNumber=1
9.2 Criar webhook
await page.locator('button').filter({ hasText: /^Adicionar$/i }).first().click();
await page.locator('[role="dialog"] input[name="name"]').first().fill('NOME');
await page.locator('[role="dialog"] button').filter({ hasText: /^Adicionar$/i }).first().click();
// não navega — webhook aparece na lista
9.3 Abrir editor de um webhook
const coord = await page.evaluate((name) => {
const t = Array.from(document.querySelectorAll('*')).find((el) => (el.textContent || '').trim() === name && el.children.length === 0);
if (!t) return null;
const r = t.getBoundingClientRect();
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
}, 'NOME');
await page.mouse.click(coord.x, coord.y);
// navega pra /webhook/<hash_id>
9.4 Excluir via kebab
// 1) acha linha do webhook
const row = await page.evaluate((name) => {
const t = Array.from(document.querySelectorAll('*')).find((el) => (el.textContent || '').trim() === name && el.children.length === 0);
if (!t) return null;
const r = t.getBoundingClientRect();
return { x: r.x, y: r.y };
}, 'NOME');
// 2) acha kebab na MESMA Y, mas X grande
const kebab = await page.evaluate((rowY) => {
const all = Array.from(document.querySelectorAll('.MuiIconButton-root, button'));
const found = all.find((b) => {
const r = b.getBoundingClientRect();
return r.x > 1300 && Math.abs(r.y - rowY) < 25 && r.width < 50 && r.height < 50;
});
if (!found) return null;
const r = found.getBoundingClientRect();
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
}, row.y);
await page.mouse.click(kebab.x, kebab.y);
await page.waitForTimeout(700);
await page.locator('[role="menuitem"], li, button').filter({ hasText: /^Excluir$/ }).first().click();
// pode ou não ter modal de confirmação — capturar com [role="dialog"]
9.5 Toggle ativar/desativar
await page.goto('https://app.sagazchat.com/webhook/<hash_id>', { waitUntil: 'domcontentloaded' });
await page.locator('button').filter({ hasText: /^(Desativar|Ativar)$/ }).first().click();
// dispara PUT /webhook/active body { status, webhookId }
10. Como uma IA inicia auditoria
- Garantir sessão CDP ativa (
scripts/_out/cdp.txt). - Confirmar empresa:
exemplo(mostrado no topo da sidebar abaixo do logo). - Listar webhooks via
/webhooks— confirmar quetesteevalidpayexistem e estão Ativo. - Não tocar nesses 2 webhooks. Pra qualquer experimento, criar LAB-WEBHOOK-* com nome único, validar, deletar pelo kebab → Excluir.
- Pra capturar PUT real do save de mapping, preencher Autocompletes do {celular}/{nome}/{email} digitando UM caminho qualquer + selecionar Fluxo (qualquer disponível em
/flowbuilder/all/get) + escolher 1 WhatsApp ou ativardistribuição(checkbox), depois clicar Salvar webhook. - Sempre olhar Network ao salvar — confirma PUT real e payload exato.
11. Scripts de auditoria criados
scripts/sagaz-webhooks-investigar.mjs— primeira tentativa (anti-sessão expirada).scripts/sagaz-webhooks-expand.mjs— varia 1 (locator falhou).scripts/sagaz-webhooks-expand-v2.mjs— varia 2 (mouse.click coord).scripts/sagaz-webhooks-expand-v3.mjs— final: clica MuiListItemButton de Automação.scripts/sagaz-webhooks-debug-auto.mjs— debug DOM.scripts/sagaz-webhooks-scroll-sidebar.mjs— rola sidebar.scripts/sagaz-webhooks-tela.mjs— abre/webhooks+ captura listing + endpoint.scripts/sagaz-webhooks-modal.mjs— modal de Adicionar.scripts/sagaz-webhooks-detalhe.mjs— abre detalhe via clique no nome.scripts/sagaz-webhooks-detalhe-completo.mjs— captura todas as seções + kebab.scripts/sagaz-webhooks-ciclo.mjs— POST + DELETE (LAB-WEBHOOK-TEMP, id 273).scripts/sagaz-webhooks-put-save.mjs— captura PUT /webhook/active (LAB-WEBHOOK-TEMP2, id 274).scripts/sagaz-webhooks-save-teste.mjs— tentativa de capturar PUT idempotente no teste (não disparou — config vazio).
12. Pendências para próxima auditoria
Capturar PUT real do Save com mapping completo— validado 2026-05-11 (LAB-WEBHOOK-FUNCIONAL id 276). Ver § 4.2.- Verificar Editar do kebab (renomear) — endpoint real e modal.
- Verificar Duplicar do kebab — gera novo hash_id ou copia o mesmo?
- Validar comportamento de webhook desativado — chamada externa retorna o quê (200, 4xx, ignorada)?
- Endpoint do botão “Adicionar campo” dentro do mapping — é só estado local até o Save, ou faz request?
- Campos adicionais não inclusos no PUT capturado — confirmar campo do payload onde vai (talvez
config.extraInputsou inputs.order >= 3?). LAB-WEBHOOK-FUNCIONAL salvou só com 3 inputs obrigatórios. - Itens da sidebar não auditados: Agendamentos (
/calendars), Campanhas (/phrase-lists), Conexões (/integrations— atenção ao naming!), N8N (/n8n), Prompts (/prompts/general), Assistente Interno (apareceu na sidebar em v4.6.0).