Voltar pro blog json-p.org

Pilar1 · Infra de WhatsApp que não cai Publicado26 de agosto de 2026 AutorElton

Webhook WhatsApp + n8n: automação de venda sem virar dev

Webhook WhatsApp + n8n na prática: modo de resposta certo, fila em tabela, idempotência e alerta pra automação de vendas não perder evento.

Você assistiu o vídeo e montou o fluxo: node Webhook, um IF pra filtrar, um HTTP Request devolvendo mensagem pela Evolution API. Clicou em "Listen for test event", mandou um "oi" do seu próprio celular, viu o JSON aparecer na tela do editor. Funcionou de primeira. Ativou o workflow e foi dormir achando que estava resolvido.

Duas semanas depois um lead reclama que mandou mensagem e ninguém respondeu. Você abre a aba Executions procurando o erro e acontece uma de duas coisas, as duas ruins: ou não existe execução nenhuma naquele horário (o evento simplesmente não chegou), ou existe, com bolinha verde de sucesso, e mesmo assim a pessoa não recebeu nada. Teve dia também que o cliente recebeu a mesma mensagem três vezes seguidas e achou que era robô quebrado. Era.

Nada disso é bug do n8n. É o desenho padrão que todo tutorial ensina: um workflow único que recebe e processa na mesma respiração. Enquanto o volume é um "oi" por hora, funciona lindamente. Quando você faz uma live e chegam 40 eventos em 20 segundos, ou quando o provedor reenvia o mesmo evento porque achou que você não respondeu, o desenho cobra a conta em venda perdida.

Dá pra montar isso no n8n sem virar dev, sim. Mas "sem virar dev" não é "sem entender". São três coisas pra entender: quando o seu webhook responde, onde o evento fica guardado antes de ser processado, e quem te avisa quando falha. É isso que separa a automação que aguenta o mês da automação que some com venda em silêncio.

A resposta curta: automação de vendas no n8n começa pelo jeito que o webhook responde

Automação de vendas com webhook de WhatsApp no n8n só fica confiável quando você quebra o fluxo em dois workflows. O primeiro é um receptor burro: recebe o POST, responde 200 imediatamente, normaliza o payload, grava numa tabela com chave única e acaba. Ele tem que durar milissegundos e não pode chamar IA, nem API de pagamento, nem nada que dependa de terceiro. O segundo é o processador: lê a tabela em ciclos curtos, aplica a régua de venda, manda a mensagem e marca o registro como feito.

O motivo é simples. Quem manda o evento (Evolution API, Cloud API, Hotmart) não quer conversa, quer um 200 rápido. Todo segundo que você gasta antes de responder é um segundo em que a conexão do provedor está segurada e o retry dele está armado apontando pra você. O resto do post é como montar essas duas peças no n8n, com o payload e o SQL de verdade.

Como o evento chega até o n8n

Do lado da Evolution API, você aponta a instância pro seu n8n. O corpo dessa chamada mudou entre versões, então confira o Swagger da sua instância antes de copiar: nas versões mais novas da v2 os campos vão aninhados dentro de um objeto webhook, e se você mandar plano toma um 400 com "instance requires property webhook". Na forma aninhada fica assim:

curl -X POST https://evo.seudominio.com/webhook/set/vendas01 \
  -H "apikey: SUA_APIKEY" \
  -H "Content-Type: application/json" \
  -d '{
    "webhook": {
      "enabled": true,
      "url": "https://n8n.seudominio.com/webhook/wa-in",
      "webhookByEvents": false,
      "webhookBase64": false,
      "events": ["MESSAGES_UPSERT", "CONNECTION_UPDATE"]
    }
  }'

Duas decisões escondidas aí. A primeira: a lista de eventos disponíveis tem cerca de vinte opções, incluindo PRESENCE_UPDATE, CHATS_UPDATE e CONTACTS_UPDATE. Marcar tudo "por garantia" é o jeito mais rápido de afogar o n8n com evento que não vira venda nenhuma. Pra funil de vendas, MESSAGES_UPSERT (mensagem chegando) e CONNECTION_UPDATE (a instância caiu ou reconectou) resolvem quase tudo.

A segunda: o webhookByEvents. Se você ligar isso, a Evolution API acrescenta o nome do evento no fim da URL, com hífen no lugar do underline. Sua URL /webhook/wa-in vira /webhook/wa-in/messages-upsert, e o node Webhook do n8n, que está escutando o path exato wa-in, para de receber qualquer coisa. É uma das causas clássicas de webhook que não dispara: o evento sai, mas bate num path que não existe.

O payload que chega no MESSAGES_UPSERT é mais ou menos este (mascarado):

{
  "event": "messages.upsert",
  "instance": "vendas01",
  "data": {
    "key": {
      "remoteJid": "[email protected]",
      "fromMe": false,
      "id": "3AF1B2C3D4E5F6A7B8C9"
    },
    "pushName": "Marcos",
    "message": { "conversation": "quero saber do combo" },
    "messageType": "conversation",
    "messageTimestamp": 1755900000
  },
  "date_time": "2026-08-20T14:03:11.000Z",
  "server_url": "https://evo.seudominio.com"
}

Detalhe que trava muita gente na primeira hora: o node Webhook do n8n não entrega esse objeto na raiz do item. Ele entrega um item com headers, params, query e body, e o JSON acima está dentro de body. Se sua expressão está lendo {{ $json.data.key.id }} e vem vazio, é isso: o caminho certo é {{ $json.body.data.key.id }}.

As três respostas possíveis do node Webhook (e qual serve pra venda)

No node Webhook existe um campo chamado Respond, com três comportamentos bem diferentes:

Pra receptor de venda, você quer Immediately, ou então Respond to Webhook como segundo node do fluxo (útil quando precisa devolver um corpo específico que o provedor exige). O que quebra é a opção do meio combinada com fluxo comprido: se o seu workflow chama a OpenAI, consulta a Hotmart e só então responde, o provedor fica pendurado 6, 10, 20 segundos esperando.

Alguns números concretos pra dimensionar isso. No n8n Cloud, requisição que passa de 100 segundos toma 524 do Cloudflare na cara, e a própria doc recomenda quebrar em dois webhooks nesse caso. A URL de teste (aquela do "Listen for test event") escuta por 120 segundos e depois morre, então ela nunca deve estar configurada no provedor. E a Hotmart, do outro lado do funil, reenvia o postback com erro até cinco vezes, ou até receber uma resposta positiva do seu servidor. Ou seja: cada segundo a mais que você demora pra responder aumenta a chance de receber o mesmo evento duplicado, o que na prática vira mensagem repetida pro lead. Se você ainda não trata isso, o assunto é idempotência de webhook, e é obrigatório aqui.

Tem uma pegadinha no Respond to Webhook que quase ninguém conta: erro que acontece antes dele vira 500 pro provedor, e o provedor reenvia. Erro que acontece depois não vira nada, porque a resposta 200 já saiu. O provedor vai marcar aquele evento como entregue com sucesso e nunca mais mandar. Responder cedo é a decisão certa, mas ela transfere a responsabilidade da durabilidade do evento do provedor pra você. É exatamente por isso que a gravação vem antes de qualquer lógica de negócio.

O receptor: quatro nodes e mais nada

O workflow de recepção inteiro:

  1. Webhook: método POST, path wa-in, Respond = Immediately, Response Code 200. Ative Header Auth (a URL sozinha não é segredo, ela vaza em log de proxy).
  2. Code: normaliza o payload e monta a chave de idempotência.
  3. Postgres: insere na tabela de eventos ignorando duplicata.
  4. Fim. Sem IF, sem HTTP Request, sem IA.

O Code node é o único bloco de JavaScript do post inteiro, e é o preço de normalizar o payload num formato que o resto da sua operação entende:

// Code node · modo "Run Once for All Items"
const out = [];

for (const item of $input.all()) {
  const b = item.json.body;             // o node Webhook entrega tudo dentro de body
  if (b.event !== 'messages.upsert') continue;

  const d = b.data || {};
  const k = d.key || {};
  if (k.fromMe) continue;               // mensagem enviada por você, não pelo lead

  const texto =
    d.message?.conversation ??
    d.message?.extendedTextMessage?.text ??
    null;

  out.push({
    json: {
      evento_id: b.instance + ':' + k.id,   // chave de idempotência
      instancia: b.instance,
      telefone: (k.remoteJid || '').split('@')[0],
      nome: d.pushName || null,
      texto: texto,
      tipo: d.messageType || null,
      recebido_em: new Date((d.messageTimestamp || 0) * 1000).toISOString(),
      bruto: b
    }
  });
}

return out;

A linha que mais importa é a do evento_id. O key.id é o identificador da mensagem gerado pelo WhatsApp, e ele é o mesmo em toda reentrega do mesmo evento. Concatenado com o nome da instância, vira uma chave estável que você pode usar como unique no banco. Com isso, reenvio duplicado deixa de ser um problema de lógica e vira um problema resolvido pelo próprio Postgres:

create table evento_whatsapp (
  evento_id    text primary key,
  instancia    text not null,
  telefone     text not null,
  texto        text,
  payload      jsonb not null,
  status       text not null default 'novo',
  tentativas   int  not null default 0,
  recebido_em  timestamptz not null default now()
);

-- node Postgres, operação Execute Query
insert into evento_whatsapp (evento_id, instancia, telefone, texto, payload)
values ($1, $2, $3, $4, $5::jsonb)
on conflict (evento_id) do nothing;

Se o provedor mandar o mesmo evento cinco vezes, você grava uma. E se seu processador cair no meio da madrugada, o evento continua lá, com status novo, esperando. Nada se perde porque o n8n reiniciou.

O processador: onde a régua de venda realmente mora

O segundo workflow começa com um Schedule Trigger de 30 segundos, pega um lote de eventos pendentes e trabalha em cima deles. O pulo do gato é como reservar o lote: se você só faz um SELECT e depois um UPDATE, duas execuções sobrepostas pegam o mesmo evento e o lead recebe mensagem dobrada. O Postgres resolve isso numa query só:

update evento_whatsapp
   set status = 'processando',
       tentativas = tentativas + 1
 where evento_id in (
   select evento_id
     from evento_whatsapp
    where status = 'novo'
    order by recebido_em
      for update skip locked
    limit 20
 )
returning *;

O for update skip locked faz a execução seguinte pular as linhas que a anterior já reservou, sem travar nem esperar. Daí em diante é n8n normal: um Switch roteando por tipo de evento ou por estado do lead, um HTTP Request mandando a mensagem pela Evolution API, e um último Postgres marcando status = 'enviado'. Se o envio falhar, você marca status = 'novo' de novo e deixa o próximo ciclo tentar, até um teto de tentativas (três é um número honesto; acima disso normalmente o problema não é transitório).

Uma pergunta que sempre aparece: por que não usar o node Wait como fila? Porque ele resolve outro problema. O Wait é ótimo pra régua de tempo ("espera 15 minutos e manda o follow-up"), mas cada espera é uma execução viva dentro do banco do n8n, e a documentação é explícita em dizer que execuções com status waiting não são elegíveis pra limpeza automática. Fila de negócio numa tabela sua é auditável: você roda select status, count(*) from evento_whatsapp group by 1 e sabe em cinco segundos quantas vendas estão paradas. Com o Wait, você sabe olhando execução por execução na tela.

Esse desenho de dois estágios (um receptor burro na frente, o processamento atrás, com estado em tabela própria) é o mesmo que eu uso nos dois SaaS que opero nessa área, o whatspix no funil de WhatsApp e o recupera na recuperação de venda. A diferença é que lá o receptor é um serviço em Node e a fila tem worker dedicado. A mecânica é idêntica, e no n8n ela cabe em oito nodes.

O que dá errado

Lista do que eu já vi quebrar (algumas por culpa minha):

Fechando

A automação de venda no WhatsApp com n8n não falha por falta de node bonito. Ela falha porque o fluxo padrão trata o webhook como se fosse uma conversa, quando ele é uma entrega de encomenda: assina o recibo primeiro, abre a caixa depois. Responde rápido, grava com chave única, processa num segundo workflow lendo uma tabela sua, e coloca um Error Workflow apitando quando algo estoura.

São oito nodes e uma tabela de sete colunas. Não precisa virar dev pra montar, mas precisa entender por que cada peça está ali, senão daqui a dois meses você vai estar de novo olhando pra uma execução verde e um cliente dizendo que nunca recebeu nada.

Fontes primárias consultadas em agosto de 2026: documentação do n8n (node Webhook e seus problemas comuns, node Respond to Webhook, node Wait, tratamento de erros, controle de concorrência e dados de execução), documentação de webhooks da Evolution API (campos de configuração, lista de eventos e comportamento do byEvents) e documentação de webhook/postback para desenvolvedores da Hotmart (códigos de resposta e política de reenvio).