Markdown
Referência · eventos

Webhook

Receba confirmações de entrega e mensagens dos contatos no seu sistema.

Cadastre a URL do seu endpoint no painel, em Integrações, e escolha os eventos. Fazemos um POST assinado para cada evento selecionado.

Eventos disponíveis (link para esta seção)

EventoDispara quandoServe para
contact.marketing_opt_outO contato pede para parar promoções.Registrar a saída sem depender do rótulo do botão.
template.statusA Meta comunica aprovação, rejeição, pausa ou outro estado.Atualizar o catálogo e suspender modelos indisponíveis.
template.qualityA Meta informa mudança de qualidade do modelo.Acompanhar a nota, incluindo a primeira leitura.
message.statusUma mensagem enviada pela plataforma muda de status.Confirmar entrega, leitura ou falha.
message.receivedUma mensagem do contato é recebida.Processar a resposta e atualizar a janela de atendimento.
Escolha os eventos usados pela sua integração. Para receber respostas dos contatos, habilite também message.received; ele contém dados da conversa.

Os novos eventos são opcionais. Habilite-os em Integrações quando seu receptor estiver preparado; não alteramos as assinaturas já existentes. Uma entrega HTTP 2xx confirma que o receptor aceitou o evento, não que concluiu a operação. Use o catálogo com status=ALL para reconciliar o estado atual dos modelos.

Testar pelo painel (link para esta seção)

Em Integrações → Webhook → Testar um evento, escolha o evento e clique em Enviar teste. Os cinco tipos estão disponíveis conforme as assinaturas salvas. O webhook precisa estar ativo, sem alterações por salvar.

Todo teste contém data.test: true, inclusive contact.marketing_opt_out. Valide primeiro a assinatura; depois, se o marcador for o booleano true, responda 2xx sem gravar o evento nem alterar cadastros. Faça isso antes das validações de telefone e das operações de negócio.

Os dados são fictícios: telefone composto por zeros, identificadores de exemplo e nenhum cadastro real consultado. Cada clique gera um delivery_id diferente, com prefixo test_; trate o identificador como texto. A assinatura e o cabeçalho adicional seguem a configuração salva.

JSON · teste de saída de promoções
{
  "event": "contact.marketing_opt_out",
  "occurred_at": "2026-11-06T16:00:00Z",
  "delivery_id": "test_0123456789abcdef0123456789abcdef",
  "data": {
    "message_id": "wamid.TESTE",
    "to": "000000000000",
    "phone_number_id": "0",
    "occurred_at": "2026-11-06T16:00:00Z",
    "source": "whatsapp",
21 linhas

O teste faz uma única tentativa, mostra o HTTP retornado e registra a ação na auditoria. Não entra na fila de entregas, não tem repetição automática e não altera contatos, consentimentos ou templates. Um 2xx confirma a recepção HTTP; não comprova o processamento de eventos reais, que não recebem esse marcador.

Mudanças de templates (link para esta seção)

JSON · template.status
{
  "event": "template.status",
  "occurred_at": "2026-11-06T16:00:00Z",
  "delivery_id": "2001",
  "data": {
    "waba_id": "123456789012345",
    "template_id": "234567890123456",
    "name": "pedido_recebido",
    "language": "pt_BR",
    "status": "APPROVED",
    "reason": "NONE"
  }
}

O evento template.quality mantém a identificação do modelo e traz previous_quality e quality, no lugar de status/reason. Campos indisponíveis podem ser nulos; aceite códigos futuros sem quebrar o receptor. A WABA identifica a conta, pois um template pode servir a vários números. O horário do envelope é o registro do evento na plataforma; não prova o instante exato da decisão da Meta.

Saída de promoções (link para esta seção)

message.received preserva texto e acrescenta interaction para botões/listas e consent quando uma saída é reconhecida. Use consent.action = marketing_opt_out, em vez de comparar o texto “Parar promoções”. O evento opcional contact.marketing_opt_out comunica essa mesma ação com identificador da mensagem e contato. Não soma uma segunda recusa; deduplique a sua operação pelo contato/mensagem.

A recusa de marketing vale para os números da empresa. Não representa cancelamento dos avisos legítimos de pedidos. A estrutura e as regras de aceite estão em Consentimento.

message.status (link para esta seção)

corpo do POST
{
  "event": "message.status",
  "occurred_at": "2026-07-30T18:00:00Z",
  "delivery_id": "1841",
  "data": {
    "message_id": "wamid.HBgNNTUyMT...",
    "status": "delivered",
    "status_rank": 3,
    "to": "5521999999999",
    "error": null,
15 linhas

message.received (link para esta seção)

O evento informa o conteúdo recebido, o contato e o número da empresa. Use esses dados para atualizar sua aplicação ou preparar uma resposta.

corpo do POST
{
  "event": "message.received",
  "occurred_at": "2026-08-02T11:14:22Z",
  "delivery_id": "1842",
  "data": {
    "message_id": "wamid.HBgNNTUyMT...",
    "from": "5521999999999",
    "phone_number_id": "1234567890",
    "type": "text",
    "text": "Recebi, obrigado!",
19 linhas
CampoO que é
fromTelefone do contato que enviou a mensagem. Em message.status, o telefone do destinatário aparece em to.
typetext, image, audio, video, document, sticker, location, contacts, button, interactive, unsupported.
interactionObjeto com type, id e title para botão ou lista recebida; nulo nos demais casos. id preserva o payload do botão ou o identificador da opção.
consentQuando reconhecemos uma saída de promoções: action, marketing_allowed, scope e changed. Nulo quando não houve essa ação. Não deduza consentimento pelo texto da mensagem.
textO texto, a legenda de uma mídia ou o rótulo de um botão/opção recebida. Nulo quando não há texto. O identificador da interação fica no campo interaction.
media_idPresente quando veio arquivo. Consulte a conversa no painel para acessar a mídia. O identificador não é uma URL pública nem garante disponibilidade permanente.
reply_to_message_idID da mensagem citada pelo contato. Use-o para localizar o envio e a operação correspondente.
window_expires_atAté quando a mensagem recebida permite resposta com texto livre. A janela não garante gratuidade.
window_expires_at representa a janela associada à mensagem recebida. Eventos podem chegar fora de ordem: mantenha a maior validade por contato e número da empresa. Perto de expirar, confirme com GET /window. A cobrança segue as regras vigentes da Meta, separadamente dessa autorização de envio.
Na coexistência, o eco de uma saída pelo celular pode vir sem a mensagem citada. Este evento descreve as mensagens recebidas dos contatos.

Headers (link para esta seção)

HeaderO que é
X-PT-TimestampTimestamp Unix em segundos. Faz parte da assinatura e muda a cada tentativa.
X-PT-SignaturePrefixo sha256= mais o HMAC em hexadecimal minúsculo.
X-PT-Delivery-IdId desta entrega. Repete nas retentativas do mesmo evento.
X-PT-AttemptNúmero da tentativa, começando em 1.
X-PT-EventUm dos cinco eventos disponíveis acima. Confira também event no corpo assinado.

Conferir a assinatura (link para esta seção)

A assinatura é o HMAC-SHA256 de "timestamp" + "." + corpo cru, em hexadecimal minúsculo, com o prefixo sha256=. O segredo aparece no painel, em Integrações.

Calcule a assinatura sobre os bytes originais do corpo. Converter o JSON e serializá-lo novamente pode alterar o conteúdo usado no HMAC.
Node.js (Express)
const crypto = require("crypto");

// Guarde o corpo CRU. Sem isto o express.json() já consumiu o stream.
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));

function assinaturaValida(req, segredo) {
  const ts = req.get("X-PT-Timestamp");
  const assinada = req.get("X-PT-Signature") || "";
  if (!segredo || !Buffer.isBuffer(req.rawBody)) return false;
  if (!/^\d{1,12}$/.test(ts || "") || !/^sha256=[a-f0-9]{64}$/.test(assinada)) return false;
41 linhas
PHP
$corpoCru = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_PT_TIMESTAMP'] ?? '';
$assinada = $_SERVER['HTTP_X_PT_SIGNATURE'] ?? '';

$formatoValido = preg_match('/^[0-9]{1,12}$/D', $ts)
    && preg_match('/^sha256=[a-f0-9]{64}$/D', $assinada);
if (!$segredo || !$formatoValido || abs(time() - (int) $ts) > 300) {
    http_response_code(401);
    exit;
}
31 linhas
Cada tentativa recebe uma nova assinatura e um timestamp atual. A tolerância de cinco minutos do exemplo vale para a tentativa HTTP, não para a idade do evento.

Campos de message.status (link para esta seção)

CampoO que é
message_idO mesmo id devolvido no envio.
statussent, delivered, read ou failed.
status_ranksent=2, delivered=3, read=4. Ignore regressões entre esses estados. failed=1 é um desfecho separado: pode substituir sent, mas não delivered ou read; não aplique uma comparação numérica simples para falhas.
toNúmero do destinatário. Mesmo nome do campo no envio.
errorObjeto {code, message, reason} só quando o status é failed; nulo nos outros. reason é o nosso código estável, code é o número da Meta.
client_refReferência informada no envio. Pode ser nula; combine-a com message_id para localizar a operação.
campaign_idPreenchido quando a mensagem saiu de uma campanha do painel.
delivery_idId desta entrega. Repete nas retentativas, então serve de chave de idempotência do seu lado.
occurred_atNo envelope: quando o evento entrou na fila. Permanece fixo nas tentativas. Em message.received, data.occurred_at informa o horário da mensagem recebido da Meta.

Confirmar o recebimento do evento (link para esta seção)

Para eventos reais, qualquer coisa fora de 2xx entra na fila de retentativa. Se o seu processamento é demorado, confirme primeiro a gravação numa fila durável, responda e processe depois. Há limite de 10 segundos por tentativa e redirecionamentos HTTP não são seguidos.

Uma resposta 2xx confirma que seu endpoint aceitou o evento. A plataforma não interpreta o corpo para decidir se deve repetir. Para eventos reais, não responda sucesso antes de persistir: se o processo cair, o evento ainda precisa estar disponível para seu worker.

O mesmo delivery_id pode chegar mais de uma vez. Use uma chave única por integração e entrega para não repetir efeitos já aplicados.

Se o seu endpoint cair (link para esta seção)

Eventos habilitados são persistidos numa fila. Após a tentativa inicial, repetimos em 30s, 2min, 10min, 1h, 6h, 24h, com variação de até 20% para distribuir a carga: sete tentativas automáticas e cerca de 31 horas de espera acumulada. O que esgota as tentativas fica guardado esperando reenvio, que você dispara pelo painel ou pela API de entregas.