# Webhooks

> Receba os eventos da instância no seu sistema, confira a assinatura e trate repetições.

Um webhook é um endereço do seu sistema que recebe, por `POST` em JSON, os eventos da instância: mensagens
recebidas e enviadas, confirmações de entrega e leitura, mudanças de conexão e muito mais.

## Cadastrar

Pelo painel (aba **Webhooks** da instância) ou pela API:

```bash
curl -X POST "https://app.onzap.io/v1/instances/SUA_INSTANCIA/webhooks" \
  -H "X-Instance-Token: SEU_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://seu-sistema.com.br/webhooks/onzap", "events": ["message.any", "message.ack"]}'
```

- Até 5 webhooks por instância, cada um com os seus eventos (`"*"` assina todos).
- A resposta traz o `secret` usado na assinatura; `POST /webhooks/{webhookId}/rotate-secret` gera outro.
- `POST /webhooks/{webhookId}/test` envia um evento de teste para conferir o endereço.

## Formato

Todo evento chega com o mesmo envelope, nos dois tipos de instância:

```json
{
  "id": "evt_01jabcdefghjkmnpqrstvwxyz0",
  "event": "message.any",
  "instanceId": "i01jabcdefghjkmnpqrstvwxyz",
  "provider": "onzap",
  "timestamp": 1767225600000,
  "payload": {
    "id": "false_5511999999999@c.us_3EB0C0A1B2C3D4E5F6",
    "chatId": "5511999999999@c.us",
    "isGroup": false,
    "chatName": "Maria",
    "chatPicture": "https://app.onzap.io/v1/avatars/i01jabcdefghjkmnpqrstvwxyz/5511999999999@c.us?exp=…&sig=…",
    "fromMe": false,
    "from": { "id": "5511999999999@c.us", "phone": "5511999999999", "pushName": "Maria" },
    "type": "text",
    "text": "Oi! Vocês entregam em Campinas?",
    "raw": { "…": "evento original do WhatsApp" }
  },
  "me": { "id": "5511888888888@c.us", "phone": "5511888888888", "pushName": "Minha Loja" }
}
```

Os headers trazem o tipo (`X-OnZap-Event`), o id do evento (`X-OnZap-Event-Id`), o id da entrega
(`X-OnZap-Delivery`) e a assinatura (`X-OnZap-Signature`). Todos os eventos e os seus formatos estão em
[Eventos de webhook](https://developer.onzap.io/referencia/eventos-de-webhook).

Todos os eventos seguem as mesmas regras:

- **Mesmos nomes de campo em tudo:** `chatId`, `from`, `timestamp` (segundos), `fromMe`. Um contato é sempre
  `{id, phone, lid, pushName}`, um grupo é sempre `{id, name, …}`, como nas rotas de leitura.
- **A mesma pessoa tem sempre o mesmo `chatId`:** o número (`…@c.us`) sempre que ele é conhecido, mesmo quando o
  WhatsApp manda o LID. O LID original vem à parte (`chatLid`, `from.lid`).
- **`raw` traz o evento original** do WhatsApp ou da Meta, para algum detalhe que o formato OnZap não cubra. O
  formato dele pode mudar; prefira sempre os campos de cima.

## Eventos

**Mensagens e conexão** (nos dois tipos de instância, no mesmo formato):

| Evento | Quando |
|---|---|
| `message` | Mensagem **recebida** |
| `message.any` | Qualquer mensagem: recebida ou enviada pela conta (`fromMe: true`) |
| `message.ack` | Status de uma mensagem enviada: `sent`, `delivered`, `read`, `played` ou `failed` |
| `message.reaction` | Reação adicionada ou removida (`emoji` vazio) |
| `message.edited` | Texto de uma mensagem editado (na API oficial, só com coexistência) |
| `message.revoked` | Mensagem apagada para todos (na API oficial, só com coexistência) |
| `session.status` | Conexão mudou: `WORKING`, `SCAN_QR_CODE`, `STOPPED`, `FAILED`… |
| `queue.sent`, `queue.failed` | Resultado de um envio da [fila](https://developer.onzap.io/fila) |

**Só na API OnZap:**

| Evento | Quando |
|---|---|
| `message.ack.group` | Um participante recebeu ou leu uma mensagem enviada a um grupo |
| `presence.update` | Online, offline, digitando ou gravando (conversas com presença assinada) |
| `group.join`, `group.leave` | Este número entrou (ou foi adicionado) num grupo, ou saiu dele |
| `group.update` | Nome, descrição, link de convite ou permissões do grupo mudaram (`changed` diz o quê) |
| `group.participants` | Participantes entraram, saíram, viraram admin ou deixaram de ser |
| `group.join_request` | Pedido para entrar num grupo com aprovação |
| `poll.vote` | Voto numa enquete (`selectedOptions`) |
| `event.response` | Resposta a um convite de evento (agenda) |
| `call.received`, `call.accepted`, `call.rejected` | Chamadas de voz ou vídeo |
| `label.upsert`, `label.deleted`, `label.chat.added`, `label.chat.deleted` | Etiquetas do WhatsApp Business |

**Só na API oficial:**

| Evento | Quando |
|---|---|
| `template.status`, `template.quality`, `template.category` | A Meta aprovou, pausou, mudou a nota ou a categoria de um template |
| `phone.quality`, `phone.name` | Qualidade e limite do número; nome de exibição aprovado ou recusado |
| `account.update`, `account.alert` | Violação, restrição, verificação ou alerta da conta do WhatsApp Business |
| `contact.marketing` | O cliente pediu para parar (ou voltou a aceitar) mensagens de marketing |
| `contact.sync`, `history.sync` | Coexistência: agenda e histórico de conversas do app WhatsApp Business |

**`message` ou `message.any`?** `message` traz só as recebidas. Para acompanhar a conversa inteira, inclusive o que
foi enviado pelo celular ou por outro sistema, assine `message.any`. Nas enviadas, `source` diz a origem: `api` ou
`app` (celular), e `status`, como está a entrega.

## Mensagens que vieram de anúncio

Quando o cliente chega por um anúncio de clique para o WhatsApp (Facebook ou Instagram), a primeira mensagem dele
traz o anúncio em `referral`, nos dois tipos de instância:

```json
"referral": {
  "sourceType": "ad",
  "sourceId": "120211234567890",
  "sourceUrl": "https://fb.me/abc",
  "headline": "Promoção de inverno",
  "body": "Até 40% off",
  "mediaType": "image",
  "clickId": "ARAkLkA8rmlFeiCktEJQ"
}
```

Use o `sourceId` para saber de qual anúncio veio cada conversa, e o `clickId` para devolver vendas e leads à
API de conversões da Meta.

## Assinatura

O header `X-OnZap-Signature: t=<unix>,v1=<hex>` traz o HMAC-SHA256 de `"<t>.<corpo cru>"` com o `secret` do webhook.
Confira antes de processar e recuse `t` com mais de 5 minutos:

```js
// Node.js
import crypto from "node:crypto";

function assinaturaValida(corpoCru, header, segredo) {
  const partes = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const esperado = crypto.createHmac("sha256", segredo).update(`${partes.t}.${corpoCru}`).digest("hex");
  const recente = Math.abs(Date.now() / 1000 - Number(partes.t)) < 300;
  return recente && crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(partes.v1 ?? ""));
}
```

```php
// PHP
function assinaturaValida(string $corpoCru, string $header, string $segredo): bool {
    parse_str(str_replace(',', '&', $header), $p);
    $esperado = hash_hmac('sha256', $p['t'] . '.' . $corpoCru, $segredo);
    return abs(time() - (int) $p['t']) < 300 && hash_equals($esperado, $p['v1'] ?? '');
}
```

```python
# Python
import hashlib, hmac, time

def assinatura_valida(corpo_cru: bytes, header: str, segredo: str) -> bool:
    partes = dict(p.split("=", 1) for p in header.split(","))
    esperado = hmac.new(segredo.encode(), f"{partes['t']}.".encode() + corpo_cru, hashlib.sha256).hexdigest()
    recente = abs(time.time() - int(partes["t"])) < 300
    return recente and hmac.compare_digest(esperado, partes.get("v1", ""))
```

Use o **corpo cru**, exatamente como chegou: se o seu framework já converteu o JSON, a assinatura não bate.

## Entrega

- **Confirmação:** responda qualquer `2xx` em até 10 segundos. Processe depois, numa fila sua, se for demorado.
- **Novas tentativas:** sem `2xx`, tentamos de novo após 10 s, 1 min, 5 min, 15 min, 1 h, 3 h, 6 h e 12 h.
- **Pelo menos uma vez:** o mesmo evento pode chegar mais de uma vez e fora de ordem. Use o `id` do envelope para
  descartar repetidos.
- **Desativação:** um endereço que falha sem parar por 24 horas (ou responde `410`) é desativado, e avisamos por
  e-mail. Reative pelo painel ou por `PATCH /webhooks/{webhookId}`.
- **Histórico:** `GET /webhooks/{webhookId}/deliveries` lista as entregas; `.../retry` reenvia uma.
- **Tempo de resposta:** cada entrega traz `durationMs`, quanto o seu servidor levou para responder. O webhook
  traz `responseTime` com a mediana (`medianMs`) e o caso lento típico (`p95Ms`) das entregas com sucesso nas
  últimas 24 horas. Responda rápido (de preferência em menos de 1 segundo) e processe depois, numa fila sua.

## Inspetor

Na aba **Inspetor** da instância, um clique cria um webhook apontando para um link da própria OnZap. Os eventos
aparecem na hora, montados como conversa (balões, confirmações, reações e mídia) ou como requisição crua, com
headers e a conferência da assinatura. É o jeito mais rápido de ver o que chega antes de programar o seu endpoint.
