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

Aprovação, rejeição e qualidade de templates são avisos operacionais do painel. Não chegam nesses dois eventos de mensagem. Para acompanhar esses estados na integração, consulte o catálogo de templates com status=ALL.

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.
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 payload do botão e o ID da opção não são expostos neste evento.
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-Eventmessage.status ou message.received. 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;
38 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;
}
27 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)

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