Webhook de status
Cadastre a URL do seu endpoint no painel, em Integrações. A cada mudança de status de uma mensagem enviada, fazemos um POST nela, assinado.
O que chega (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,
"phone_number_id": "840747309123670",
"client_ref": "pedido-8421",
"campaign_id": null
}
}Headers (link para esta seção)
| Header | O que é |
|---|---|
| X-PT-Timestamp | Epoch em SEGUNDOS, inteiro. Entra na assinatura. 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 | Nome do evento. Hoje sempre message.status. |
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") || "";
const esperada =
"sha256=" +
crypto.createHmac("sha256", segredo).update(`${ts}.`).update(req.rawBody).digest("hex");
// Comparação em tempo constante: comparar com === vaza, pelo tempo, o quanto você acertou.
const a = Buffer.from(assinada);
const b = Buffer.from(esperada);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false;
// Recusa evento velho (replay). 5 minutos é folgado e cobre relógio fora de sincronia.
return Math.abs(Date.now() / 1000 - Number(ts)) < 300;
}
app.post("/webhooks/patrociniotech", (req, res) => {
if (!assinaturaValida(req, process.env.PT_WEBHOOK_SECRET)) return res.sendStatus(401);
enfileirarParaProcessar(req.body); // grave e processe depois
res.sendStatus(200); // responda rápido
});$corpoCru = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_PT_TIMESTAMP'] ?? '';
$assinada = $_SERVER['HTTP_X_PT_SIGNATURE'] ?? '';
$esperada = 'sha256=' . hash_hmac('sha256', $ts . '.' . $corpoCru, $segredo);
if (!hash_equals($esperada, $assinada) || abs(time() - (int) $ts) > 300) {
http_response_code(401);
exit;
}
http_response_code(200);Campos do evento (link para esta seção)
| Campo | O que é |
|---|---|
| message_id | O mesmo id devolvido no envio. |
| status | sent, delivered, read ou failed. |
| status_rank | Ordem do ciclo de vida, de 1 a 4. A entrega não garante ordem: compare com o que você já guardou e ignore o menor, senão um sent atrasado apaga um read que já chegou. |
| 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 | O que você mandou no envio. É por aqui que se correlaciona. |
| 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 | Quando o evento aconteceu. NÃO muda entre tentativas (quem muda é o header). |
Como responder (link para esta seção)
Qualquer coisa fora de 2xx entra na fila de retentativa. Se o seu processamento é demorado, grave o evento e responda na hora; processe depois. Endpoint lento vira fila acumulada e atraso em todo mundo.
Trate o mesmo delivery_id chegando duas vezes como normal: a rede falha, e preferimos entregar de novo a perder. Use o delivery_id como chave de idempotência.
Se o seu endpoint cair (link para esta seção)
Nada se perde. Cada evento vira uma linha numa fila e tentamos de novo em 30 segundos, 2 minutos, 10 minutos, 1 hora, 6 horas e 24 horas: cerca de 31 horas de janela. O que esgota as tentativas fica guardado esperando reenvio, que você dispara pelo painel ou pela API de entregas.