Guia
Webhook do WhatsApp Cloud API: como a Meta entrega cada evento e como receber sem perder nada
O webhook do WhatsApp Cloud API é a URL HTTPS que a Meta chama a cada evento do seu número. Primeiro ela valida o endereço com um GET que precisa devolver o hub.challenge. Depois manda cada mensagem recebida e cada status por POST, assinado no header X-Hub-Signature-256. Responda 200 rápido e deduplique pelo id do evento.
Por Gustavo Calixto, fundador do Omnique. Atualizado em .

O que é o webhook do WhatsApp Cloud API?
É o endereço do seu servidor que a Meta chama sempre que algo acontece no número conectado à Cloud API: uma mensagem chega, um envio muda de status, um template é aprovado. Sem webhook, a API só envia. Receber depende de um endpoint HTTPS público, com certificado válido, que responda rápido e aguente reentregas.
O webhook fica no app da Meta e recebe os eventos das contas do WhatsApp Business ligadas a ele. Você escolhe quais campos assinar no Painel de Apps, em WhatsApp, Configuração. O campo que importa no dia a dia é o messages, que leva tanto as mensagens recebidas quanto os status das mensagens enviadas.
Criar o app e cadastrar a URL é assunto do guia de como configurar a WhatsApp Cloud API, e o caminho inteiro até a primeira mensagem está no guia de API oficial do WhatsApp. Aqui o foco é o que chega nessa URL, como validar e como tratar sem perder nem duplicar evento.
Como funciona a verificação do webhook com hub.challenge?
Quando você cadastra a URL, a Meta faz um GET com três parâmetros: hub.mode igual a subscribe, hub.verify_token com o texto que você definiu e hub.challenge, um valor aleatório. Se o token bate, o endpoint responde 200 com o hub.challenge puro no corpo. Qualquer outra resposta faz a Meta recusar a URL.
// GET no mesmo endereço do webhook
const p = new URL(req.url).searchParams;
const ok =
p.get("hub.mode") === "subscribe" &&
p.get("hub.verify_token") === VERIFY_TOKEN;
if (!ok) {
return new Response(null, { status: 403 });
}
return new Response(p.get("hub.challenge"));Dois tropeços aparecem sempre. O primeiro é devolver o challenge dentro de um JSON, quando ele tem de ir puro. O segundo é usar certificado autoassinado, que a Meta não aceita. O token de verificação não protege os eventos: ele só prova que você controla a URL. Quem protege cada evento é a assinatura.
Como validar a assinatura X-Hub-Signature-256?
Todo POST da Meta traz o header X-Hub-Signature-256 no formato sha256= seguido de um hash. Calcule o HMAC SHA256 do corpo da requisição usando o App Secret do seu app como chave e compare com o header, em tempo constante. Se for diferente, responda erro e não processe: é assim que você barra evento forjado.
import crypto from "node:crypto";
function assinaturaValida(corpoCru, header) {
const esperado = "sha256=" + crypto
.createHmac("sha256", APP_SECRET)
.update(corpoCru)
.digest("hex");
const a = Buffer.from(header ?? "");
const b = Buffer.from(esperado);
return a.length === b.length &&
crypto.timingSafeEqual(a, b);
}O detalhe que mais quebra essa validação é o corpo. O HMAC vale para os bytes exatos que a Meta enviou. Se o seu framework já transformou o JSON em objeto e você serializa de novo, espaços e caracteres especiais mudam e a assinatura nunca bate. Leia o corpo cru antes de qualquer parser e guarde o App Secret só no servidor.
Quais eventos chegam: messages, statuses e outros campos
O campo messages concentra o dia a dia. Dentro dele, value.messages traz o que o cliente mandou e value.statuses traz o que aconteceu com o que você enviou: sent, delivered, read, failed e, em mensagem de voz, played. O número de origem vem em metadata.phone_number_id.
Uma mensagem de texto recebida chega neste envelope:
{
"object": "whatsapp_business_account",
"entry": [{
"id": "<WABA_ID>",
"changes": [{
"field": "messages",
"value": {
"messaging_product": "whatsapp",
"metadata": {
"display_phone_number": "5511...",
"phone_number_id": "<ID_DO_NUMERO>"
},
"contacts": [{
"profile": { "name": "Maria" },
"wa_id": "5511..."
}],
"messages": [{
"from": "5511...",
"id": "wamid.HBgL...",
"timestamp": "1791210000",
"type": "text",
"text": { "body": "Oi, tudo bem?" }
}]
}
}]
}]
}Num status, o array statuses aparece no lugar de messages, com o id da mensagem enviada (o mesmo wamid que a API devolveu no envio), o status e o horário. Como entry, changes e os arrays de dentro são listas, percorra todos os itens em vez de ler só a primeira posição.
Além de messages, estes campos aparecem com frequência em quem opera a API:
| Campo | O que traz | Quando importa |
|---|---|---|
messages | Mensagens recebidas e status dos envios | Sempre |
message_template_status_update | Template aprovado, recusado ou pausado | Quem envia template |
account_update | Avisos sobre a conta, inclusive violação de política | Para reagir rápido a uma restrição |
phone_number_quality_update | Mudança no limite de envio do número | Campanhas e avisos em volume |
smb_message_echoes | Mensagens enviadas pelo app do celular | Número em coexistência |
history e smb_app_state_sync | Histórico e contatos do app WhatsApp Business | Número em coexistência, logo depois de conectar |
Os três últimos só existem quando o número continua no app do celular e na API ao mesmo tempo, assunto do guia de coexistência.
A documentação de status traz um aviso que pega muita gente: read só existe depois de delivered, mas quando a mensagem é entregue e lida ao mesmo tempo a Meta manda só o read. Somando isso às reentregas, a ordem de chegada não serve de relógio. Ordene pelo timestamp de dentro do evento, nunca pela hora em que ele chegou ao seu servidor.
Quanto tempo a Meta tenta reentregar um webhook?
Até 7 dias. Se o seu endpoint responde qualquer coisa diferente de 200, ou não responde, a Meta tenta de novo com frequência decrescente até conseguir ou até o prazo acabar. A própria documentação avisa que essas reentregas podem gerar notificações duplicadas. Cada payload pode ter até 3 MB.
Na prática, isso tem três consequências. Um endpoint fora do ar por uma hora recebe uma enxurrada de eventos atrasados quando volta. Um endpoint tão lento que a conexão cai recebe o mesmo evento mais de uma vez. E um endpoint que devolve 500 por causa de um bug num tipo de mensagem vai receber aquele evento de novo por dias.
Boas práticas para não perder nem duplicar evento
Valide a assinatura, grave o evento e responda 200 logo em seguida. O trabalho pesado (chamar uma IA, consultar o CRM, responder o cliente) roda depois, a partir de uma fila. E toda gravação começa por uma checagem de duplicidade com chave única no banco, porque a Meta pode entregar o mesmo evento mais de uma vez.
- Responda rápido. O endpoint que recebe da Meta só valida, grava e confirma. Quem demora é a fila, não o webhook.
- Deduplique pelo id do evento. Para mensagem, o
wamidé único. Para status, combine owamidcom o status, porque a mesma mensagem gerasent,deliverederead. - Use o banco como trava, não a memória. Um conjunto em memória some a cada deploy e não é compartilhado entre as cópias do seu servidor. Chave única no banco resolve.
- Trate tipo desconhecido sem quebrar. Chegou um tipo de mensagem que o seu código não conhece? Registre e responda 200. Um erro 500 aí vira reentrega por dias.
- Retente o seu lado a partir da fila. Se o processamento falhar depois da confirmação, reprocesse a partir do que você gravou, sem depender de a Meta reenviar.
create table eventos_whatsapp (
chave text primary key,
recebido_em timestamptz default now()
);
-- chave: o wamid, ou wamid:status
insert into eventos_whatsapp (chave)
values ($1)
on conflict (chave) do nothing
returning chave;
-- sem linha de volta: evento repetidoSe o insert não devolve linha, o evento já foi processado: responda 200 e siga. É o mesmo princípio que o Omnique usa na entrada, antes de enfileirar cada evento.
Por que o webhook do WhatsApp não chega?
Quando o envio funciona e o recebimento não, a causa quase sempre está na configuração, não no código: verificação que não devolveu o hub.challenge, campo messages sem assinatura no painel do app, certificado inválido ou endpoint respondendo erro. A tabela resume como conferir cada uma em poucos minutos.
| Sintoma | Causa provável | Como conferir |
|---|---|---|
| A Meta não aceita a URL | Token diferente ou challenge devolvido dentro de JSON | Abra a URL com os três parâmetros hub no navegador e veja se volta só o challenge |
| URL aceita, nenhuma mensagem chega | Campo messages não assinado | Painel de Apps, WhatsApp, Configuração, lista de campos do webhook |
| Nada chega e o seu log está vazio | Certificado autoassinado ou vencido | Teste o endereço num verificador de TLS |
| Eventos repetidos | Resposta diferente de 200 ou lenta demais | Veja o status que o seu endpoint devolveu nas últimas chamadas |
| Chega read sem delivered | Comportamento normal | Mensagem entregue e lida ao mesmo tempo gera só read |
No n8n, o WhatsApp Trigger tem particularidades próprias, tratadas no guia de API oficial do WhatsApp no n8n.
Como o Omnique repassa o webhook para o seu endpoint?
O Omnique recebe o webhook da Meta por você, valida a assinatura, descarta duplicatas e põe cada evento numa fila durável. Depois faz um POST no endereço que você informar, com um JSON mais simples, assinatura HMAC própria e um delivery_id que não muda entre tentativas. Você não cria app na Meta nem configura token de verificação.
O número entra pelo Cadastro Incorporado da Meta, dentro do painel do Omnique, e o endereço que a Meta chama é configurado automaticamente. No painel você só informa a URL de destino: um Webhook do n8n, o seu backend, uma função serverless. O corpo que chega é este:
{
"event": "message",
"origin": "inbound",
"timestamp": "2026-10-06T12:34:56.000Z",
"org_id": "5f3b...",
"delivery_id": "9c1e...",
"data": {
"message": { "...": "objeto da Meta" },
"channel": {
"id": "a1b2...",
"phone_number_id": "1234567890",
"waba_id": "9876543210",
"display_name": "Meu Atendimento",
"type": "whatsapp"
}
}
}O campo event diz o tipo (message, status, template, echo e outros) e o objeto dentro de data é o que a Meta mandou, sem alteração. O campo origin diz quem originou: inbound para o cliente final, outbound_api para envio feito pela API, outbound_device para mensagem digitada no celular em coexistência e platform para avisos da própria Meta.
| Situação | O que o Omnique faz |
|---|---|
| Seu endpoint responde 2xx em até 15 segundos | Entrega concluída e registrada no log da instância |
| Erro, tempo esgotado ou resposta fora de 2xx | Nova tentativa automática, até 5 por padrão, com espera crescente de 1 segundo a 5 minutos |
| Resposta 410 | Para de tentar na hora, porque o endereço foi removido |
| Tentativas esgotadas | A entrega fica no log com o evento guardado cifrado por 30 dias e o botão Reenviar |
| 10 falhas definitivas seguidas | O webhook é desligado e você recebe aviso no painel e por e-mail |
| Banco do Omnique indisponível na hora de receber da Meta | O Omnique responde erro à Meta, que reentrega depois |
Cada entrega traz os headers X-Omnique-Event, X-Omnique-Delivery e X-Omnique-Attempt, além da assinatura X-Omnique-Signature-256, calculada com HMAC SHA256 sobre o corpo cru e o segredo whsec_ mostrado quando você cria o webhook. A validação é a mesma do código acima, só troca o segredo. O delivery_id se repete em toda tentativa e no reenvio manual: use como chave de idempotência e o seu sistema não processa o mesmo evento duas vezes.
A referência completa, com cada tipo de evento e exemplos de corpo, está na documentação do Omnique. Para ver as entregas chegando no log, crie a conta e conecte um número no teste grátis. O valor por número está em preços.

