# Webhooks (/docs/webhooks)





Um webhook é uma notificação que o PEPINO ZAP envia ao **seu** servidor quando algo acontece em uma instância — uma mensagem chega, o status de entrega de uma mensagem enviada muda, ou a conexão da instância muda de estado. Em vez de o seu sistema ficar consultando a API repetidamente para saber "chegou algo novo?", o seu endpoint recebe um `POST` assinado a cada evento, no momento em que ele acontece.

<Callout type="warn">
  **A URL do webhook é a *sua* — o PEPINO ZAP não gera nenhuma URL.** Aqui o modelo é o de
  Stripe/Twilio, não o de Slack/Discord. No Slack/Discord você cria um "incoming webhook" e *recebe*
  uma URL pronta para postar; **aqui é o inverso**: **você** expõe um endpoint público no **seu**
  servidor (ex.: `https://seusistema.com/webhook/pepinozap`) e cadastra a URL dele no campo `url`; o
  PEPINO ZAP faz `POST` nesse endereço a cada evento. **Não existe nenhuma URL gerada por nós para
  você consumir** — quem fornece a URL é você.
</Callout>

Esta página é a **visão geral**: o que é um webhook, como cadastrá-lo via API (self-service), o que cada chamada retorna, os erros possíveis e quando preferir webhook ou WebSocket. O detalhe de cada parte vive nas páginas de [Eventos](/docs/webhooks/events), [Assinatura](/docs/webhooks/signing) e [Entrega e retry](/docs/webhooks/delivery).

Todas as requisições de gerenciamento (cadastrar, listar, remover) exigem o header `Authorization: Bearer <API_KEY>`. Os exemplos usam a URL de produção `https://api.pepinozap.com.br` com uma key `pzk_live_`; no modo teste, use `https://sandbox.pepinozap.com.br` com uma key `pzk_test_`. O **host** decide o modo, e a key precisa casar com o host — `pzk_live_` em `api.*`, `pzk_test_` em `sandbox.*`; usar a key no host errado retorna `401`. Ver [Autenticação](/docs/get-started/authentication#modos-teste-e-producao).

## Como funciona, em três passos [#como-funciona-em-três-passos]

1. Uma instância gera um evento — `message.received` (mensagem recebida), `message.status` (entregue/lida) ou `instance.status` (conexão mudou).
2. A plataforma envia um `POST` assinado para a URL que você cadastrou, com o corpo do evento e os headers de identificação e assinatura.
3. O seu backend lê o corpo, valida a assinatura e responde com `2xx` para confirmar o recebimento. Se você não responder `2xx` (ou demorar mais que o timeout), a entrega é reenviada.

Cada entrega traz três headers, sempre:

| Header                | Conteúdo                                                                                                                                                                                                                                                                                                                               |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `X-Pepino-Event`      | Nome do evento da entrega (ex.: `message.received`).                                                                                                                                                                                                                                                                                   |
| `X-Pepino-Webhook-Id` | Identificador do **webhook cadastrado** (a configuração, ex.: `wh_abc123`). É **constante** em todas as entregas daquele mesmo webhook — não é um id por entrega nem um número de sequência. Para **deduplicar** (o mesmo evento pode chegar mais de uma vez), use um identificador de negócio presente em `data`, como `waMessageId`. |
| `X-Pepino-Signature`  | `sha256=<hmac-hex>` — o HMAC-SHA256 do corpo **bruto** da requisição, calculado com o secret do webhook.                                                                                                                                                                                                                               |

A assinatura é o que prova que a entrega veio do PEPINO ZAP (e não foi adulterada). Validá-la é **fortemente recomendado** antes de processar qualquer entrega — veja [Assinatura](/docs/webhooks/signing).

## O envelope de cada entrega [#o-envelope-de-cada-entrega]

Independentemente do evento, o corpo do `POST` tem sempre o mesmo formato de envelope. O que muda entre os eventos é só o conteúdo de `data`:

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

* `event` — o nome do evento (`message.received`, `message.status` ou `instance.status`).
* `instanceId` — a instância que originou o evento. Um webhook é escopado ao **Projeto**, então recebe eventos de **todas** as instâncias do Projeto; use este campo para saber de qual.
* `timestamp` — data e hora do evento em ISO 8601 (UTC).
* `data` — o objeto específico do evento. O formato de cada um está em [Eventos](/docs/webhooks/events).

## Cadastrando um webhook [#cadastrando-um-webhook]

Você cadastra seus webhooks **via API**, de forma **self-service**, escopados ao seu Projeto — não depende do operador. Faça um `POST /v1/webhooks` com três campos:

```bash
curl -X POST https://api.pepinozap.com.br/v1/webhooks \
  -H "Authorization: Bearer $YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "meu-endpoint",
    "url": "https://meusistema.com/webhook/pepinozap",
    "events": []
  }'
```

### O corpo da requisição [#o-corpo-da-requisição]

| Campo    | Tipo             | Obrigatório | O que é                                                                                                                                                                                     |
| -------- | ---------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`   | string           | Sim         | Um rótulo livre para você identificar o webhook (ex.: `producao-crm`). Espaços nas pontas são removidos; vazio é rejeitado com `400`. Nomes podem repetir — não há checagem de duplicidade. |
| `url`    | string           | Sim         | A URL pública do seu endpoint que vai receber os `POST`. Precisa ser `http`/`https` absoluta. Recomenda-se `https`.                                                                         |
| `events` | array de strings | Não         | Quais eventos este webhook deve receber. &#x2A;*Array vazio (`[]`) ou omitido = todos os eventos.** Para filtrar, liste só os que quer (ex.: `["message.received"]`).                       |

Os valores aceitos em `events` são estes:

| Valor                | Quando dispara                                                           |
| -------------------- | ------------------------------------------------------------------------ |
| `message.received`   | Uma mensagem chegou em uma instância.                                    |
| `message.status`     | Mudou o status de entrega de uma mensagem enviada (entregue/lida).       |
| `instance.status`    | A conexão de uma instância mudou (conectou, caiu, deslogou, foi banida). |
| `call.received`      | Uma chamada (voz/vídeo) foi recebida.                                    |
| `message.deleted`    | O remetente apagou/revogou uma mensagem.                                 |
| `group.participants` | Alguém entrou/saiu ou foi promovido/rebaixado em um grupo.               |
| `group.updated`      | Nome ou descrição de um grupo mudou.                                     |
| `contact.updated`    | O nome de exibição de um contato mudou.                                  |
| `label.updated`      | Uma etiqueta (Business) foi criada, editada ou apagada.                  |
| `label.association`  | Uma etiqueta foi aplicada ou removida de uma conversa.                   |

Qualquer string fora dessa lista faz a chamada falhar com `400` e a mensagem `evento inválido: <valor>`. O payload dos principais está em [Eventos](/docs/webhooks/events).

<Callout type="warn">
  A `url` é validada na criação: precisa ter esquema `http` ou `https` e ter um host. Se você
  apontar para um **endereço IP interno/privado** literal (loopback como `127.0.0.1`, faixas
  privadas como `10.x`/`192.168.x`, link-local etc.), a criação é rejeitada com `400` — é uma
  proteção anti-SSRF. Para testar localmente, exponha o endpoint com um túnel (por exemplo, um
  serviço de tunelamento HTTP) e cadastre a URL pública dele, não `localhost`.
</Callout>

### A resposta: `201 Created` com o secret [#a-resposta-201-created-com-o-secret]

A criação retorna `201 Created` com o webhook recém-criado **e** o `secret`:

```json
{
  "id": "wh_abc123",
  "name": "meu-endpoint",
  "url": "https://meusistema.com/webhook/pepinozap",
  "events": [],
  "enabled": true,
  "lastSuccessAt": null,
  "lastFailureAt": null,
  "lastFailureReason": null,
  "createdAt": "2026-05-30T12:00:00Z",
  "secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
```

* `id` — o identificador do webhook. Guarde-o: é o que você usa para **remover** o webhook depois (`DELETE /v1/webhooks/{id}`).
* `enabled` — webhooks nascem **habilitados** (`true`); já começam a receber entregas. Você pode **pausar** e **retomar** a entrega depois sem perder a config (ver [Pausar e retomar](#pausar-e-retomar)).
* `lastSuccessAt`, `lastFailureAt`, `lastFailureReason` — telemetria da última entrega bem-sucedida e da última falha. Vêm `null` enquanto não houve entrega; depois ajudam a diagnosticar um endpoint com problema (veja [Entrega e retry](/docs/webhooks/delivery)).
* `secret` — a chave para validar a assinatura das entregas. Detalhado a seguir.

### O secret `whsec_` é mostrado uma única vez [#o-secret-whsec_-é-mostrado-uma-única-vez]

O campo `secret`, com prefixo `whsec_`, **só aparece nesta resposta de criação**. Ele não volta a ser exibido em nenhuma outra chamada — `GET /v1/webhooks` **não** o retorna. É com esse valor que você recalcula o HMAC e confere o header `X-Pepino-Signature` de cada entrega (ver [Assinatura](/docs/webhooks/signing)).

<Callout type="warn">
  Copie o `secret` agora e guarde-o em variável de ambiente ou cofre de segredos (fora do
  código-fonte). Se você perder o secret, **não há como recuperá-lo**: a única saída é **remover o
  webhook e criar outro**, o que gera um secret novo — e, a partir daí, você passa a validar as
  assinaturas com o secret novo. Cada webhook tem o seu próprio secret.
</Callout>

### Variações de cadastro [#variações-de-cadastro]

**Recebendo só um tipo de evento.** Liste apenas o que interessa em `events`:

```bash
curl -X POST https://api.pepinozap.com.br/v1/webhooks \
  -H "Authorization: Bearer $YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "so-recebidas", "url": "https://meusistema.com/in", "events": ["message.received"] }'
```

**Vários endpoints.** Você pode cadastrar mais de um webhook no mesmo Projeto — por exemplo, um endpoint para mensagens e outro para status de conexão. Cada um tem URL, eventos e secret próprios, e cada um recebe sua própria entrega assinada.

## Listando seus webhooks [#listando-seus-webhooks]

Use `GET /v1/webhooks` para ver os webhooks cadastrados no Projeto:

```bash
curl https://api.pepinozap.com.br/v1/webhooks \
  -H "Authorization: Bearer $YOUR_API_KEY"
```

A resposta é `200 OK` com um array em `items`:

```json
{
  "items": [
    {
      "id": "wh_abc123",
      "name": "meu-endpoint",
      "url": "https://meusistema.com/webhook/pepinozap",
      "events": [],
      "enabled": true,
      "lastSuccessAt": "2026-06-05T11:59:00Z",
      "lastFailureAt": null,
      "lastFailureReason": null,
      "createdAt": "2026-05-30T12:00:00Z"
    }
  ]
}
```

<Callout type="info">
  Repare que a listagem &#x2A;*não inclui o `secret`** — esse campo só existe na resposta de criação. Use
  `lastSuccessAt` / `lastFailureAt` / `lastFailureReason` para auditar a saúde de cada endpoint: um
  `lastFailureReason` preenchido e um `lastSuccessAt` antigo indicam que o seu endpoint vem
  rejeitando ou não respondendo às entregas.
</Callout>

A listagem respeita o **modo da key**: uma key `pzk_test_` (host `sandbox.*`) lista os webhooks de teste; uma key `pzk_live_` (host `api.*`) lista os de produção. São conjuntos separados.

## Removendo um webhook [#removendo-um-webhook]

Para parar de receber entregas em um endpoint, remova o webhook pelo `id` retornado na criação (ou obtido na listagem):

```bash
curl -X DELETE https://api.pepinozap.com.br/v1/webhooks/wh_abc123 \
  -H "Authorization: Bearer $YOUR_API_KEY"
```

Em sucesso, retorna `204 No Content` (sem corpo). Se o `id` não existir, não for do seu Projeto, ou for de outro modo (teste vs. produção), retorna `404` com `webhook não encontrado neste projeto`.

A remoção é **definitiva** e invalida o secret daquele webhook. Para apenas **interromper temporariamente** as entregas (sem perder a config), use o **pause** abaixo em vez de remover. Já para trocar **URL ou eventos** não há edição: o fluxo é **remover e criar de novo** com os novos parâmetros (o que gera um secret novo) — atualize a configuração do seu endpoint antes de descartar o antigo.

## Pausar e retomar [#pausar-e-retomar]

Às vezes você precisa **parar de receber** as entregas por um tempo — manutenção do seu endpoint, um deploy, ou investigar um problema — sem **perder a configuração** (URL, secret e eventos). Para isso, alterne o campo `enabled` com `PATCH /v1/webhooks/{id}`:

```bash
# pausar a entrega
curl -X PATCH https://api.pepinozap.com.br/v1/webhooks/wh_abc123 \
  -H "Authorization: Bearer $YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": false }'

# retomar
curl -X PATCH https://api.pepinozap.com.br/v1/webhooks/wh_abc123 \
  -H "Authorization: Bearer $YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "enabled": true }'
```

Retorna `200 OK` com o webhook atualizado. Enquanto `enabled` for `false`, **nenhuma entrega é feita** para aquele endpoint (os eventos nem chegam a ser enfileirados para ele); ao voltar para `true`, as entregas recomeçam a partir dos **próximos** eventos — o que ocorreu durante a pausa **não** é entregue retroativamente.

<Callout type="info">
  **A vantagem sobre remover:** pausar mantém o &#x2A;*mesmo `id` e o mesmo `secret`** — você religa
  quando quiser, sem reconfigurar nada do seu lado nem trocar a chave de assinatura. Remover, ao
  contrário, é definitivo e invalida o secret. No painel (**Integrações → Webhooks**), esse mesmo
  controle é o botão **Pausar / Retomar**.
</Callout>

## Entrega e idempotência (resumo) [#entrega-e-idempotência-resumo]

A plataforma envia cada entrega via `POST` e espera uma resposta `2xx` rápida. O comportamento completo está em [Entrega e retry](/docs/webhooks/delivery); o essencial:

* **Timeout de 15 s por tentativa.** Responda `2xx` em poucos segundos e processe o evento de forma assíncrona.
* **Sucesso = `2xx`.** Qualquer `4xx`/`5xx`, timeout ou erro de rede conta como falha.
* **Retry com backoff exponencial, até 6 tentativas.** Por isso, o **mesmo evento pode chegar mais de uma vez**: trate as entregas de forma **idempotente**, deduplicando por um identificador de negócio presente no `data` (por exemplo, `waMessageId`). O header `X-Pepino-Webhook-Id` identifica o webhook cadastrado (é constante por webhook), então não serve sozinho para deduplicar eventos.

## Erros possíveis no cadastro [#erros-possíveis-no-cadastro]

O envelope de erro é `{ "error": { "code", "message" } }`.

| Código | Significado                                                                                                                                                                                                                                                                       |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | Corpo inválido. Casos: JSON malformado (`JSON inválido: ...`); `name` ausente (`nome obrigatório`); `url` inválida — esquema diferente de http/https, sem host, ou apontando para IP interno/privado (`URL: ...`); evento não suportado em `events` (`evento inválido: <valor>`). |
| `401`  | API key ausente, inválida, ou usada no host do modo errado (`pzk_live_` em `sandbox.*` ou vice-versa). Ver [Autenticação](/docs/get-started/authentication#erros-esperados).                                                                                                      |
| `403`  | Projeto inativo ou sem permissão.                                                                                                                                                                                                                                                 |
| `404`  | (No `DELETE`) webhook não encontrado neste Projeto — id inexistente, de outro Projeto, ou de outro modo.                                                                                                                                                                          |
| `500`  | Erro interno ao gerar o secret ou ao persistir o webhook.                                                                                                                                                                                                                         |

## Webhook ou WebSocket? [#webhook-ou-websocket]

Os dois transportes entregam **exatamente os mesmos eventos**, com o **mesmo envelope**. A escolha é sobre a garantia de entrega que você precisa:

|                  | Webhook                                               | WebSocket                                               |
| ---------------- | ----------------------------------------------------- | ------------------------------------------------------- |
| Endpoint público | Necessário                                            | Não precisa                                             |
| Entrega          | Com retry e backoff, até 6 tentativas                 | Só enquanto o cliente está conectado (sem buffer/retry) |
| Indicado para    | Integrações de backend que exigem garantia de entrega | Frontends, dashboards, protótipos                       |

Se você não tem um endpoint público — por exemplo, num frontend ou protótipo — pode receber os mesmos eventos por uma conexão [WebSocket](/docs/webhooks/websocket), sem expor uma URL. O webhook continua sendo a opção recomendada quando você precisa de **entrega garantida**. Os dois podem ser usados ao mesmo tempo: o WebSocket não substitui o webhook quando você precisa de durabilidade.

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

* [Eventos](/docs/webhooks/events) — os três eventos disponíveis e o payload de cada um.
* [Assinatura](/docs/webhooks/signing) — como validar `X-Pepino-Signature` com o secret `whsec_`, com exemplos em Node.js, Python e Go.
* [Entrega e retry](/docs/webhooks/delivery) — a política de timeout, sucesso/falha e reenvio em detalhe.
* [WebSocket](/docs/webhooks/websocket) — receber os eventos em tempo real sem endpoint público.
