Wafly › Tutoriais › Webhook de WhatsApp
Webhook de WhatsApp: eventos, payload completo e como receber na sua aplicação
Enviar mensagem por API é a metade fácil. O que transforma um disparador em bot de verdade é o caminho contrário: a mensagem do cliente chegar na sua aplicação, em tempo real. Isso é o webhook: a API chama uma URL sua a cada evento da instância.
Os 4 webhooks da instância
| Evento | Endpoint de configuração | Quando dispara |
|---|---|---|
| Mensagem recebida | update-webhook-received | A instância recebe qualquer mensagem |
| Entrega confirmada | update-webhook-delivery | Mensagem enviada é confirmada como entregue |
| Instância conectada | update-webhook-connected | A sessão conecta ao WhatsApp |
| Instância desconectada | update-webhook-disconnected | A sessão cai. Funciona como alerta de monitoramento de graça |
Passo 1: configurar a URL
Cada webhook é um PUT com a URL de destino no corpo (string vazia "" remove):
curl -X PUT "https://wafly.com.br/api-bridge-whats/instances/SUA_INSTANCIA/token/SEU_TOKEN/update-webhook-received" \
-H "Client-Token: SEU_CLIENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "value": "https://meuservidor.com/webhook/whatsapp" }'
O payload de mensagem recebida, campo a campo
Toda mensagem recebida chega como POST com Content-Type: application/json. Este é o corpo real de uma mensagem de texto em conversa privada:
{
"type": "ReceivedCallback",
"instanceId": "minha-instancia",
"connectedPhone": "5511999999999",
"phone": "5511988887777",
"senderName": "Maria Souza",
"senderPhoto": "https://pps.whatsapp.net/...",
"senderLid": "5511988887777@s.whatsapp.net",
"chatName": "Maria Souza",
"messageId": "3EB0C767D26B8F1A2C31",
"momment": 1785312000000,
"status": "RECEIVED",
"fromMe": false,
"isGroup": false,
"isNewsletter": false,
"isEdit": false,
"isStatusReply": false,
"waitingMessage": false,
"broadcast": false,
"text": { "message": "Oi, ainda tem em estoque?" }
}
| Campo | Tipo | O que contém |
|---|---|---|
type | string | Tipo do evento. Em mensagem recebida é sempre ReceivedCallback |
instanceId | string | Identificador da instância que recebeu — útil quando você aponta várias para a mesma URL |
connectedPhone | string | O seu número, o que está conectado na instância |
phone | string | A conversa de origem: o número do contato, ou o ID do grupo quando isGroup é true |
participantPhone | string | Só em grupo: quem, dentro do grupo, mandou a mensagem |
senderName | string | Nome de exibição (push name) do remetente |
senderPhoto | string | URL da foto de perfil, quando disponível |
senderLid | string | JID interno do remetente |
chatName | string | Nome da conversa: do contato, ou do grupo |
messageId | string | ID único da mensagem. Use como chave de idempotência |
momment | number | Timestamp em epoch de milissegundos (atenção: o campo tem dois "m" mesmo) |
status | string | RECEIVED para mensagem de terceiro, SENT quando fromMe é true |
fromMe | boolean | True quando a mensagem foi enviada pelo próprio número conectado. Filtre isso ou seu bot responde a si mesmo |
isGroup | boolean | True quando a origem é um grupo |
isNewsletter | boolean | True quando a origem é um canal |
isEdit | boolean | True quando é a edição de uma mensagem já enviada |
isStatusReply | boolean | True quando é resposta a um status |
broadcast | boolean | True em mensagem de lista de transmissão |
text.message | string | O conteúdo, em mensagem de texto |
Como distinguir texto, mídia e grupo
O envelope acima é o mesmo em todos os casos; o que muda é a chave de conteúdo. Em texto vem text.message. Em mídia (imagem, áudio, vídeo, documento, sticker), localização, contato, enquete e reação vem uma chave própria de cada tipo, no lugar de text. O caminho mais rápido de descobrir a sua é logar o payload inteiro na primeira mensagem de teste.
Para grupo, a diferença prática é que phone passa a ser o ID do grupo e aparece participantPhone com quem falou. Se você responder usando phone, a resposta cai no grupo; se usar participantPhone, cai no privado da pessoa — e aí vale lembrar que mandar privado para quem nunca te escreveu é o caminho mais curto para o erro 463 e para o banimento do número.
Os outros tipos de callback
O campo type distingue os eventos que chegam na sua URL. Faça o roteamento por ele:
type | Quando chega | Campos próprios |
|---|---|---|
ReceivedCallback | Mensagem recebida (ou enviada, com fromMe: true) | ver tabela acima |
MessageStatusCallback | Mudança de status de mensagens que você enviou | status, ids (array), phone, participant em grupo |
DeliveryCallback | Confirmação de entrega | phone, momment |
ConnectedCallback | A instância conectou | connected: true, instanceId |
DisconnectedCallback | A sessão caiu | disconnected: true, error, reason |
O DisconnectedCallback é o mais subestimado dos cinco. Ele traz reason, que diz por que a sessão caiu — e é a diferença entre descobrir a queda pelo seu monitoramento ou pelo cliente reclamando que ninguém respondeu. Trate como alerta, não como log.
Vale a ressalva honesta: numa API não oficial o webhook depende da sessão estar de pé. Se a sessão cai, os eventos param até reconectar — não há fila persistida nem garantia de reentrega como na Cloud API oficial da Meta. Por isso o alerta de desconexão importa tanto aqui.
Passo 2: receber (exemplo em Node.js)
import express from "express";
const app = express();
app.use(express.json());
const processados = new Set(); // troque por Redis/banco em produção
app.post("/webhook/whatsapp", (req, res) => {
res.sendStatus(200); // responda JÁ; processe depois
const evt = req.body;
// 1. roteie pelo tipo do evento
if (evt.type === "DisconnectedCallback") {
return alertar(`Sessão caiu: ${evt.reason}`); // seu alerta aqui
}
if (evt.type !== "ReceivedCallback") return;
// 2. ignore o que você mesmo enviou, ou o bot responde a si mesmo
if (evt.fromMe) return;
// 3. idempotência: o mesmo evento pode chegar duas vezes
if (processados.has(evt.messageId)) return;
processados.add(evt.messageId);
// 4. só texto, neste exemplo
const texto = evt.text?.message;
if (!texto) return;
const de = evt.isGroup ? evt.participantPhone : evt.phone;
console.log(`[${new Date(evt.momment).toISOString()}] ${de}: ${texto}`);
// responder: use evt.phone (cai no grupo se for grupo)
});
app.listen(3000);
Na primeira mensagem de teste, logue o payload inteiro e inspecione, que é o jeito mais rápido de ver todos os campos disponíveis para o seu caso (texto, mídia, remetente, grupo). Em desenvolvimento local, exponha a porta com um túnel (ngrok, cloudflared) para o webhook alcançar sua máquina.
Boas práticas que evitam dor
- Responda 200 imediatamente e processe o evento de forma assíncrona (fila, job). Receiver lento = timeout = retries = eventos duplicados.
- Idempotência: trate o ID da mensagem como chave. Se o mesmo evento chegar duas vezes, processe uma.
- Segredo na URL: use um caminho imprevisível (ex.:
/webhook/whatsapp/x7k2m9) e valide-o, porque qualquer um que descubra a URL pode mandar POST nela. - Filtre o que não é seu caso: mensagens de grupo (
isGroup) e eventos que você não usa devem sair cedo do handler. - Monitore o webhook de desconexão: é seu alarme de sessão caída antes do cliente reclamar.
Sem servidor? Use n8n
O Webhook node do n8n gera a URL pública por você. Cadastre-a no update-webhook-received e cada mensagem vira uma execução do workflow. O caminho completo está no tutorial WhatsApp no n8n.
Teste com uma instância real
3 dias grátis, sem cartão. Configure o webhook e veja o payload chegando em minutos.
Criar instância grátisPerguntas frequentes
Qual é o payload do webhook de mensagem recebida?
JSON com type: "ReceivedCallback" e os campos instanceId, connectedPhone, phone, participantPhone (só em grupo), senderName, chatName, messageId, momment (epoch ms), status, mais os booleanos fromMe, isGroup, isNewsletter, isEdit, isStatusReply e broadcast. Em texto, o conteúdo vem em text.message. Tabela completa acima.
Como evitar que o bot responda às próprias mensagens?
Descarte todo evento com fromMe: true. O que você envia também gera ReceivedCallback, com status: "SENT". Sem esse filtro o bot entra em laço.
O que é um webhook de WhatsApp?
Uma URL sua que a API chama a cada evento da instância (mensagem recebida, entrega, conexão, desconexão). É o que torna o bot bidirecional.
Funciona com n8n?
Sim. O Webhook node gera a URL; cadastre-a como webhook de recebidas e cada mensagem dispara o workflow.
Preciso responder rápido?
Sim: 200 imediato, processamento assíncrono depois. Lentidão gera retries e eventos duplicados.
Como removo um webhook?
Faça o mesmo PUT com "value": "" (string vazia).
Continue
Endpoints verificados em julho/2026. Encontrou algo impreciso? Fale com a gente que corrigimos.