# Eventos disponíveis (/docs/webhooks/events)





O PEPINO ZAP envia os eventos abaixo por webhook. Esta página é o **catálogo de eventos**: descreve o envelope comum a todos eles e, para cada evento, o objeto `data` **exato** que você recebe — campo a campo, com exemplos reais de payload.

Você cadastra os webhooks **via API** (`POST /v1/webhooks`), de forma self-service e escopada ao seu Projeto — veja [Webhooks](/docs/webhooks). Para validar que a entrega realmente veio do PEPINO ZAP, consulte [Assinatura](/docs/webhooks/signing). Para o comportamento de timeout, reenvio e idempotência, consulte [Entrega e retry](/docs/webhooks/delivery). Os **mesmos** eventos e o **mesmo** envelope também chegam por [WebSocket](/docs/webhooks/websocket), se você preferir um transporte em tempo real sem expor um endpoint público.

<Callout type="info">
  **Webhook ≠ histórico.** Estes eventos são o **push ao vivo** — o que acontece no momento. Para
  **ler o histórico persistido** (listar conversas, paginar mensagens, baixar a mídia já hospedada
  por nós), use os endpoints do **Inbox** com a **mesma API key** — `GET /v1/inbox/conversations` e
  `.../conversations/{id}/messages`. O PEPINO ZAP é o *system of record*: você **não** precisa
  armazenar as mensagens que chegam aqui por conta própria. Ver [Persistência e
  histórico](/docs/get-started/persistencia).
</Callout>

| Evento               | Quando dispara                                                                                       |
| -------------------- | ---------------------------------------------------------------------------------------------------- |
| `message.received`   | Uma mensagem com conteúdo chegou em uma instância.                                                   |
| `message.status`     | Mudou o status de entrega de uma mensagem **enviada** pela instância (entregue/lida).                |
| `instance.status`    | A conexão da instância com o WhatsApp mudou de estado.                                               |
| `call.received`      | A instância **recebeu uma chamada** (voz ou vídeo).                                                  |
| `message.deleted`    | Um contato &#x2A;*apagou (revogou)** uma mensagem em uma conversa com a instância.                   |
| `group.participants` | Alguém **entrou, saiu, foi promovido ou rebaixado** em um grupo.                                     |
| `group.updated`      | O **nome ou o tópico** (descrição) de um grupo mudou.                                                |
| `contact.updated`    | O **nome de exibição** (push name) de um contato mudou.                                              |
| `label.updated`      | Uma **etiqueta** (WhatsApp Business) foi criada, editada ou apagada.                                 |
| `label.association`  | Uma **etiqueta** foi aplicada ou removida de uma conversa.                                           |
| `campaign.status`    | Mudou o status de um **destinatário de campanha** (enviado/entregue/lido/respondeu/falhou/inválido). |

<Callout type="info">
  Um webhook só recebe os eventos que **assina**. Ao criar o webhook (`POST /v1/webhooks`), o campo
  `events` define a lista — e uma lista &#x2A;*vazia significa "todos os eventos"**. Para receber só
  mensagens recebidas, por exemplo, crie o webhook com `"events": ["message.received"]`. Os nomes
  válidos são exatamente os da tabela acima; qualquer outro valor é recusado com `400`. Ver
  [Webhooks](/docs/webhooks).
</Callout>

## Envelope comum [#envelope-comum]

Toda entrega usa o método `POST` e **sempre** o mesmo envelope. Apenas o conteúdo de `data` muda de um evento para outro — o que permite escrever um único handler que primeiro lê `event` e só então interpreta `data`.

```json
{
  "event": "message.received",
  "instanceId": "cm5x8a9k20001abc123def456",
  "timestamp": "2026-05-29T22:16:30Z",
  "data": {}
}
```

Campos do envelope:

| Campo        | Tipo   | Significado                                                                                                                                                                                                                                                                                                                                      |
| ------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `event`      | string | Nome do evento — um dos onze da tabela acima (`message.received`, `message.status`, `instance.status`, `call.received`, `message.deleted`, `group.participants`, `group.updated`, `contact.updated`, `label.updated`, `label.association`, `campaign.status`). **Faça o `switch` por aqui** — não tente adivinhar o tipo pelos campos de `data`. |
| `instanceId` | string | Identificador da instância que originou o evento (formato `cm...`). É o mesmo `id` retornado ao criar a instância. Em um Projeto com vários números, use-o para saber **de qual instância** veio o evento.                                                                                                                                       |
| `timestamp`  | string | Data e hora do evento em **ISO 8601 / RFC 3339, em UTC** (sufixo `Z`). Para `message.received` e `message.status`, é o horário do evento no WhatsApp; para `instance.status`, o horário da transição de conexão.                                                                                                                                 |
| `data`       | objeto | Os dados específicos do evento. O formato depende de `event` — detalhado nas seções abaixo.                                                                                                                                                                                                                                                      |

Headers presentes em **toda** entrega:

```http
Content-Type: application/json
User-Agent: pepino-zap-webhooks/1
X-Pepino-Event: message.received
X-Pepino-Webhook-Id: <id-do-webhook>
X-Pepino-Signature: sha256=<hmac-hex>
```

* `X-Pepino-Event` — repete o `event` do corpo. Útil para rotear a entrega **antes** de fazer parse do JSON.
* `X-Pepino-Webhook-Id` — identifica **qual webhook cadastrado** originou a entrega. É **constante** por webhook (o mesmo em todas as entregas), então **não** é uma chave de deduplicação; para deduplicar, use um identificador de negócio em `data` (como `waMessageId`). Ver [Idempotência](/docs/webhooks/delivery#idempotencia).
* `X-Pepino-Signature` — assinatura HMAC-SHA256 do corpo bruto, no formato `sha256=<hmac-hex>`. Permite verificar a autenticidade do que foi recebido. O cálculo está descrito em [Assinatura](/docs/webhooks/signing).

<Callout type="warn">
  Calcule e valide a assinatura sobre os **bytes crus** do corpo, antes de qualquer `JSON.parse`.
  Re-serializar o objeto depois do parse muda espaçamento e ordem de chaves, e a assinatura deixa de
  bater. Ver [Assinatura](/docs/webhooks/signing).
</Callout>

## `message.received` [#messagereceived]

Disparado quando a instância **recebe** uma mensagem com conteúdo.

```json
{
  "event": "message.received",
  "instanceId": "cm5x8a9k20001abc123def456",
  "timestamp": "2026-05-29T22:16:30Z",
  "data": {
    "from": "5511999999999@s.whatsapp.net",
    "type": "image",
    "text": "Mensagem de teste",
    "waMessageId": "3EB0C767D26A1D8E2B5F",
    "mediaUrl": "https://...&X-Amz-Expires=86400",
    "mimeType": "image/jpeg"
  }
}
```

Campos de `data`:

| Campo         | Tipo   | Sempre presente?       | Significado                                                                                                                                                                                                                                                       |
| ------------- | ------ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `from`        | string | Sim                    | Remetente da mensagem (ou o grupo de origem). Pode vir em mais de um formato — ver abaixo.                                                                                                                                                                        |
| `type`        | string | Sim                    | Tipo do conteúdo: `text`, `image`, `video`, `audio`, `document`, `sticker`, `location`, `contact`, `poll` ou `unknown`.                                                                                                                                           |
| `text`        | string | Sim (pode ser `""`)    | Conteúdo textual. Em `text`, é o corpo. Em `image`/`video`/`document`, é a **legenda** (vazio se não houver). Em `location`/`contact`/`poll`, é o **nome/título** do item (nome do local, nome do contato, pergunta da enquete). Em `audio`/`sticker`, vem vazio. |
| `waMessageId` | string | Sim                    | Identificador da mensagem no WhatsApp. Use-o para correlacionar com replies e para **deduplicar** mensagens repetidas.                                                                                                                                            |
| `mediaUrl`    | string | Só em mídia baixada    | URL pré-assinada para baixar o arquivo. Presente **apenas** quando o tipo é de mídia (`image`/`video`/`audio`/`document`/`sticker`) **e** o download foi concluído. Ver o Callout abaixo.                                                                         |
| `mimeType`    | string | Só junto de `mediaUrl` | Tipo MIME do arquivo (ex. `image/jpeg`, `audio/ogg`, `application/pdf`). Acompanha sempre o `mediaUrl`.                                                                                                                                                           |

<Callout type="warn">
  O objeto `data` traz **apenas** estes campos. Não há `pushName`, `senderPhone`, `conversationId`
  nem dados da mensagem citada no payload do webhook — não dependa de campos que não estão listados
  aqui.
</Callout>

### O campo `type` em detalhe [#o-campo-type-em-detalhe]

O `type` indica como interpretar o restante do payload:

| `type`     | O que é                                                                    | O que esperar em `text`     | `mediaUrl`?         |
| ---------- | -------------------------------------------------------------------------- | --------------------------- | ------------------- |
| `text`     | Mensagem de texto (inclui texto com link/citação)                          | O corpo da mensagem         | Não                 |
| `image`    | Imagem                                                                     | A legenda (se houver)       | Sim, quando baixada |
| `video`    | Vídeo                                                                      | A legenda (se houver)       | Sim, quando baixada |
| `audio`    | Áudio ou nota de voz                                                       | Vazio                       | Sim, quando baixada |
| `document` | Documento/arquivo                                                          | A legenda (se houver)       | Sim, quando baixada |
| `sticker`  | Figurinha                                                                  | Vazio                       | Sim, quando baixada |
| `location` | Localização (pin ou ao vivo)                                               | O nome do local (se houver) | Não                 |
| `contact`  | Contato compartilhado (vCard)                                              | O nome exibido do contato   | Não                 |
| `poll`     | Enquete                                                                    | A pergunta da enquete       | Não                 |
| `unknown`  | Conteúdo recebido que não se encaixa nos tipos acima, mas ainda traz texto | Pode ter texto              | Não                 |

<Callout type="info">
  Trate `type` como uma lista **aberta**: faça um `switch` nos valores que você conhece e tenha um
  caso `default` para o resto. Assim, qualquer tipo novo (ou `unknown`) é tratado de forma graciosa,
  sem quebrar o handler.
</Callout>

### O campo `from`: dois (e às vezes três) formatos [#o-campo-from-dois-e-às-vezes-três-formatos]

O `from` identifica a origem e **não é sempre um telefone**. Ele pode vir em três formatos, distinguíveis pelo sufixo após o `@`:

* **Número** com sufixo `@s.whatsapp.net` — por exemplo `5511999999999@s.whatsapp.net`. É o caso mais comum: o trecho antes do `@` é o telefone em E.164 (sem o `+`).
* **LID** com sufixo `@lid` — por exemplo `123456789@lid`. Ocorre quando o contato usa o recurso de privacidade do WhatsApp e o número real ainda não foi resolvido. Aqui o valor antes do `@lid` é um **identificador interno**, não um telefone.
* **Grupo** com sufixo `@g.us` — por exemplo `120363012345678901@g.us`. Aparece quando a mensagem foi recebida em um **grupo**. O valor antes do `@` identifica o grupo, não uma pessoa.

<Callout type="warn">
  Ao processar `from`, **não** assuma que o trecho antes do `@` é sempre um telefone. Use o
  **sufixo** para decidir: `@s.whatsapp.net` = número, `@lid` = identificador de privacidade,
  `@g.us` = grupo. Tentar discar/normalizar o trecho de um `@lid` ou `@g.us` como telefone leva a
  dados incorretos.
</Callout>

<Callout type="info">
  Mensagens recebidas em **grupos** geram `message.received` por padrão (com `from` terminando em
  `@g.us`). Se você só atende conversas 1:1, ligue a configuração `ignoreGroups` da instância — com
  ela ativa, mensagens de grupo não geram webhook. Ver [Configurações da
  instância](/docs/get-started/first-instance#configuracoes-da-instancia).
</Callout>

### Mídia: como baixar o arquivo [#mídia-como-baixar-o-arquivo]

Em mensagens de mídia, o binário não vem no payload — vem uma **URL pré-assinada** em `mediaUrl`, com validade de **24 horas**.

```json
"data": {
  "from": "5511999999999@s.whatsapp.net",
  "type": "audio",
  "text": "",
  "waMessageId": "3EB0C767D26A1D8E2B5F",
  "mediaUrl": "https://....s3.amazonaws.com/media/...&X-Amz-Expires=86400",
  "mimeType": "audio/ogg"
}
```

Baixe o arquivo diretamente pela URL, com um `GET` simples e **sem credenciais adicionais** (a autorização já está embutida na própria URL):

```bash
curl -L -o arquivo "$MEDIA_URL"
```

<Callout type="info">
  `mediaUrl` só é incluído quando o download da mídia foi **concluído** pela plataforma. Se o
  download não estava pronto no momento do evento, o payload vem **sem** `mediaUrl`/`mimeType`,
  apenas com os metadados (`from`, `type`, `text`, `waMessageId`). Trate a ausência desses campos
  com naturalidade — não é um erro. Como a URL expira em 24h, **baixe e armazene** o arquivo no seu
  lado se precisar dele por mais tempo; não guarde a URL para usar depois.
</Callout>

### O que **não** gera `message.received` [#o-que-não-gera-messagereceived]

Apenas mensagens com **conteúdo exibível** geram o evento. Não são entregues:

* **Ecos das próprias mensagens** que a instância envia (mensagens "de mim").
* **Mensagens de controle** do WhatsApp: cabeçalho de álbum (vem junto das fotos), reações, edições, exclusões, distribuição de chave de sessão e mensagens de protocolo.
* **Status** (`status@broadcast`), **listas de transmissão** e **canais** (newsletters) — não são conversas e não viram evento.
* **Grupos**, quando a configuração `ignoreGroups` da instância está ligada.

## `message.status` [#messagestatus]

Disparado quando muda o status de entrega de uma mensagem que **a instância enviou**. Como o envio é assíncrono (a API responde `202` apenas enfileirando a mensagem), este evento é a sua **confirmação de entrega e de leitura**.

```json
{
  "event": "message.status",
  "instanceId": "cm5x8a9k20001abc123def456",
  "timestamp": "2026-05-29T22:17:00Z",
  "data": {
    "status": "READ",
    "to": "5511999999999@s.whatsapp.net",
    "waMessageIds": ["3EB0C767D26A1D8E2B5F", "3EB1A2B3C4D5E6F70819"]
  }
}
```

Campos de `data`:

| Campo          | Tipo            | Significado                                                                                                                                  |
| -------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`       | string          | `DELIVERED` (entregue no aparelho do destinatário) ou `READ` (lida — ou, no caso de áudio, ouvida).                                          |
| `to`           | string          | Destinatário das mensagens, no mesmo formato de JID do `from` (geralmente `...@s.whatsapp.net`; pode ser `...@g.us` para grupos).            |
| `waMessageIds` | array de string | Lista dos `waMessageId` afetados por este status. **Um único receipt pode cobrir várias mensagens de uma vez** — por isso é sempre um array. |

Para correlacionar com o que você enviou, guarde o `waMessageId` retornado no envio e cruze-o com os `waMessageIds` recebidos aqui.

<Callout type="info">
  O fluxo normal é `DELIVERED` seguido de `READ`, mas **nem toda mensagem recebe os dois** (e nem
  necessariamente nessa ordem): se o destinatário nunca abre a conversa, você verá só `DELIVERED`;
  com retries, o mesmo `status` pode chegar repetido. Trate cada `message.status` como uma
  atualização **idempotente** do estado da mensagem, e não como um passo obrigatório de uma
  sequência. Não há um status de "falhou" neste evento.
</Callout>

## `instance.status` [#instancestatus]

Disparado quando muda o status de **conexão** da instância com o WhatsApp.

```json
{
  "event": "instance.status",
  "instanceId": "cm5x8a9k20001abc123def456",
  "timestamp": "2026-05-29T22:18:00Z",
  "data": {
    "status": "DISCONNECTED",
    "jid": "",
    "reason": ""
  }
}
```

Campos de `data`:

| Campo    | Tipo   | Significado                                                                                                                                                                                  |
| -------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status` | string | A transição de conexão. Assume um dos valores da tabela abaixo.                                                                                                                              |
| `jid`    | string | Identificador WhatsApp do número conectado (ex. `5511999999999@s.whatsapp.net`). Preenchido quando há um número associado à transição (notadamente em `CONNECTED`); vem `""` caso contrário. |
| `reason` | string | Informação adicional sobre a transição, quando houver (por exemplo, o motivo de um `LOGGED_OUT` ou o código de um `BANNED`). Vem `""` quando não há detalhe.                                 |

Valores possíveis de `status`:

| `status`          | Significado                                                                                     | O que fazer                                                                                                                   |
| ----------------- | ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `CONNECTED`       | A instância conectou e está operacional. O campo `jid` traz o identificador WhatsApp do número. | Pode enviar mensagens.                                                                                                        |
| `DISCONNECTED`    | A conexão caiu.                                                                                 | Nada — a instância **reconecta automaticamente**.                                                                             |
| `LOGGED_OUT`      | A sessão foi encerrada no aparelho (o usuário removeu o aparelho conectado).                    | É preciso **parear de novo via QR Code**.                                                                                     |
| `STREAM_REPLACED` | O número foi aberto em outra sessão; esta conexão foi substituída.                              | Verifique se o número não está conectado em outro lugar.                                                                      |
| `BANNED`          | O número foi bloqueado pela plataforma do WhatsApp (temporário ou permanente).                  | Estado terminal — não reconecta; o envio passa a responder `409`. O `reason` traz o código do bloqueio. Ver o detalhe abaixo. |

<Callout type="info">
  Este evento cobre apenas as **transições** acima. O ciclo de **pareamento** — os estados
  `CONNECTING` e `QR_PENDING`, e o sucesso de leitura do QR — **não** é enviado por
  `instance.status`. Acompanhe o pareamento pelo [stream de
  QR](/docs/api/instancias/streamInstanceQR) (ou pelo polling) e consulte o estado atual a qualquer
  momento com [`GET /v1/instances/{id}`](/docs/api/instancias/getInstance).
</Callout>

<Callout type="warn">
  Uma vez pareada, a instância **reconecta sozinha** após quedas, reinícios e deploys, sem novo QR.
  Por isso, um `DISCONNECTED` é normalmente transitório: não acione um novo pareamento por causa
  dele. Só `LOGGED_OUT` exige escanear o QR de novo. &#x2A;*Você pode confiar no `status`:** se a sessão
  morre — por exemplo, o número é **desvinculado no aparelho** enquanto a instância está fora do ar e
  o `LOGGED_OUT` não chega na hora — a plataforma **reconcilia** o estado e emite um `instance.status`
  com `DISCONNECTED`, em geral em um a dois minutos. Ou seja, o `status` **não fica preso
  indefinidamente** em um `CONNECTED` fantasma. Se, mesmo assim, uma instância permanecer em
  `DISCONNECTED` sem voltar para `CONNECTED` por alguns minutos (confirme com [`GET
    /v1/instances/{id}`](/docs/api/instancias/getInstance)), trate como sessão encerrada e gere um novo
  QR Code. Para o ciclo de vida completo, veja [Sua primeira
  instância](/docs/get-started/first-instance#ciclo-de-vida).
</Callout>

<Callout type="info">
  **Sobre o `BANNED`.** Ele cobre dois casos que o WhatsApp trata como bloqueio: um **bloqueio
  temporário** (anti-spam — por exemplo, mensagens demais para quem não tem você na agenda, ou gente
  demais te bloqueando) e um **banimento permanente** da conta. Nos dois o `status` vai para
  `BANNED`, o envio passa a responder `409` e a instância **não reconecta**; o `reason` traz o código
  do bloqueio quando o WhatsApp o informa. &#x2A;*Há ainda um caso sem evento:** um número "sombreado"
  (soft-block) pode, em vez de emitir `BANNED`, simplesmente **nunca concluir o pareamento** — o QR
  fica sendo regerado sem chegar a `CONNECTED`. O protocolo multi-device do WhatsApp não dá um sinal
  determinístico de banimento aí; por isso, trate um número que **não pareia depois de várias
  tentativas** como provavelmente bloqueado, em vez de insistir no mesmo número.
</Callout>

## `call.received` [#callreceived]

Disparado quando a instância **recebe uma chamada** (voz ou vídeo) de um contato — no mesmo instante em que o telefone "tocaria". Útil para registrar tentativas de ligação, alertar um atendente, ou casar com a configuração de **rejeitar chamadas**: mesmo com a rejeição ligada, o `call.received` é emitido — a chamada chegou, e só depois foi rejeitada.

```json
{
  "event": "call.received",
  "instanceId": "inst_123",
  "timestamp": "2026-06-06T12:00:00Z",
  "data": {
    "from": "5511999999999@s.whatsapp.net",
    "callId": "AB12CD34EF56"
  }
}
```

| Campo    | Tipo   | Descrição                                                                                                      |
| -------- | ------ | -------------------------------------------------------------------------------------------------------------- |
| `from`   | string | Quem ligou, no mesmo formato JID do `from` de `message.received` (a parte antes do `@` é o telefone em E.164). |
| `callId` | string | Identificador da chamada no WhatsApp. Útil para correlacionar os eventos da mesma ligação e para deduplicar.   |

<Callout type="info">
  Este evento sinaliza apenas que **chegou** uma chamada — não há eventos de "atendida", "encerrada"
  ou duração. Se você quer que o número **nunca** receba chamadas, ligue `rejectCalls` (e
  opcionalmente `rejectCallMessage`) nas [configurações da
  instância](/docs/get-started/first-instance#configuracoes-da-instancia).
</Callout>

## `message.deleted` [#messagedeleted]

Disparado quando um contato &#x2A;*apaga para todos (revoga)** uma mensagem em uma conversa com a instância — o "Apagar mensagem → Apagar para todos" do WhatsApp. Serve para manter o seu CRM/registro **fiel**: ao receber este evento, marque como apagada (ou esconda) a mensagem correspondente no seu sistema.

```json
{
  "event": "message.deleted",
  "instanceId": "inst_123",
  "timestamp": "2026-06-06T12:00:00Z",
  "data": {
    "from": "5511999999999@s.whatsapp.net",
    "waMessageId": "3EB0C767D26A1D8E2B5F"
  }
}
```

| Campo         | Tipo   | Descrição                                                                                                                                            |
| ------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `from`        | string | A conversa onde a mensagem foi apagada (JID do contato ou do grupo), no mesmo formato do `from` de `message.received`.                               |
| `waMessageId` | string | O `waMessageId` da **mensagem apagada** — cruze com o `waMessageId` recebido em `message.received` para localizar qual mensagem revogar no seu lado. |

<Callout type="info">
  Refere-se a apagar **para todos** (revogação), que é o que se propaga pelo WhatsApp — "Apagar para
  mim" é local no aparelho de quem apagou e **não** gera evento. Respeita o `ignoreGroups`: se você
  ignora grupos, uma revogação em grupo também não dispara.
</Callout>

## `group.participants` [#groupparticipants]

Disparado quando a composição de um grupo muda: alguém **entrou**, **saiu** (ou foi removido), foi **promovido** a admin ou **rebaixado**. Só vale para grupos dos quais a instância participa.

```json
{
  "event": "group.participants",
  "instanceId": "inst_123",
  "timestamp": "2026-06-06T12:00:00Z",
  "data": {
    "groupJid": "120363012345678901@g.us",
    "by": "5511999999999@s.whatsapp.net",
    "joined": ["5511888888888@s.whatsapp.net"],
    "left": [],
    "promoted": [],
    "demoted": []
  }
}
```

| Campo                                      | Tipo            | Descrição                                                                                 |
| ------------------------------------------ | --------------- | ----------------------------------------------------------------------------------------- |
| `groupJid`                                 | string          | O grupo (`...@g.us`).                                                                     |
| `by`                                       | string          | Quem fez a mudança (`""` se o WhatsApp não informou).                                     |
| `joined` / `left` / `promoted` / `demoted` | array de string | Os participantes afetados (JIDs). Em geral só um desses arrays vem preenchido por evento. |

## `group.updated` [#groupupdated]

Disparado quando os **metadados** de um grupo mudam — o **nome** ou o **tópico** (descrição).

```json
{
  "event": "group.updated",
  "instanceId": "inst_123",
  "timestamp": "2026-06-06T12:00:00Z",
  "data": {
    "groupJid": "120363012345678901@g.us",
    "by": "5511999999999@s.whatsapp.net",
    "name": "Novo nome do grupo"
  }
}
```

| Campo      | Tipo   | Descrição                                                   |
| ---------- | ------ | ----------------------------------------------------------- |
| `groupJid` | string | O grupo.                                                    |
| `by`       | string | Quem fez a mudança.                                         |
| `name`     | string | Presente **só** se o nome mudou — o novo nome.              |
| `topic`    | string | Presente **só** se o tópico/descrição mudou — o novo texto. |

## `contact.updated` [#contactupdated]

Disparado quando o **nome de exibição** (push name) de um contato muda — percebido ao receber uma mensagem dele com um nome diferente do que estava em cache.

```json
{
  "event": "contact.updated",
  "instanceId": "inst_123",
  "timestamp": "2026-06-06T12:00:00Z",
  "data": {
    "from": "5511999999999@s.whatsapp.net",
    "oldName": "João",
    "newName": "João Silva"
  }
}
```

| Campo     | Tipo   | Descrição                   |
| --------- | ------ | --------------------------- |
| `from`    | string | O contato.                  |
| `oldName` | string | O nome anterior (em cache). |
| `newName` | string | O novo nome de exibição.    |

<Callout type="info">
  Refere-se ao **push name** que o próprio contato definiu — não à foto de perfil, e não ao nome que
  **você** salvou na sua agenda.
</Callout>

## `label.updated` [#labelupdated]

Disparado quando uma **etiqueta** (recurso do WhatsApp **Business**) é criada, renomeada ou apagada.

```json
{
  "event": "label.updated",
  "instanceId": "inst_123",
  "timestamp": "2026-06-06T12:00:00Z",
  "data": {
    "labelId": "5",
    "name": "Cliente VIP",
    "color": 3,
    "deleted": false
  }
}
```

| Campo     | Tipo    | Descrição                              |
| --------- | ------- | -------------------------------------- |
| `labelId` | string  | Identificador da etiqueta.             |
| `name`    | string  | Nome atual da etiqueta.                |
| `color`   | number  | Índice de cor da etiqueta no WhatsApp. |
| `deleted` | boolean | `true` se a etiqueta foi apagada.      |

## `label.association` [#labelassociation]

Disparado quando uma **etiqueta** é **aplicada** ou **removida** de uma conversa.

```json
{
  "event": "label.association",
  "instanceId": "inst_123",
  "timestamp": "2026-06-06T12:00:00Z",
  "data": {
    "labelId": "5",
    "chat": "5511999999999@s.whatsapp.net",
    "labeled": true
  }
}
```

| Campo     | Tipo    | Descrição                              |
| --------- | ------- | -------------------------------------- |
| `labelId` | string  | A etiqueta.                            |
| `chat`    | string  | A conversa (contato ou grupo).         |
| `labeled` | boolean | `true` = aplicada; `false` = removida. |

<Callout type="info">
  Etiquetas (`label.*`) são um recurso do **WhatsApp Business** — só fazem sentido se o número
  conectado é uma conta Business que usa etiquetas. Os eventos do **sync inicial** são ignorados;
  você recebe só as mudanças em tempo real.
</Callout>

## `campaign.status` [#campaignstatus]

Disparado a cada mudança de status de um **destinatário de uma campanha** de disparo em massa. É o acompanhamento em tempo real do disparo, por destinatário — complementar ao agregado de `GET /v1/campaigns/{id}`.

```json
{
  "event": "campaign.status",
  "instanceId": "cm5x8a9k20001abc123def456",
  "timestamp": "2026-06-18T14:32:00Z",
  "data": {
    "campaignId": "camp_3Hk9Zq2pVxL",
    "recipient": "5511999999999",
    "name": "Maria",
    "status": "delivered",
    "error": null
  }
}
```

Campos de `data`:

| Campo        | Tipo           | Significado                                                              |
| ------------ | -------------- | ------------------------------------------------------------------------ |
| `campaignId` | string         | A campanha a que o destinatário pertence.                                |
| `recipient`  | string         | O número (apenas dígitos) do destinatário.                               |
| `name`       | string         | Nome do destinatário (variável `nome`), se informado. Pode vir ausente.  |
| `status`     | string         | Estado: `sent`, `delivered`, `read`, `responded`, `failed` ou `invalid`. |
| `error`      | string \| null | Motivo, quando `failed` ou `invalid` (ex.: número não está no WhatsApp). |

O ciclo típico de um destinatário é `sent` -> `delivered` -> `read`, e `responded` se o contato responder. `invalid` sai antes do envio (número sem WhatsApp, quando `validateNumbers` está ligado) e **não consome** o teto diário; `failed` é uma falha real de envio. Trate cada evento como uma atualização **idempotente** do estado daquele destinatário — nem todos os estados ocorrem para todos, e a ordem pode variar.

## Erros e situações de borda [#erros-e-situações-de-borda]

Estes eventos são **entregues ao seu endpoint**; eles não retornam códigos de erro para você. O que você precisa observar do lado do receptor:

* **Responda `2xx` rápido.** Qualquer resposta fora de `2xx`, timeout (15s) ou erro de rede faz a entrega **falhar e ser reenviada** com backoff, até 6 tentativas. Ver [Entrega e retry](/docs/webhooks/delivery).
* **Deduplique.** Por causa do retry, o **mesmo evento pode chegar mais de uma vez**. Use um identificador de negócio em `data` — `waMessageId` em `message.received`, `waMessageIds` em `message.status` — como chave de idempotência. O `X-Pepino-Webhook-Id` é constante por webhook e **não** serve para isso.
* **Valide a assinatura.** Trate como inválida (e descarte) qualquer entrega cujo `X-Pepino-Signature` não bata com o HMAC do corpo bruto. Ver [Assinatura](/docs/webhooks/signing).
* **Tolere campos ausentes e tipos novos.** Em `message.received`, `mediaUrl`/`mimeType` podem não vir; `text` pode ser `""`. Em `instance.status`, `jid`/`reason` podem ser `""`. Trate `type` e `status` como listas abertas, com um caso `default`.

Os erros de **cadastro** de webhook (criar/listar/remover via `POST /v1/webhooks`) — como `400` para evento inválido ou URL recusada, `401` para key ausente/inválida — estão documentados em [Webhooks](/docs/webhooks).

## Próximos passos [#próximos-passos]

* [Webhooks](/docs/webhooks) — como cadastrar webhooks via API e gerenciar o secret.
* [Assinatura](/docs/webhooks/signing) — como validar `X-Pepino-Signature` (com exemplos em Node, Python e Go).
* [Entrega e retry](/docs/webhooks/delivery) — timeout, critério de sucesso e política de reenvio.
* [WebSocket](/docs/webhooks/websocket) — receber os mesmos eventos em tempo real, sem endpoint público.
