# Catálogo de eventos

> Todos os eventos que o hub envia ao seu webhook, com o formato de cada um.

Fonte: https://metahub.matratecnologia.com/docs/eventos

O campo `type` do envelope assume um destes valores. Os `connection.*` e `whatsapp.*` são emitidos; os de lead ainda não.

| `type` | Quando | Situação |
|---|---|---|
| `connection.created` | Cliente concluiu a conexão do canal | emitido |
| `connection.updated` | Dados ou estado da conexão mudaram | emitido |
| `connection.disconnected` | Conexão encerrada ou revogada | emitido |
| `connection.token_expiring` | Credencial da conexão perto de expirar | emitido |
| `whatsapp.message.received` | Mensagem recebida | emitido |
| `whatsapp.message.status` | Mudança de estado de um envio no hub, ou status de entrega da Meta | emitido |
| `whatsapp.message.echo` | Mensagem enviada pelo app do WhatsApp Business (Coexistence) | emitido |
| `whatsapp.history.received` | Bloco do histórico sincronizado (Coexistence) | emitido |
| `whatsapp.contacts.synced` | Contatos do app WhatsApp Business sincronizados (Coexistence) | emitido |
| `whatsapp.identity.updated` | O contato trocou de número ou de BSUID | emitido |
| `whatsapp.media.updated` | Download de uma mídia recebida terminou (ou falhou) | emitido |
| `whatsapp.template.status_updated` | Status de aprovação de template mudou | emitido |
| `whatsapp.template.updated` | Categoria, qualidade ou componentes de template mudaram | emitido |
| `whatsapp.usage.free_tier` | O nível gratuito de mensagens de serviço do número chegou a 800 ou acabou | emitido |
| `leadgen.lead.created` | Novo lead de formulário do Lead Ads | planejado |

Trate `type` desconhecido como "ignorar e responder 2xx", para novos eventos não gerarem retries no seu receptor. Vale o mesmo para campo novo dentro de `data` e para `message.type` desconhecido.

## Eventos de conexão

Em todos, `connectionId` do envelope é o id da conexão e `data` é a conexão no mesmo formato de `GET /v1/connections/:connectionId` (sem o envoltório `connection`), no estado logo após a mudança. Use `createdAt` do envelope para ordenar, já que a ordem de chegada não é garantida.

| `type` | Origem | `meta` |
|---|---|---|
| `connection.created` | Cliente concluiu o fluxo; `status` ainda `pending` | `null` |
| `connection.updated` | Ativação concluída (`pending` para `active`), falha de ativação (`error`), reconexão, token vencido (`error` com `token_expired`), aviso da Meta ou mudança do `health` (`meta.health: "changed"`) | ver abaixo |
| `connection.disconnected` | `DELETE /v1/connections/:id`, ou a Meta removeu o acesso | `{ "reason": ..., "accountEvent": ... }` |
| `connection.token_expiring` | A credencial vence em até 7 dias; um aviso por conexão | `null` |

**`connection.disconnected`.** `meta.reason` diz quem encerrou: `api` (seu `DELETE`), `meta_uninstalled` (o cliente removeu o parceiro no WhatsApp Manager) ou `meta_account_deleted` (a conta do WhatsApp foi apagada). Nos dois últimos, `meta.accountEvent` traz o nome do evento da Meta (`PARTNER_APP_UNINSTALLED`, `ACCOUNT_DELETED`); no primeiro o campo não existe.

**Eventos originados por avisos da Meta.** Quando um `connection.updated` nasce de um aviso de conta da Meta, o envelope traz `meta: { "accountEvent": "<nome>" }`:

| `meta.accountEvent` | O que mudou na conexão |
|---|---|
| `ACCOUNT_OFFBOARDED` | `active` passa para `error` com `errorCode: "offboarded"` |
| `ACCOUNT_RECONNECTED` | `error` com `offboarded` volta para `active` |
| `DISABLED_UPDATE`, `ACCOUNT_RESTRICTION`, `ACCOUNT_VIOLATION` | Nada muda no estado: é um aviso de que a conta sofreu restrição ou violação. Sai um `connection.updated` para **cada** conexão `active`, `pending` ou `error` da conta (WABA), e sai de novo se a Meta reentregar o mesmo aviso: deduplique pelo seu lado se a reação não puder repetir |
| `MM_LITE_TERMS_SIGNED` | Os termos do Marketing Messages API foram aceitos; emitido uma vez por conta |

Os demais avisos de conta da Meta não geram evento. Se o seu receptor precisa reagir a uma restrição, trate `connection.updated` com `meta.accountEvent` em `DISABLED_UPDATE`, `ACCOUNT_RESTRICTION` ou `ACCOUNT_VIOLATION`.

**`connection.token_expiring`.** Chega uma vez, até 7 dias antes de `tokenExpiresAt`. Se nada for feito, no vencimento a conexão vai para `error` com `token_expired` (`connection.updated`) e o cliente precisa repetir o fluxo de conexão com uma sessão nova.

## Eventos do WhatsApp

Em todos, `connectionId` do envelope é a conexão do número, exceto nos eventos de template, que são da conta (WABA) e vêm com `connectionId: null`. Datas em ISO 8601. `meta`, quando preenchido, traz o item original da Meta, só para diagnóstico: o contrato é o `data`.

**O hub não entrega o que chegou antes da conexão atual.** Mensagem, status ou aviso com data anterior à conexão do número ao workspace (ou à entrada da conta no workspace) é descartado: é dado do dono anterior. Isso vale também para retries da Meta depois de desconectar e reconectar em outro workspace.

### `whatsapp.message.received`

```json
{
  "phoneNumberId": "109876543210987",
  "message": {
    "wamid": "wamid.HBgM...",
    "type": "text",
    "timestamp": "2026-10-23T15:00:00.000Z",
    "from": { "userId": "BR.1349...", "parentUserId": null, "waId": "5543999990000", "username": null, "name": "Maria" },
    "groupId": null,
    "context": null,
    "referral": null,
    "text": { "body": "Oi" }
  },
  "window": { "expiresAt": "2026-10-24T15:00:00.000Z" }
}
```

- `meta`: `{ message: <item original>, contact: <contacts[] do remetente> | null }`.
- O conteúdo fica num campo com o nome do `type`: `text`, `media` (para `image`, `audio`, `video`, `document`, `sticker`: `{ id, status, mimeType, sha256, caption, filename, voice, animated }`, com `id` do hub), `location`, `contacts`, `interactive` (`button_reply`, `list_reply`, `nfm_reply` com `flow.response` já como objeto, `product`), `button` (resposta de botão de template), `reaction` (`emoji: null` = reação removida), `order`, `revoke`, `edit` (`{ wamid, message }`, com a mensagem nova no mesmo formato; `edit` e `revoke` só chegam de número Coexistence, porque a Meta só oferece esses webhooks para o app WhatsApp Business, e o contato só vê "Editar" e "Apagar para todos" nessas conversas) e `unsupported` (`{ type, errors: [{ code, title }] }`).
- `interactive.product.priceAmount`/`salePriceAmount` vêm como a Meta manda: o valor multiplicado por 1 000. `order.productItems[].itemPrice` vem como número decimal.
- `type` fora da lista conhecida mantém o nome e ganha `unsupported`; ignore o que não reconhecer e responda 2xx.
- `context`: `{ wamid, from, forwarded, frequentlyForwarded, referredProduct }` quando a mensagem responde ou cita outra; `referral` em mensagem vinda de anúncio (com `ctwaClid`).
- `groupId` vem preenchido em mensagem de grupo; ela não abre janela (`window: null`).
- Deduplicação: o hub emite uma vez por `wamid`, mas a entrega é "pelo menos uma vez"; deduplique pelo `id` do envelope.

### `whatsapp.message.status`

Dois tipos de origem, pelo `data.source`:

`source: "hub"` (transição do envio no hub: `accepted`, `failed`, `uncertain`, `abandoned`):

```json
{
  "messageId": "...",
  "wamid": "wamid.HBgM...",
  "source": "hub",
  "status": "accepted",
  "timestamp": "2026-10-23T15:00:00.000Z",
  "pacing": null,
  "error": null
}
```

`error` é `{ code, metaCode }` em `failed`; em `uncertain` vem `null`, e o código da Meta, quando houver, aparece só em `metaErrorCode` de `GET …/messages/:messageId`. `meta` traz `{ reconciled: true }` (e `overridesAdminResolution: true`) quando o `accepted` veio da conciliação, e `{ resolvedByAdmin: true }` no `abandoned`. O `accepted` com `overridesAdminResolution` vence o `abandoned` do mesmo `messageId` em qualquer ordem.

`source: "meta"` (status de entrega do webhook da Meta: `sent`, `delivered`, `read`, `played`, `failed`):

```json
{
  "messageId": "...",
  "wamid": "wamid.HBgM...",
  "source": "meta",
  "status": "delivered",
  "timestamp": "2026-10-23T15:00:03.000Z",
  "recipient": { "userId": "BR.1349...", "parentUserId": null, "waId": "5543999990000" },
  "errors": [],
  "pricing": { "billable": true, "model": "PMP", "type": "regular", "category": "utility", "subtype": null },
  "conversation": null
}
```

- **Correlação:** use `messageId` (id do envio no hub). Ele vem `null` quando o status chegou antes da resposta da Meta ao envio, ou é de mensagem enviada fora do hub; nesse caso correlacione pelo `wamid`, que chega no `accepted` (`source: "hub"`). O status com `messageId: null` não é reemitido quando o hub liga a mensagem depois; o `deliveryState` do envio já reflete o status.
- `pricing` vem só em alguns status (`sent` e um de `delivered`/`read`), com `category` como a Meta manda (`marketing_lite`, `authentication-international`…). `conversation` quase sempre `null`: desde a Graph v24.0 a Meta só o manda em mensagem enviada dentro de uma janela de ponto de entrada gratuito, e `expiresAt` só vem no `sent`. Reação enviada pelo hub chegou no teste manual só com `sent` e sem `pricing`. Ver [Custos](https://metahub.matratecnologia.com/docs/custos.md).
- `errors`: `[{ code, title, details }]` em `failed`. `131049` (esperar), `132015` (template pausado) e `131050` (contato não quer marketing) são só repassados: a decisão é do seu app.
- Cada status (por mensagem e por valor de `status`) sai uma vez. A Meta pode pular `delivered` quando entrega e leitura chegam juntas.

### `whatsapp.message.echo`

Mensagem que o próprio negócio enviou pelo app WhatsApp Business (Coexistence): `{ phoneNumberId, message }`, com `message` no formato do recebido e `message.from` sendo o negócio, mais `message.to` (identidade do contato). `meta`: `{ message, contact }`. Eco não abre janela.

### `whatsapp.history.received`

Histórico de até 180 dias do app WhatsApp Business (Coexistence), em blocos de até 100 mensagens de uma conversa:

```json
{
  "phoneNumberId": "109876543210987",
  "phase": 0,
  "chunkOrder": 3,
  "progress": 40,
  "thread": { "userId": "BR.1349...", "parentUserId": null, "waId": "5543999990000", "username": null },
  "messages": []
}
```

- Cada mensagem tem o formato do recebido mais `direction` (`inbound` ou `outbound`) e `historyStatus` (status da Meta em maiúsculas). Mídia antiga vem como `type` `media_placeholder`.
- Os blocos chegam fora de ordem; ordene pelo `timestamp` das mensagens. `progress` 100 marca o fim (também visível em `sync[].progress` da conexão).
- A mídia dos últimos 14 dias chega depois, num evento próprio por mensagem, com `phase`, `chunkOrder` e `progress` `null`, e a mídia baixada como em qualquer mensagem recebida.
- Histórico não abre janela.

### `whatsapp.contacts.synced`

Contatos do app WhatsApp Business (Coexistence): `{ phoneNumberId, contacts: [{ action, userId, parentUserId, username, phone, fullName, firstName, timestamp }] }`, com `action` `add` ou `remove`. O hub não guarda contatos.

### `whatsapp.identity.updated`

```json
{
  "kind": "user_changed_user_id",
  "previous": { "userId": "BR.1349...", "parentUserId": null },
  "current": { "userId": "BR.8812...", "parentUserId": null, "waId": "5543999990000" },
  "timestamp": "2026-10-23T15:00:00.000Z"
}
```

`kind` é `user_changed_number` (trocou de telefone) ou `user_changed_user_id` (trocou de BSUID).

### `whatsapp.media.updated`

`{ id, status, mimeType, size, sha256, wamid, errorCode }`, com `status` `stored` (pronta: `GET /v1/media/:id`), `failed` ou `expired`. Pode chegar antes do `whatsapp.message.received` da mesma mensagem.

### `whatsapp.template.status_updated`

`{ template, event, reason, otherInfo: { title, description } | null, disableDate }`, com `template` no formato da API de templates e `event` como a Meta manda (`APPROVED`, `REJECTED`, `PAUSED`, `DISABLED`, `PENDING_DELETION`, `FLAGGED`, `REINSTATED`…). Um webhook mais velho que o status gravado não gera evento.

### `whatsapp.template.updated`

`{ template, change }`, com `change` `category_pending` (a Meta avisou que vai trocar a categoria: `template.correctCategory` e `pendingCategoryAt`), `category` (troca concluída), `quality` (`template.qualityScore`) ou `components` (o template foi editado; `template.components` já traz a versão nova). Aviso de qualidade ou de categoria mais velho que o último aplicado (a Meta reentrega fora de ordem) não muda o template nem gera evento.

### `whatsapp.usage.free_tier`

Nível gratuito de mensagens de serviço do número: `{ phoneNumberId, month, timezone, level, limit, free, charged }`, com `level` `warning` (800 gratuitas no mês) ou `exhausted` (primeira cobrada). Um por nível, número e mês. Detalhes em [Custos](https://metahub.matratecnologia.com/docs/custos.md).
