Escopo: JavaScript/Node.js. Guia baseado em Baileys v7 e Meta Cloud API v21.0 com Node.js 22. Conceitos transversais de bots (webhook, polling, command routing, rate limit): shared/platform/bots.md. Diferença entre API oficial e cliente não-oficial, Template Messages, verificação de webhook: shared/platform/bots-advanced.md.
O WhatsApp tem dois caminhos de automação com tradeoffs muito diferentes. Baileys simula o cliente WhatsApp Web (não-oficial, sem aprovação necessária). A Meta Cloud API (Interface de Programação Meta na Nuvem) é a via oficial, com aprovação e número homologado. O SDK Node.js oficial da Meta foi arquivado: use fetch nativo do Node.js 22.
Conceitos fundamentais
| Conceito | O que é |
|---|---|
| Baileys (cliente WhatsApp Web não-oficial) | Biblioteca que simula o WhatsApp Web via WebSocket; sem aprovação da Meta |
| Meta Cloud API (Interface de Programação Meta na Nuvem) | API oficial da Meta para WhatsApp Business; exige aprovação e número homologado |
| WhatsApp Business Account (Conta WhatsApp Business) | Entidade que agrupa números e templates aprovados pela Meta |
| Template Message (mensagem por template) | Mensagem com formato pré-aprovado pela Meta; obrigatória fora da janela de 24h |
| 24-hour window (janela de 24 horas) | Período após mensagem do usuário em que o bot pode responder com texto livre |
| webhook verification (verificação de webhook) | Handshake hub.challenge que a Meta exige para confirmar dono do endpoint |
| QR pairing (pareamento por QR) | Fluxo do Baileys que registra a sessão lendo um QR code com o app do celular |
| multi-file auth state (estado de autenticação em múltiplos arquivos) | Persistência de credenciais Baileys em arquivos separados (useMultiFileAuthState) |
Instalação
# Baileys v7 (cliente não-oficial, ESM-only)
npm install @whiskeysockets/baileys
# Meta Cloud API: sem pacote; usa fetch nativo do Node.js 22
Baileys v7
Import
Baileys v7 é ESM-only: require é quebrado. makeWASocket é default export; os utilitários são named imports.
❌ Ruim: CommonJS quebrado no v7; makeWASocket como named import
const { makeWASocket, useMultiFileAuthState } = require('@whiskeysockets/baileys');
✅ Bom: ESM; makeWASocket como default import; named imports separados
import makeWASocket, { useMultiFileAuthState, DisconnectReason } from '@whiskeysockets/baileys';
Setup do Client
❌ Ruim: sem reconnect; sem tratamento de logout; printQRInTerminal ausente
import makeWASocket, { useMultiFileAuthState } from '@whiskeysockets/baileys';
const { state, saveCreds } = await useMultiFileAuthState('./auth');
const socket = makeWASocket({ auth: state });
socket.ev.on('creds.update', saveCreds);
✅ Bom: reconnect automático; guard para logout; printQRInTerminal ativo
import makeWASocket, { useMultiFileAuthState, DisconnectReason } from '@whiskeysockets/baileys';
async function startBot() {
const { state, saveCreds } = await useMultiFileAuthState('./auth');
const socket = makeWASocket({
auth: state,
printQRInTerminal: true,
});
socket.ev.on('creds.update', saveCreds);
socket.ev.on('connection.update', (update) => {
const { connection, lastDisconnect } = update;
if (connection !== 'close') return;
const statusCode = lastDisconnect?.error?.output?.statusCode;
const shouldReconnect = statusCode !== DisconnectReason.loggedOut;
if (shouldReconnect) startBot();
});
socket.ev.on('messages.upsert', ({ messages }) => {
for (const message of messages) {
processIncomingMessage(socket, message);
}
});
}
startBot();
Processando Mensagens
❌ Ruim: sem guard para fromMe; routing inline com texto; lógica misturada
socket.ev.on('messages.upsert', async ({ messages }) => {
const msg = messages[0];
const text = msg.message?.conversation;
if (text === '/pedido') {
await socket.sendMessage(msg.key.remoteJid, { text: 'Qual o ID?' });
}
});
✅ Bom: guard fromMe; extração e routing separados; Strategy Map
function processIncomingMessage(socket, message) {
if (message.key.fromMe) return;
const text = extractMessageText(message);
const chatId = message.key.remoteJid;
if (!text) return;
routeCommand(socket, chatId, text);
}
function extractMessageText(message) {
const text =
message.message?.conversation ||
message.message?.extendedTextMessage?.text ||
null;
return text;
}
const COMMAND_MAP = {
'/order': orderCommand,
'/status': statusCommand,
'/help': helpCommand,
};
async function routeCommand(socket, chatId, text) {
const commandKey = text.split(' ')[0].toLowerCase();
const command = COMMAND_MAP[commandKey];
if (!command) return;
await command(socket, chatId, text);
}
Enviando Mensagens
async function orderCommand(socket, chatId, messageText) {
const orderId = messageText.split(' ')[1];
if (!orderId) {
const errorPayload = { text: 'Informe o ID. Exemplo: /order 12345' };
await socket.sendMessage(chatId, errorPayload);
return;
}
const order = await fetchOrder(orderId);
const summary = buildOrderSummary(order);
const messagePayload = { text: summary };
await socket.sendMessage(chatId, messagePayload);
}
Meta Cloud API
Verificação do Webhook
❌ Ruim: sem verificação de hub.mode; responde 200 sem checar token
app.get('/webhook', (req, res) => {
res.status(200).send(req.query['hub.challenge']);
});
✅ Bom: verifica mode e token antes de responder com o challenge
app.get('/webhook', (request, response) => {
const mode = request.query['hub.mode'];
const token = request.query['hub.verify_token'];
const challenge = request.query['hub.challenge'];
const isVerificationRequest = mode === 'subscribe' && token === process.env.WEBHOOK_VERIFICATION_TOKEN;
if (!isVerificationRequest) {
response.sendStatus(403);
return;
}
response.status(200).send(challenge);
});
Recebendo Mensagens
Responda 200 OK imediatamente. A Meta cancela a entrega se o endpoint demorar para responder.
❌ Ruim: req/res abreviados; processamento síncrono; sem checar body.object
app.post('/webhook', async (req, res) => {
const message = extractMessage(req.body);
await processMessage(message);
res.sendStatus(200);
});
✅ Bom: request/response sem abreviação; 200 imediato; check de body.object; async após resposta
import express from 'express';
const app = express();
app.use(express.json());
app.post('/webhook', (request, response) => {
response.sendStatus(200);
const isWhatsAppEvent = request.body?.object === 'whatsapp_business_account';
if (!isWhatsAppEvent) return;
const message = extractMessage(request.body);
if (message) processMessage(message);
});
function extractMessage(body) {
const message = body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0] ?? null;
return message;
}
async function processMessage(message) {
const chatId = message.from;
const text = message.text?.body ?? '';
await routeCommand(chatId, text);
}
Enviando Mensagens
❌ Ruim: endpoint sem versão; env vars fora do padrão Meta; sem recipient_type; sem Content-Type
async function sendText(chatId, text) {
await fetch(`https://graph.facebook.com/${process.env.PHONE_ID}/messages`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.TOKEN}` },
body: JSON.stringify({ messaging_product: 'whatsapp', to: chatId, text: { body: text } }),
});
}
✅ Bom: endpoint v21.0; env vars padrão Meta; recipient_type explícito; headers completos
async function sendTextMessage(chatId, text) {
const endpoint = `https://graph.facebook.com/v21.0/${process.env.WA_PHONE_NUMBER_ID}/messages`;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.WA_ACCESS_TOKEN}`,
};
const payload = {
messaging_product: 'whatsapp',
recipient_type: 'individual',
to: chatId,
type: 'text',
text: { body: text },
};
const body = JSON.stringify(payload);
const response = await fetch(endpoint, { method: 'POST', headers, body });
return response;
}
Template Message
O primeiro contato com um usuário exige uma Template Message aprovada pela Meta.
async function sendOrderTemplate(chatId, orderId, orderStatus) {
const endpoint = `https://graph.facebook.com/v21.0/${process.env.WA_PHONE_NUMBER_ID}/messages`;
const headers = {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.WA_ACCESS_TOKEN}`,
};
const payload = {
messaging_product: 'whatsapp',
to: chatId,
type: 'template',
template: {
name: 'order_status_update',
language: { code: 'pt_BR' },
components: [
{
type: 'body',
parameters: [
{ type: 'text', text: orderId },
{ type: 'text', text: orderStatus },
],
},
],
},
};
const body = JSON.stringify(payload);
const response = await fetch(endpoint, { method: 'POST', headers, body });
return response;
}
Veja também
- shared/platform/bots-advanced.md (WhatsApp): API oficial vs não-oficial, Template Messages, verificação de webhook (conceitual)
- shared/platform/bots.md: webhook vs polling, session state, rate limit
Desenvolvido por @thiagocajadev · Fork baseado no repositório pmndrs/docs · Poimandres.