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:
- Immediately: responde na hora com o código configurado e o corpo
Workflow got started, e o resto do workflow continua rodando em segundo plano. - When Last Node Finishes: segura a conexão até o último node terminar e devolve a saída dele como resposta.
- Using 'Respond to Webhook' node: você escolhe o ponto exato do fluxo onde a resposta sai, colocando um node Respond to Webhook ali. Só o primeiro desses nodes a ser executado vale; os outros são ignorados.
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:
- 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). - Code: normaliza o payload e monta a chave de idempotência.
- Postgres: insere na tabela de eventos ignorando duplicata.
- 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):
- URL de teste configurada no provedor. A Test URL do n8n só escuta 120 segundos depois de você clicar em "Listen for test event". Funciona no seu teste, morre em produção, e você jura que "estava funcionando ontem". A URL de produção só existe com o workflow publicado.
- Dois workflows com o mesmo path. O n8n recusa registrar o mesmo path com o mesmo método duas vezes. Se você duplicou o fluxo pra testar e esqueceu o antigo ativo, o novo simplesmente não sobe.
- Não filtrar
fromMe. A Evolution API disparamessages.upserttambém pras mensagens que você mesmo enviou. Sem o filtro, seu bot responde à própria resposta, e o loop só para quando o número é bloqueado. - Confundir "success" com "entregue". A bolinha verde do Executions só diz que os nodes rodaram sem exceção. Um 200 do endpoint de envio da Evolution API significa "aceitei enfileirar", não "chegou no celular do cliente". Quem confirma entrega é o evento
MESSAGES_UPDATEcom a mudança de status. - Concorrência sem limite. No n8n self-hosted o
N8N_CONCURRENCY_PRODUCTION_LIMITvem como-1, ou seja, sem limite. Numa VPS de 2 GB, 40 execuções paralelas com Code node e HTTP Request derrubam o processo. Setar um valor (5 a 10 pra começar) faz o excedente esperar em fila FIFO em vez de matar a instância. - Banco do n8n engordando calado. A limpeza de execuções vem ligada por padrão, com 336 horas (14 dias) de idade máxima e 10.000 execuções de teto. Parece muito até você ligar
webhookBase64e passar a guardar áudio inteiro dentro do payload de cada execução. Se o backend é SQLite, o espaço apagado nem volta pro disco sem um VACUUM. - Nenhum Error Workflow configurado. Em Settings do workflow tem o campo Error workflow. Aponte pra um fluxo que começa com Error Trigger e manda um Telegram pra você. Sem isso, falha de execução automática é 100% silenciosa. Vale lembrar que isso avisa quando o fluxo roda e quebra, não quando ele para de receber evento; pra esse segundo caso você precisa de um monitor separado, no espírito do que descrevi em monitorar a Evolution API.
- n8n atrás de proxy sem
N8N_PROXY_HOPS. Se você usa lista de IP permitido no webhook e o n8n está atrás de Traefik ou nginx, todo request parece vir do proxy. A variável precisa refletir quantos proxies existem na frente.
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).