Como criar um bot de WhatsApp com Node.js: respostas automáticas
Um bot de WhatsApp com Node.js resolve o tipo de pergunta que chega repetida o dia inteiro: horário de funcionamento, status do pedido, formas de pagamento, como falar com alguém. Em vez de uma pessoa responder a mesma coisa cem vezes, um webhook recebe a mensagem, uma regra decide a resposta e uma API devolve o texto em segundos.
A boa notícia é que a arquitetura é simples. Você precisa de um servidor Express para receber as mensagens, uma máquina de estados para saber em que ponto da conversa cada contato está e uma função que chama a API de envio. Nada de framework pesado nem de infraestrutura cara para começar.
Este passo a passo monta exatamente isso, com código funcional em cada etapa: do menu inicial ao transbordo para um atendente humano, passando por envio de imagem e por como medir se o bot está ajudando.
O que o bot vai fazer: menu, horário e FAQ
Antes de abrir o editor, defina o escopo. Um bot que tenta resolver tudo frustra o usuário; um bot que resolve três coisas bem economiza horas da equipe. O exemplo deste tutorial cobre o fluxo mais comum em pequenos negócios:
- Saudação com menu numerado: a primeira mensagem de qualquer contato recebe as opções disponíveis.
- Horário de funcionamento: resposta fixa, sem lógica.
- Dúvidas frequentes: um submenu com perguntas e respostas prontas.
- Falar com um atendente: o bot para de responder e avisa a equipe.
Dois comportamentos deixam o fluxo robusto: a palavra “menu” sempre volta ao início, e uma conversa parada por mais de 30 minutos recomeça do zero. Sem isso, o usuário fica preso em um estado antigo e o bot parece quebrado.
Desenhe esse fluxo em papel ou em uma ferramenta de diagrama antes de codificar. Cada caixa vira um estado, cada seta vira uma condição, e o código da seção de máquina de estados fica quase uma transcrição do desenho.
Escolhendo a API e criando o projeto do bot de WhatsApp com Node.js
Seu código não fala com o WhatsApp diretamente; ele fala com um provedor que expõe o envio e o recebimento por HTTP. Existem duas famílias de serviço:
| Opção | Como conecta | Quando faz sentido |
|---|---|---|
| API oficial (WhatsApp Business Platform) | Conta comercial verificada pela Meta, mensagens-modelo aprovadas para iniciar conversas | Operações maiores, exigência de conformidade, envio ativo em volume |
| APIs baseadas em sessão (QR Code) | Você conecta um número comum lendo um QR Code, como no WhatsApp Web | Testes, bots de atendimento receptivo, pequenos volumes, começar hoje |
A APIBrasil oferece as duas famílias no mesmo gateway, com o mesmo padrão de chamada: POST com JSON, header Authorization: Bearer e resposta com error, message e data. Para o canal por sessão, cada número conectado é um “dispositivo” com um token próprio, enviado no header DeviceToken.
Se é a sua primeira vez com essa API, o tutorial sobre como enviar mensagens de WhatsApp com Node.js usando a APIBrasil é um bom ponto de partida para o primeiro envio.
Com o número conectado e os tokens em mãos, crie o projeto:
mkdir bot-whatsapp && cd bot-whatsapp
npm init -y
npm install express dotenv
# .env (no .gitignore)
APIBRASIL_TOKEN=seu-bearer-token
APIBRASIL_DEVICE_TOKEN=token-do-dispositivo-conectado
NUMERO_EQUIPE=5511999999999
PORT=3000
Adicione "type": "module" ao package.json para usar import. O projeto terá três arquivos: server.js (webhook), fluxo.js (máquina de estados) e whatsapp.js (envio).
Recebendo mensagens via webhook com Express
O webhook é uma rota pública que o provedor chama com um POST sempre que chega uma mensagem no número conectado. No canal Baileys da APIBrasil, o corpo chega como uma lista de eventos, cada um com session, channelName e data; o remetente está em data.key.remoteJid e o texto em data.message.conversation.
// server.js
import 'dotenv/config';
import express from 'express';
import { processarMensagem } from './fluxo.js';
const app = express();
app.use(express.json({ limit: '2mb' }));
app.post('/webhook/whatsapp', (req, res) => {
// Confere a origem: o provedor envia o token do dispositivo no header "device"
if (req.get('device') !== process.env.APIBRASIL_DEVICE_TOKEN) {
return res.sendStatus(403);
}
// Responde já: o provedor não precisa esperar o bot pensar
res.sendStatus(200);
const eventos = Array.isArray(req.body) ? req.body : [req.body];
for (const evento of eventos) {
const dados = evento?.data;
if (!dados?.key || dados.key.fromMe) continue; // ignora o que o próprio bot enviou
if (!dados.key.remoteJid?.endsWith('@s.whatsapp.net')) continue; // ignora grupos e status
const numero = dados.key.remoteJid.replace('@s.whatsapp.net', '');
const texto = dados.message?.conversation ?? dados.message?.extendedTextMessage?.text ?? '';
processarMensagem({ numero, texto: texto.trim(), nome: dados.pushName }).catch(console.error);
}
});
app.listen(process.env.PORT, () => console.log(`Webhook em http://localhost:${process.env.PORT}`));
Três decisões nesse código evitam dores de cabeça. Responder 200 antes de processar impede que o provedor considere a entrega falha por timeout. Ignorar fromMe evita que o bot responda às próprias mensagens em loop. E tratar o corpo como lista, mesmo quando vier um objeto só, deixa o endpoint tolerante a variações do provedor.
Para testar na sua máquina, exponha a porta com o ngrok (ngrok http 3000) e cadastre a URL gerada, com o caminho /webhook/whatsapp, no campo de webhook de mensagens do dispositivo no painel da APIBrasil. Mande um “oi” de outro celular e veja o evento chegar no terminal.

Máquina de estados simples para o fluxo da conversa
Uma conversa tem contexto: quando o usuário digita “2”, o significado depende do que o bot perguntou antes. A forma mais limpa de guardar esse contexto é uma máquina de estados: cada contato tem um estado atual (INICIO, MENU, FAQ, HUMANO) e cada mensagem é interpretada à luz dele.
// fluxo.js
import { enviarTexto } from './whatsapp.js';
import { transferirParaHumano } from './atendimento.js';
const sessoes = new Map(); // numero -> { estado, atualizadoEm } (troque por Redis em produção)
const TEMPO_SESSAO = 30 * 60 * 1000;
const MENU = 'Olá! Sou o assistente virtual. Digite o número da opção:\n' +
'1 - Horário de funcionamento\n2 - Dúvidas frequentes\n3 - Falar com um atendente';
const FAQ = [
{ pergunta: 'Como rastrear meu pedido?', resposta: 'Use o código enviado por e-mail na página de rastreio.' },
{ pergunta: 'Quais formas de pagamento?', resposta: 'Pix, cartão de crédito e boleto.' },
{ pergunta: 'Qual é o prazo de entrega?', resposta: 'Depende da região; o prazo aparece no fechamento do pedido.' },
];
const MENU_FAQ = 'Escolha a dúvida:\n' + FAQ.map((f, i) => `${i + 1} - ${f.pergunta}`).join('\n');
function obterEstado(numero) {
const sessao = sessoes.get(numero);
const expirou = !sessao || Date.now() - sessao.atualizadoEm > TEMPO_SESSAO;
return expirou ? 'INICIO' : sessao.estado;
}
export async function processarMensagem({ numero, texto, nome }) {
const estado = obterEstado(numero);
let resposta;
let proximo = estado;
if (texto.toLowerCase() === 'menu' || estado === 'INICIO') {
resposta = MENU;
proximo = 'MENU';
} else if (estado === 'MENU') {
if (texto === '1') {
resposta = 'Atendemos de segunda a sexta, das 9h às 18h. Digite "menu" para voltar.';
} else if (texto === '2') {
resposta = MENU_FAQ;
proximo = 'FAQ';
} else if (texto === '3') {
resposta = 'Certo! Um atendente vai continuar a conversa por aqui em instantes.';
proximo = 'HUMANO';
await transferirParaHumano(numero, nome);
} else {
resposta = 'Não entendi. ' + MENU;
}
} else if (estado === 'FAQ') {
const item = FAQ[Number(texto) - 1];
resposta = item ? `${item.resposta}\n\nDigite "menu" para voltar.` : 'Opção inválida. ' + MENU_FAQ;
} else if (estado === 'HUMANO') {
return; // o bot fica em silêncio enquanto a pessoa atende
}
sessoes.set(numero, { estado: proximo, atualizadoEm: Date.now() });
await enviarTexto(numero, resposta);
}
O Map funciona para desenvolvimento e para um único processo. Assim que o bot rodar em mais de uma instância ou precisar sobreviver a reinícios, mova as sessões para o Redis com um TTL de 30 minutos, o que também elimina a verificação manual de expiração. Subir um Redis local leva um comando se você já tem o Docker instalado e rodando containers.
Para adicionar uma opção nova, você cria um estado, uma condição e uma resposta. Essa previsibilidade é a vantagem da máquina de estados sobre uma cadeia de if baseada só no texto.
Enviando respostas e mídia pela API
O envio concentra-se em um módulo que monta os headers uma vez e trata o retorno. O número vai no formato internacional sem símbolos (5531999999999), que é exatamente o que veio no remoteJid. O envio de mídia usa a mesma estrutura, com o arquivo em Base64:
// whatsapp.js
import { readFile } from 'node:fs/promises';
const BASE = 'https://gateway.apibrasil.io/api/v2/evolution/message';
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.APIBRASIL_TOKEN}`,
DeviceToken: process.env.APIBRASIL_DEVICE_TOKEN,
};
async function chamar(rota, corpo) {
const resposta = await fetch(`${BASE}/${rota}`, { method: 'POST', headers, body: JSON.stringify(corpo) });
const dados = await resposta.json().catch(() => ({}));
// A API pode responder 200 com error: true; 402 indica saldo/plano insuficiente
if (!resposta.ok || dados.error) {
throw new Error(`Falha no envio (${resposta.status}): ${dados.message ?? 'sem detalhes'}`);
}
return dados;
}
export function enviarTexto(number, text) {
// "composing" mostra o "digitando..." por 1 segundo antes de enviar
return chamar('sendText', { number, text, options: { delay: 1, presence: 'composing' } });
}
export async function enviarImagem(number, caminho, caption) {
const media = await readFile(caminho, { encoding: 'base64' });
return chamar('sendMedia', {
number, mediatype: 'image', mimetype: 'image/png', caption, media, fileName: 'imagem.png',
});
}
Com isso, mandar um cardápio ou uma tabela de preços é uma linha: await enviarImagem(numero, './cardapio.png', 'Nosso cardápio de hoje'). Para PDF, troque mediatype para document e ajuste o mimetype e o fileName.
Dois cuidados de produção: nunca envie para quem não iniciou a conversa (isso é o caminho mais curto para o número ser bloqueado) e coloque os envios em uma fila quando houver picos, para não disparar dezenas de chamadas simultâneas. Se o seu backend é PHP, a mesma lógica vale; o tutorial sobre como enviar mensagens de texto com a API do WhatsApp em PHP e Guzzle mostra a chamada equivalente.
Passando para um humano e medindo o atendimento
Nenhum bot resolve tudo, e a pior experiência possível é um usuário preso em um menu sem saída. A opção 3 do exemplo muda o estado para HUMANO, silencia o bot para aquele contato e avisa a equipe. O módulo de atendimento registra o pedido e notifica o número interno:
// atendimento.js
import { enviarTexto } from './whatsapp.js';
export async function transferirParaHumano(numero, nome) {
// 1. Registre o ticket no seu banco, CRM ou planilha
console.log(JSON.stringify({ evento: 'transbordo', numero, nome, em: new Date().toISOString() }));
// 2. Avise a equipe no número interno (ou em um grupo)
await enviarTexto(
process.env.NUMERO_EQUIPE,
`Novo atendimento: ${nome ?? 'contato'} (${numero}) pediu para falar com um atendente.`
);
}
Quando o atendimento terminar, o atendente precisa devolver o contato ao bot. Uma rota simples resolve: POST /sessoes/:numero/liberar, protegida por token interno, que apaga a sessão daquele número. Na próxima mensagem, o fluxo recomeça pelo menu.
Depois que o bot está no ar, a pergunta passa a ser se ele entrega resultado. Registre cada evento em log estruturado (mensagem recebida, opção escolhida, transbordo, tempo até a primeira resposta humana) e acompanhe três números: a fração de conversas resolvidas sem humano, o tempo médio até o atendente assumir e os horários de pico.
Um monitoramento de atendimento no WhatsApp com esses indicadores mostra onde o menu confunde, quais dúvidas merecem entrar no FAQ e se a equipe está dando conta dos transbordos.
Perguntas frequentes
Preciso da API oficial do WhatsApp para criar um bot?
Não para começar. APIs por sessão (QR Code) permitem testar e operar bots receptivos em pequenos volumes no mesmo dia. A API oficial faz sentido quando você precisa iniciar conversas em escala com mensagens-modelo, de conformidade formal ou de garantias de continuidade que só a plataforma da Meta oferece.
Como evitar que o número do bot seja bloqueado?
Responda apenas a quem escreveu primeiro, nunca dispare em massa para listas, respeite quem pede para parar e use um pequeno atraso entre mensagens, como o delay do exemplo. Um número com histórico de uso normal, conectado em um aparelho estável, tem muito menos problemas.
O bot funciona em grupos?
O código ignora mensagens de grupos (JIDs terminados em @g.us) de propósito, porque responder a tudo em um grupo gera ruído. Dá para adaptar, filtrando por menção ao bot ou por um comando específico, mas trate como um fluxo separado.
Posso usar inteligência artificial nas respostas?
Sim, e o encaixe natural é o caso “não entendi”: em vez de repetir o menu, envie o texto a um modelo de linguagem com o contexto do negócio e devolva a resposta. Mantenha a máquina de estados para ações que exigem precisão, como transbordo e coleta de dados, e registre as respostas geradas para revisão.
Quanto tempo leva para colocar um bot desses em produção?
Um bot de WhatsApp com Node.js como o deste tutorial roda em uma tarde. O que consome tempo é o conteúdo (escrever bem as respostas do FAQ) e a operação: Redis para sessões, fila para envios, logs e alguém acompanhando os indicadores nas primeiras semanas.
Próximos passos
Você tem um bot de WhatsApp com Node.js que recebe mensagens por webhook, mantém o contexto de cada conversa, responde com texto e imagem e sabe a hora de chamar uma pessoa.
Para evoluir, troque o Map por Redis, coloque os envios em uma fila, adicione um estado de coleta de dados (nome, pedido, e-mail) e use os indicadores de atendimento para decidir o que automatizar em seguida. Cada melhoria é um estado a mais na mesma estrutura que você já entende.