Tutorial: webhook WhatsApp em Node.js (código completo)
Vou mostrar do zero como receber mensagem WhatsApp via Cloud API da Meta usando Node.js + Express, validar a assinatura HMAC pra segurança e processar diferentes tipos de mensagem (texto, áudio, imagem, status). Código testado em produção, pronto pra copiar.
Pré-requisitos
- Node.js 18+ instalado.
- Conta Meta Business com WABA configurada.
- App criado em
developers.facebook.comcom produto WhatsApp adicionado. - Você tem em mãos:
WHATSAPP_TOKEN(System User),APP_SECRET,PHONE_NUMBER_ID,VERIFY_TOKEN(você cria). - Servidor com HTTPS público (vamos usar Render no exemplo — funciona em Railway, Fly.io, AWS, etc.).
Passo 1 — Setup do projeto
mkdir wa-webhook && cd wa-webhook npm init -y npm install express # crypto já vem no Node core, não precisa instalar touch server.js .env
Arquivo .env:
WHATSAPP_TOKEN=EAAxxxx... APP_SECRET=abc123... PHONE_NUMBER_ID=109876543210987 VERIFY_TOKEN=meu_token_secreto_aleatorio_xyz PORT=3000
Passo 2 — Endpoint de verificação (GET)
Quando você cadastra o webhook no Business Manager, a Meta envia um GET com hub.challenge. Seu endpoint precisa retornar exatamente esse valor de volta, autenticando que você "é o dono" do endpoint.
// server.js
import express from 'express';
import crypto from 'crypto';
const app = express();
// IMPORTANTE: precisa do raw body pra validar assinatura depois
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
const { WHATSAPP_TOKEN, APP_SECRET, PHONE_NUMBER_ID, VERIFY_TOKEN, PORT = 3000 } = process.env;
// Verificação inicial do webhook
app.get('/webhook', (req, res) => {
const mode = req.query['hub.mode'];
const token = req.query['hub.verify_token'];
const challenge = req.query['hub.challenge'];
if (mode === 'subscribe' && token === VERIFY_TOKEN) {
console.log('Webhook verificado');
return res.status(200).send(challenge);
}
return res.sendStatus(403);
});
Passo 3 — Endpoint de recebimento (POST)
Aqui chegam os eventos: mensagem nova, status de envio, mudança de perfil, etc. Sempre responda 200 rapidamente (em <5 segundos), senão a Meta retenta e pode banir seu webhook.
// Recepção de eventos
app.post('/webhook', (req, res) => {
// 1) Validar assinatura antes de qualquer coisa
if (!verificarAssinatura(req)) {
console.warn('Assinatura inválida — rejeitando');
return res.sendStatus(401);
}
// 2) Responder 200 já (processar async)
res.sendStatus(200);
// 3) Processar evento
processarEvento(req.body).catch(err => console.error('Erro processando:', err));
});
function verificarAssinatura(req) {
const sig = req.get('x-hub-signature-256');
if (!sig) return false;
const esperado = 'sha256=' + crypto
.createHmac('sha256', APP_SECRET)
.update(req.rawBody)
.digest('hex');
// crypto.timingSafeEqual evita timing attack
try {
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(esperado));
} catch { return false; }
}
Passo 4 — Processar a mensagem
O payload da Meta tem estrutura aninhada. Aqui está o parser pros 4 tipos mais comuns:
async function processarEvento(body) {
// Payload: { object, entry: [{ changes: [{ value: { ... } }] }] }
const entry = body.entry?.[0];
const change = entry?.changes?.[0]?.value;
if (!change) return;
// a) Mensagem recebida
const msgs = change.messages || [];
for (const msg of msgs) {
const from = msg.from; // número do remetente
const id = msg.id;
if (msg.type === 'text') {
const texto = msg.text.body;
console.log(`📩 ${from}: ${texto}`);
await responder(from, `Recebi: "${texto}"`);
}
else if (msg.type === 'image' || msg.type === 'audio' || msg.type === 'video' || msg.type === 'document') {
const mediaId = msg[msg.type].id;
console.log(`📷 ${from} mandou ${msg.type} id=${mediaId}`);
// Pra baixar o arquivo: GET https://graph.facebook.com/v21.0/{mediaId}
// (com Bearer token) → retorna URL com TTL curto pra download
}
else if (msg.type === 'button' || msg.type === 'interactive') {
// Botão de template ou lista interativa clicado
console.log(`🔘 ${from} clicou em botão`);
}
}
// b) Status de envio (delivered, read, failed)
const statuses = change.statuses || [];
for (const st of statuses) {
console.log(`✓ status msg=${st.id} status=${st.status}`);
}
}
Passo 5 — Enviar mensagem de volta
async function responder(to, texto) {
const url = `https://graph.facebook.com/v21.0/${PHONE_NUMBER_ID}/messages`;
const body = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
to,
type: 'text',
text: { body: texto, preview_url: false }
};
const r = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${WHATSAPP_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
});
if (!r.ok) {
console.error('Erro enviando:', r.status, await r.text());
}
}
app.listen(PORT, () => console.log(`🟢 Webhook ouvindo na porta ${PORT}`));
Passo 6 — Deploy
Render é o mais simples pra testar (free tier suficiente). Alternativas: Railway, Fly.io, AWS Lambda.
- Crie repo no GitHub com o código.
- Em
render.com: New → Web Service → conecte ao repo. - Build command:
npm install. Start command:node server.js. - Adicione as variáveis de ambiente (WHATSAPP_TOKEN, APP_SECRET, etc.).
- Deploy. Pegue a URL pública (ex:
https://wa-webhook.onrender.com).
Passo 7 — Cadastrar webhook no Business Manager
developers.facebook.com→ seu app → WhatsApp → Configuration.- Em "Webhook", clique Configure.
- Callback URL:
https://wa-webhook.onrender.com/webhook. - Verify Token: o mesmo valor de
VERIFY_TOKENdo seu .env. - Clique Verify and Save. Meta faz GET → seu endpoint responde challenge → cadastrado.
- Em "Webhook fields", clique Manage e ative pelo menos
messagesemessage_template_status_update.
Testando
- Mande mensagem do seu celular pessoal pro número WhatsApp da empresa.
- Logs do servidor devem mostrar:
📩 5511...: oi teste. - Você deve receber de volta:
Recebi: "oi teste".
Erros comuns
- 401 do webhook validation: VERIFY_TOKEN no .env diferente do cadastrado no BM.
- Assinatura inválida: esqueceu de configurar
verify: (req, _res, buf) => { req.rawBody = buf }no express.json. - Timeout 5s: processou síncrono. Sempre responda
res.sendStatus(200)imediatamente e processe async. - Mensagem chega mas não envia: token expirado (System User token padrão expira em 60 dias se não for permanent). Gere token permanente em Business Settings → Users → System Users.
- Erro 470 "outside 24h window": tentou mandar texto livre pra contato que não respondeu nas últimas 24h. Use template HSM.
Quando vale a pena fazer do zero (e quando não)
Construir do zero faz sentido se você é dev construindo SaaS com WhatsApp embutido no produto (ex: app de delivery, fintech mandando notificação). Precisa de controle total do código, integração com microsserviços internos, deploy próprio.
Não faz sentido se você só quer atender cliente no WhatsApp da empresa — gastar 2-4 semanas implementando painel multi-atendente, bot IA, broadcast HSM e templates do zero é trabalho perdido. Plataforma como a MercaBot resolve em 3 minutos por R$ 197/mês.
Atendimento WhatsApp sem código
Se o objetivo é atender cliente (não construir produto), pule a parte de webhook. MercaBot conecta em 3 minutos com painel pronto + bot IA + broadcast.
Testar grátis →