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)
| Evento | Dispara quando | Serve para |
|---|---|---|
message.status | Uma mensagem enviada pela plataforma muda de status. | Confirmar entrega, leitura ou falha. |
message.received | Uma mensagem do contato é recebida. | Processar a resposta e atualizar a janela de atendimento. |
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)
{
"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,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.
{
"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!",| Campo | O que é |
|---|---|
| from | Telefone do contato que enviou a mensagem. Em message.status, o telefone do destinatário aparece em to. |
| type | text, image, audio, video, document, sticker, location, contacts, button, interactive, unsupported. |
| text | O 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_id | Presente 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_id | ID da mensagem citada pelo contato. Use-o para localizar o envio e a operação correspondente. |
| window_expires_at | Até quando a mensagem recebida permite resposta com texto livre. A janela não garante gratuidade. |
Headers (link para esta seção)
| Header | O que é |
|---|---|
| X-PT-Timestamp | Timestamp Unix em segundos. Faz parte da assinatura e muda a cada tentativa. |
| X-PT-Signature | Prefixo sha256= mais o HMAC em hexadecimal minúsculo. |
| X-PT-Delivery-Id | Id desta entrega. Repete nas retentativas do mesmo evento. |
| X-PT-Attempt | Número da tentativa, começando em 1. |
| X-PT-Event | message.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.
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;$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;
}Campos de message.status (link para esta seção)
| Campo | O que é |
|---|---|
| message_id | O mesmo id devolvido no envio. |
| status | sent, delivered, read ou failed. |
| status_rank | sent=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. |
| to | Número do destinatário. Mesmo nome do campo no envio. |
| error | Objeto {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_ref | Referência informada no envio. Pode ser nula; combine-a com message_id para localizar a operação. |
| campaign_id | Preenchido quando a mensagem saiu de uma campanha do painel. |
| delivery_id | Id desta entrega. Repete nas retentativas, então serve de chave de idempotência do seu lado. |
| occurred_at | No 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.
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.