# Eventos

Os eventos avisam o que acontece na inbox, na hora: mensagem nova, mudança de status, edição, reação. Eles
chegam por [`GET /events`](/endpoint/get/events), uma conexão aberta (SSE), e pelo [**webhook**](#webhook), com os
mesmos eventos e o mesmo formato.

## Conectar

```bash
curl -N "https://<subdomínio>.orkesio.com/events?events=message.received,message.status" \
  -H "token: <token da inbox>"
```

- `events`: os eventos que você quer, separados por vírgula. Sem `events`, chegam todos, menos `status_post.received`
  (os Status de todos os contatos), `newsletter.updated` e `newsletter.message` (os canais) e `history.batch` (o
  histórico em lotes), que só vêm pedidos pelo nome.
- Só os nomes da Orkesio (com ponto). Um nome da uazapi (`messages`) responde `400`: para os eventos da uazapi, use o
  `/sse`, que continua igual.
- `exclude`: o mesmo filtro do webhook, com os mesmos valores. `exclude=sent_by_api` tira as mensagens enviadas pela
  api. Os valores: `sent_by_api`, `not_sent_by_api`, `out`, `in`, `groups` e `not_groups`. Valor que não existe, ou
  `in` junto com `out`, responde `400`.
- Ao conectar, chega a linha `: connected` (um comentário do SSE, que o `EventSource` do navegador ignora).

Com o `EventSource` (navegador), cada evento chega no `onmessage`, e o tipo está dentro do JSON. O `EventSource` não
manda headers; o token pode ir na URL (`?token=`), mas aí ele fica no histórico e nos logs. Prefira conectar pelo seu
servidor e repassar ao navegador.

```js
const source = new EventSource("https://<subdomínio>.orkesio.com/events?events=message.received&token=<token>");
source.onmessage = (e) => {
  const event = JSON.parse(e.data);
  if (event.type === "message.received") mostrar(event.data);
};
```

## O formato

Cada evento chega como duas linhas do SSE: o `id` do evento e o `data`, um JSON com o **envelope**:

```
id: evt_01JAB3K9ZQ7V0M4T8R2X6WYD5H
data: {"id":"evt_01JAB3K9ZQ7V0M4T8R2X6WYD5H","type":"message.received","created_at":"2026-10-17T12:00:00.000Z","inbox":{...},"data":{...}}
```

| Campo | O que é |
|---|---|
| `id` | Id do evento: `evt_` + 26 letras, em ordem de criação. Use para descartar repetidos. |
| `type` | O evento (`message.received`, `message.status`...). |
| `created_at` | Quando o evento aconteceu (ISO 8601, UTC). |
| `inbox` | A inbox: `id`, `name`, `phone` (o número, só no WhatsApp), `account_id` (a Página, a conta ou o bot, nos outros canais) e `channel` (`whatsapp_web`, `whatsapp_cloud`, `messenger`, `instagram` ou `telegram`). |
| `data` | O conteúdo, com formato fixo por `type` (tabela abaixo). |

O envelope é o mesmo do webhook: quem trata um, trata o outro.

## Os eventos de mensagem

| Evento | Quando chega | `data` |
|---|---|---|
| [`message.received`](/webhook/message.received) | O contato mandou uma mensagem. | `message` (a [mensagem](/docs/mensagem)), `chat` (a conversa, com o `lead_id`) e `lead` (só em conversa direta) |
| [`message.sent`](/webhook/message.sent) | Uma mensagem da inbox saiu: pela API, por automação, pelo celular ou por outro aparelho (`sent_by` diz qual). | `message`, `chat` e `lead`, como em `message.received` |
| [`message.status`](/webhook/message.status) | Uma mensagem da inbox foi entregue, lida, ouvida ou falhou. | `chat`, `status`, `at` e `messages` (os ids); `error` na falha |
| [`message.failed`](/webhook/message.failed) | Só as falhas: uma mensagem da inbox não saiu (recusada, fora do prazo, cancelada ou da campanha). Para quem só quer saber dos problemas. | `message` (com `status: failed` e o `error`) e `chat` (`id`, `lid`, `type`, `name`) |
| [`group.receipts`](/webhook/group.receipts) | Participantes de um grupo receberam ou leram uma mensagem da inbox. Em lote, a cada 60 segundos. | `chat`, `message` e `receipts` (quem, o quê, quando) |
| [`message.updated`](/webhook/message.updated) | Uma mensagem mudou: editada, apagada, fixada, desafixada, transcrita ou com a mídia pronta. | `change` (o que mudou), `message` (já com a mudança) e `chat` |
| [`message.reaction`](/webhook/message.reaction) | Alguém reagiu a uma mensagem, ou tirou a reação. | `message` (a reação, como mensagem com `type: reaction`) e `chat` |

## Os demais eventos

| Evento | Quando chega | `data` |
|---|---|---|
| [`chat.updated`](/webhook/chat.updated) | Uma conversa mudou: arquivada, fixada, silenciada, etiquetas, não lidas, automação, efêmeras, bloqueio, nome. Também quando a conversa aparece pela primeira vez (`changes` vazio). Para quem recebe o `message.received` e o `message.sent`, a mensagem nova não gera este evento só pelas não lidas e pela vez: elas vêm no `data.chat` deles. | `chat` (o estado novo) e `changes` (o que mudou) |
| [`chat.presence`](/webhook/chat.presence) | O contato ficou online ou offline, ou está digitando, gravando áudio ou parou. | `chat`, `state`; no grupo, `participant`; `last_seen_at` no offline, quando o contato mostra |
| [`inbox.status`](/webhook/inbox.status) | A conexão da inbox mudou: conectando, QR code ou código de pareamento pronto, conectada, desconectada, deslogada ou banida. | `status`; `qr_code` ou `pair_code` para mostrar; `reason` e `banned_until` na queda |
| [`label.updated`](/webhook/label.updated) | Uma etiqueta do WhatsApp Business foi criada, editada ou apagada. | `action`, `id`, `name`, `color` (na exclusão, só `action` e `id`) |
| [`call.updated`](/webhook/call.updated) | Ligação tocando, atendida, recusada, encerrada ou perdida. | a ligação: `id`, `chat`, `author`, `direction`, `status`, `at`; `video` quando se sabe e `duration_seconds` na encerrada |
| [`status_post.received`](/webhook/status_post.received) | Um contato postou um Status. Só chega pedido pelo nome. | `message` (a mensagem do Status) |
| [`newsletter.updated`](/webhook/newsletter.updated) | As visualizações ou reações dos posts de um canal mudaram, depois de [`POST /newsletters/{newsletter_id}/live-updates`](/endpoint/post/newsletters~:newsletter_id~live-updates) e enquanto a assinatura vale. Só chega pedido pelo nome. | `newsletter_id` e `posts` (`server_id`, `reactions` e `views` só quando muda) |

**O que não chega aqui:**

- Registros do grupo e do sistema (alguém entrou, o nome mudou) não chegam como `message.received`, para não disparar
  automação que espera uma mensagem de verdade. Eles ficam na [busca](/docs/buscar) (`category=event`).
- Publicações de Status (`status@broadcast`) não chegam nos eventos de mensagem: chegam em `status_post.received`,
  pedido pelo nome.
- O recibo de leitura das mensagens **recebidas** (quando você as lê pelo celular) não chega em `message.status`, que
  é só das mensagens da inbox.

## Ordem, repetição e quedas

- **Ordem:** os eventos de uma inbox saem na ordem em que acontecem.
- **Repetição:** use o `id` do evento para descartar um repetido.
- **Sem reenvio:** se a conexão cair, os eventos desse intervalo não são reenviados. Ao reconectar:
  - **mensagens novas:** busque com [`GET /messages`](/docs/buscar) usando `after` com a hora do último evento
    recebido;
  - **mudanças em mensagens antigas** (status, edição, reação, apagada): o `after` filtra pela hora em que a mensagem
    foi **enviada**, não pela hora da mudança. Uma mensagem enviada às 10h e lida às 12h, durante a queda, não volta em
    `after=12h`. Para elas, consulte de novo as que ainda importam (por exemplo, as suas enviadas ainda sem `read`,
    com `id` ou com `direction=out` no período).
- **Cliente lento:** cada conexão tem uma fila curta. Se o cliente não lê, os eventos que não cabem são descartados;
  leia sem bloquear e processe à parte.

## Webhook

O webhook entrega os mesmos eventos, com o mesmo envelope, por `POST` no seu endereço. As rotas
ficam em [`/webhooks`](/endpoint/get/webhooks); os webhooks da uazapi (`/webhook`) continuam iguais e separados.

### Criar

```json
POST /webhooks
{
  "name": "CRM",
  "url": "https://crm.exemplo.com/whatsapp",
  "events": ["message.received", "message.status"],
  "retries": 3,
  "headers": { "Authorization": "Bearer seu-token" }
}
```

- Até 5 webhooks por inbox.
- `events`: os nomes da Orkesio (com ponto). O formato da entrega é o envelope desta página.
- `url`: `https://` (recomendado) ou `http://`, sempre em endereço público. Mandar de novo uma `url` que já existe atualiza aquele webhook (`200`), sem criar outro.
- `headers`: até 5, enviados em toda entrega, para o seu destino autenticar. Nas respostas, o valor volta como `***`.
- A resposta da criação (`201`) traz o **`secret`** inteiro, só nela (e quando você o troca com `rotate_secret`): guarde-o. Na atualização pela mesma `url` (`200`) ele vem mascarado e o que você guardou continua valendo.

### Webhook global

O dono do servidor (header `admintoken`) tem **um** webhook global, em [`/server/webhook`](/endpoint/get/server~webhook),
com os mesmos campos e a mesma entrega. Ele recebe os eventos de **todas as inboxes** do servidor; `inbox` (no
envelope) diz de qual veio cada evento, e as falhas guardadas trazem a inbox em `inbox_id`.

### O que chega

Cada evento é um `POST` com o envelope no corpo (`Content-Type: application/json`) e três headers do padrão
[Standard Webhooks](https://www.standardwebhooks.com):

| Header | O que é |
|---|---|
| `Webhook-Id` | O id do evento (o mesmo `id` do envelope). Igual nas novas tentativas: use para descartar repetidos. |
| `Webhook-Timestamp` | Quando a entrega saiu (segundos Unix). Recuse entregas muito antigas (por exemplo, mais de 5 minutos). |
| `Webhook-Signature` | `v1,` e o HMAC-SHA256, em base64, de `<Webhook-Id>.<Webhook-Timestamp>.<corpo>`, com a chave do `secret` (a parte depois de `whsec_`, decodificada de base64). |

Conferir a assinatura prova que a entrega saiu da Orkesio e que o corpo não mudou. O passo a passo, com exemplos
em Node, Python e PHP e um caso de teste, está em [Conferir a assinatura (HMAC)](/docs/assinatura).

### Responder

Responda `2xx` rápido, depois de registrar o evento, e processe à parte. O corpo da sua resposta é ignorado.

### Quando a entrega falha

- **Novas tentativas:** com `retries` (até 3), a entrega é repetida depois de 10 s, 1 min e 5 min, só quando não há
  risco de chegar em dobro: a conexão nem abriu, ou o destino respondeu `429`, `502` ou `503`. Tempo esgotado, `500`
  e `504` não repetem, porque o destino pode ter processado o evento.
- **Falhas guardadas:** a entrega de `message.received` ou `message.sent` que falhou de vez fica 24 horas em
  [`GET /webhooks/{webhook_id}/failures`](/endpoint/get/webhooks~:webhook_id~failures). A falha dos outros eventos
  conta só em `health`.
- **Reenvio:** [`POST /webhooks/resends`](/endpoint/post/webhooks~resends) reenvia as de
  mensagem, montadas de novo a partir da mensagem salva, com o mesmo `Webhook-Id`.
- **Desligamento:** depois de 24 horas sem nenhuma resposta boa (com pelo menos 10 falhas), a API desliga o webhook e
  marca `health.disabled_at`. Salvar de novo (`PATCH`) religa.
- **Saúde:** cada webhook traz `health`, com o tempo de resposta do destino, as falhas em seguida e o último erro.
- **Teto de espera:** cada webhook guarda até 200 novas tentativas aguardando o intervalo (10 s, 1 min, 5 min). Com o
  destino fora do ar e muitos eventos, o que passa disso falha de vez na hora.
- **Reinício do servidor:** as novas tentativas que estavam esperando se perdem e não ficam nas falhas guardadas.

### Repetidos

A entrega é **best effort**: um evento pode se perder (resposta `500`, tempo esgotado, teto de espera ou reinício, como
acima) e pode chegar duas vezes (um reenvio das falhas guardadas, ou um `502`/`503` devolvido por um proxy depois de o
destino já ter processado). Guarde o `Webhook-Id` dos últimos eventos e ignore o que já viu; para não perder dado,
confira pelas rotas de leitura o que importa.

### Vindo do webhook da uazapi

O [`GET /webhooks`](/endpoint/get/webhooks) também lista os webhooks da uazapi (`format: "v1"`, eventos em
`v1_events`). Por aqui, eles só ligam e desligam (`PATCH` com `enabled`) e se apagam; o resto se altera no `/webhook`.
Para passar a receber o formato da Orkesio:

1. Crie um webhook com `POST /webhooks` e os eventos da Orkesio. Pode ser o mesmo link do webhook da uazapi, com um
   caminho ou header que diga o formato.
2. Confira que as entregas chegam e que a assinatura bate.
3. Desligue o da uazapi (`PATCH` com `enabled: false`) ou apague. Enquanto os dois estiverem ligados, cada evento chega
   duas vezes, uma em cada formato.
