Runbook — Webhooks do Sagazchat

Manual operacional para uma IA ou pessoa criar, configurar e excluir webhooks no Sagazchat sem depender de tentativa visual cega.

8 min de leitura Atualizado em 14 de mai. de 2026

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, empresa exemplo). App em v4.6.0 (sidebar mostrava v4.5.7 antes do deploy do mesmo dia). Webhooks pré-existentes NA CONTA — NÃO DELETAR:

  • teste (Ativo, hash SFNoh67QaSXJI4xuTw5XkpPaONxXIKcQnXVzrbBFvB, 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 deletado
  • LAB-WEBHOOK-TEMP2 (id 274) — criado, desativado, deletado
  • LAB-WEBHOOK-TEMP3 (id 275, hash DFJVRJKvZDGLGLGCiSMyew0jiJZucwyvRxSLSpQHoX) — criado, alimentado via fetch JSON, deletado
  • LAB-WEBHOOK-FUNCIONAL (id 276, hash 4XZbDr9daITK25P1TyZjX2Jeq3TZUY0yBOtX5Da1Hz) — VIVO, configurado funcional: fluxo VENDA-SAGAZCHAT (id 540), WhatsApp exemplo (id 216). Alimentado via POST urlencoded daqui (OK_SHOOT 200). 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 — sem s final)
  • 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:

ColunaSignificado
NomeNome do webhook. Clicar no nome abre o editor.
StatusAtivo ou Desativado — controlado pelo botão Desativar/Ativar dentro do editor.
Requisições do mêsContador que zera mensalmente. Bate com a quota do plano (Basic 15k, Pro 30k).
Requisições totaisTotal histórico desde a criação.
AçõesKebab com 4 itens (ver § 6).

Topo da tela:

  • Contador N/30000 (ou N/15000 em 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:

  1. Abrir Automação › Webhooks.
  2. Clicar + Adicionar.
  3. Modal Adicionar abre com um único campo Nome e botões Cancelar / Adicionar.
  4. Preencher Nome.
  5. Clicar Adicionar.
  6. A UI NÃO redireciona automaticamente pro editor — o webhook aparece na lista com status Ativo e 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çãoTipoObrigatório
1Este é o link do seu webhookURL gerada + ícone de copiar
2Última requisição que este webhook recebeuCode preview JSON + botão Atualizar dados
3Selecione os campos do {celular} do seu clienteAutocomplete (caminho dentro do JSON recebido)sim
4Selecione os campos do {nome} do seu clienteAutocompletesim
5Selecione os campos do {email} do seu clienteAutocompletesim (sem certeza — [a validar])
6Selecione o FLUXO que será disparadoCombobox Escolha um fluxosim
7Canal de envioCombobox (default WhatsApp)sim
8Selecione o WHATSAPP ou ative a distribuiçãoCombobox Escolha um whatsapp + checkbox distribuiçãosim (1 dos 2)
9Selecione a etiqueta que deseja adicionarMulti-search Etiquetas (Opcional)não
10Selecione a etiqueta que deseja removerMulti-search Etiquetas (Opcional)não
11Campos adicionaisBotã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 webhook teste da conta tem config: 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:

CampoO que significaObservação
config.inputs[]Lista dos 3 mappings obrigatóriosNote a ORDEM: 0=nome, 1=email, 2=celular — diferente da ordem visual da tela
config.inputs[].orderPosição (0–2)Apenas 3 entradas; sempre nessas chaves
config.inputs[].keyValueVariável Sagazchatnome, email, celular (string literal)
config.inputs[].dataChave do JSON recebidoCom prefixo # (ex: #telefone aponta pra chave telefone do payload)
config.idFlowID do fluxo a dispararVem de GET /flowbuilder/all/get
config.keysFullTodas as chaves do JSON da última requisiçãoBackend mantém pra UI re-renderizar opções
config.tagNameEtiqueta a aplicarstring ou null
config.tagNameRemoveEtiqueta a removerstring ou null
config.versionSchema version2 (atual em 2026-05-11)
webhookIdID numérico do webhookNÃO hash_id
whatsappIdID do canal WhatsApp não-oficialOU null
waOficialIdID do canal WhatsApp OficialOU null
isMultWhatsappModo distribuiçãofalse = canal específico; true = distribuir
multWhatsapp / multWAOficialListas pra modo distribuição[] quando isMultWhatsapp: false
waOficialTemplate*Template WA OficialSó 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 (campo hash_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:

ItemAçãoEndpoint inferido
EditarProvável: renomear (modal).PUT /webhook/<id> com body { "name": "..." }[a validar]
DuplicarCria cópia do webhook (gera novo hash_id).POST /webhook/clone/<id>[a validar]
ConfiguraçõesAtalho equivalente a clicar no nome — abre editor.
ExcluirRemove 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 — listar
  • GET /webhook/<hash_id> — detalhe
  • GET /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.
  • id vs hash_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 isso teste tem 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/active retorna string "ok", não objeto JSON. Parsing precisa tratar isso.
  • Resposta OK_SHOOT do POST externo: URL pública /webhook/<user_id>/<hash_id> retorna a string literal OK_SHOOT (não JSON). Cliente externo que faz .json() quebra.
  • webhookId no 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.txt apontando para http://localhost:9242 com 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

  1. Garantir sessão CDP ativa (scripts/_out/cdp.txt).
  2. Confirmar empresa: exemplo (mostrado no topo da sidebar abaixo do logo).
  3. Listar webhooks via /webhooks — confirmar que teste e validpay existem e estão Ativo.
  4. Não tocar nesses 2 webhooks. Pra qualquer experimento, criar LAB-WEBHOOK-* com nome único, validar, deletar pelo kebab → Excluir.
  5. 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 ativar distribuição (checkbox), depois clicar Salvar webhook.
  6. 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 completovalidado 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.extraInputs ou 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).