# 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

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

> 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](https://patrociniotech.com/documentacao-api/templates.md) com `status=ALL`.

## message.status

corpo do POST

```text
{
  "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
  }
}
```

## message.received

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

```text
{
  "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!",
    "media_id": null,
    "media_mime": null,
    "media_filename": null,
    "reply_to_message_id": "wamid.HBgNNTUyMT...",
    "contact_name": "Maria",
    "occurred_at": "2026-08-02T11:14:22Z",
    "window_expires_at": "2026-08-03T11:14:22Z"
  }
}
```

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

> Atenção: **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](https://patrociniotech.com/documentacao-api/janela.md). 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

| 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

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.

> Atenção: 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)

```javascript
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;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  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;

  return true;
}

app.post("/webhooks/patrociniotech", async (req, res) => {
  if (!assinaturaValida(req, process.env.PT_WEBHOOK_SECRET)) return res.sendStatus(401);
  const { delivery_id, event } = req.body;
  if (!delivery_id || !["message.status", "message.received"].includes(event)) {
    return res.sendStatus(400);
  }
  try {
    // Implemente com armazenamento durável e chave única por integração + delivery_id.
    // A operação precisa confirmar a gravação ou reconhecer uma duplicata já gravada.
    await gravarNaFilaDuravel(req.body);
    return res.sendStatus(200);
  } catch {
    return res.sendStatus(503); // permite nova tentativa se a gravação falhou
  }
});
```

PHP

```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;
}

$esperada = 'sha256=' . hash_hmac('sha256', $ts . '.' . $corpoCru, $segredo);
if (!hash_equals($esperada, $assinada)) {
    http_response_code(401);
    exit;
}

try {
    $evento = json_decode($corpoCru, true, 512, JSON_THROW_ON_ERROR);
    // Implemente com chave única por integração + delivery_id e confirme a gravação.
    gravarNaFilaDuravel($evento);
    http_response_code(200);
} catch (JsonException $erro) {
    http_response_code(400);
} catch (Throwable $erro) {
    http_response_code(503);
}
```

> 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

| 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

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.

> Atenção: 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

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](https://patrociniotech.com/documentacao-api/entregas.md).

---

Fonte: https://patrociniotech.com/documentacao-api/webhook
Índice para IA: https://patrociniotech.com/documentacao-api/llms.txt
