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 .

Mulher de casaco vermelho digita código num notebook perto de uma janela
Foto: Christina Morillo no Pexels

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:

CampoO que trazQuando importa
messagesMensagens recebidas e status dos enviosSempre
message_template_status_updateTemplate aprovado, recusado ou pausadoQuem envia template
account_updateAvisos sobre a conta, inclusive violação de políticaPara reagir rápido a uma restrição
phone_number_quality_updateMudança no limite de envio do númeroCampanhas e avisos em volume
smb_message_echoesMensagens enviadas pelo app do celularNúmero em coexistência
history e smb_app_state_syncHistórico e contatos do app WhatsApp BusinessNú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.

  1. Responda rápido. O endpoint que recebe da Meta só valida, grava e confirma. Quem demora é a fila, não o webhook.
  2. Deduplique pelo id do evento. Para mensagem, o wamid é único. Para status, combine o wamid com o status, porque a mesma mensagem gera sent, delivered e read.
  3. 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.
  4. 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.
  5. 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 repetido

Se 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.

SintomaCausa provávelComo conferir
A Meta não aceita a URLToken diferente ou challenge devolvido dentro de JSONAbra a URL com os três parâmetros hub no navegador e veja se volta só o challenge
URL aceita, nenhuma mensagem chegaCampo messages não assinadoPainel de Apps, WhatsApp, Configuração, lista de campos do webhook
Nada chega e o seu log está vazioCertificado autoassinado ou vencidoTeste o endereço num verificador de TLS
Eventos repetidosResposta diferente de 200 ou lenta demaisVeja o status que o seu endpoint devolveu nas últimas chamadas
Chega read sem deliveredComportamento normalMensagem 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çãoO que o Omnique faz
Seu endpoint responde 2xx em até 15 segundosEntrega concluída e registrada no log da instância
Erro, tempo esgotado ou resposta fora de 2xxNova tentativa automática, até 5 por padrão, com espera crescente de 1 segundo a 5 minutos
Resposta 410Para de tentar na hora, porque o endereço foi removido
Tentativas esgotadasA entrega fica no log com o evento guardado cifrado por 30 dias e o botão Reenviar
10 falhas definitivas seguidasO webhook é desligado e você recebe aviso no painel e por e-mail
Banco do Omnique indisponível na hora de receber da MetaO 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.

Perguntas frequentes

Por que chegam mensagens repetidas no meu webhook do WhatsApp?

Porque a Meta reentrega todo evento que não recebeu resposta 200, por até 7 dias, e a própria documentação avisa que isso pode gerar duplicatas. Grave o id do evento numa tabela com chave única e ignore o que já existe.

Quanto tempo a Meta tenta reenviar um webhook que falhou?

Até 7 dias, com frequência decrescente, segundo a documentação de webhooks do WhatsApp. Por isso um endpoint que volta depois de horas fora do ar recebe os eventos atrasados de uma vez.

Preciso validar o header X-Hub-Signature-256?

Precisa. É o único jeito de saber que o POST veio da Meta. Calcule o HMAC SHA256 do corpo cru com o App Secret do app e compare com o header em tempo constante.

O status read pode chegar sem o delivered?

Pode. Quando a mensagem é entregue e lida ao mesmo tempo, a Meta envia só o read, porque a entrega fica implícita na leitura.

Com o Omnique, preciso cadastrar o webhook no painel da Meta?

Não. O endereço que a Meta chama e o token de verificação são configurados pelo Omnique quando você conecta o número. Você só informa a URL de destino dos eventos.

Dá para mandar os eventos de um número para dois sistemas?

No Omnique, cada número entrega para um webhook de destino. Para alimentar dois sistemas, use o primeiro como porta de entrada, um fluxo do n8n por exemplo, e repasse de lá para o segundo.

Fontes

  1. Meta for Developers: webhooks do WhatsApp, consultado em 06/10/2026.
  2. Meta for Developers: Create a webhook endpoint, consultado em 06/10/2026.
  3. Meta for Developers: messages webhook reference, consultado em 06/10/2026.
  4. Meta for Developers: status messages webhook reference, consultado em 06/10/2026.
  5. Meta for Developers: como integrar usuários do app WhatsApp Business, consultado em 06/10/2026.
  6. Meta for Developers: política da Plataforma do WhatsApp Business e monitoramento de spam, consultado em 06/10/2026.

Leia também

Conecte seu número oficial em minutos

Teste grátis por 3 dias, sem cartão. A mensagem você paga direto à Meta.

Testar grátis por 3 dias