> ## Documentation Index
> Fetch the complete documentation index at: https://docs.z2pay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks: recebendo eventos

> Como a Z2Pay entrega eventos no seu servidor, qual é o formato do payload e como verificar a assinatura HMAC-SHA256.

Webhooks são notificações HTTP que a Z2Pay envia para o seu servidor sempre que algo
acontece na sua conta (uma transação é paga, um reembolso é concluído, uma assinatura
é cancelada, etc.). Em vez de você ficar consultando a API repetidamente, a Z2Pay faz
um `POST` para uma URL que você cadastra.

<Info>
  Esta página explica **como receber e validar** os eventos. Para **cadastrar, listar
  e remover** endpoints de webhook, veja [Referência: Webhooks](/pt-BR/webhooks).
  Para a lista de eventos disponíveis, veja [Eventos](/pt-BR/webhooks/eventos).
</Info>

## Como funciona

<Steps>
  <Step title="Você cadastra um endpoint">
    Registre uma URL pública (`http` ou `https` — recomendamos fortemente HTTPS) e,
    opcionalmente, um `secret`. Você também escolhe quais eventos quer receber — uma lista
    vazia significa "todos os eventos". Veja [Referência: Webhooks](/pt-BR/webhooks).
  </Step>

  <Step title="Um evento acontece">
    Algo muda na sua conta (ex.: uma transação é paga). A Z2Pay seleciona seus webhooks
    ativos que escutam aquele tipo de evento.
  </Step>

  <Step title="A Z2Pay faz POST no seu endpoint">
    Para cada webhook correspondente, a Z2Pay envia um `POST` com o envelope JSON do
    evento no corpo da requisição e cabeçalhos de identificação/assinatura.
  </Step>

  <Step title="Seu servidor responde 2xx">
    Responda com um status `2xx` em até **30 segundos**. Qualquer outra coisa (resposta fora
    da faixa `2xx`, timeout, falha de conexão) conta como falha — e o Z2Pay **reenvia
    automaticamente** com intervalos crescentes (veja
    [Entrega e retentativas](/pt-BR/webhooks/entregas)).
  </Step>
</Steps>

## O envelope do evento

Todo webhook chega no mesmo formato de envelope. O conteúdo específico do recurso fica
sempre dentro de `data`.

```json theme={null}
{
  "id": "whd_bnl41fsjd33ndrgvx08s59a4i",
  "type": "transaction.paid",
  "data": {
    "id": "txn_bnht3sucidgqm04w8clzfmh7h",
    "status": "paid",
    "amount": 15000,
    "paymentMethod": "credit_card"
  },
  "occurredAt": "2026-06-24T18:32:05.184Z",
  "companyId": "comp_a67vga3rx8fbg4eg4gay8m3a5"
}
```

<ResponseField name="id" type="string">
  Identificador único desta entrega (prefixo `whd_`). Cada tentativa de entregar o mesmo
  evento ao mesmo endpoint usa o mesmo `id` — use-o como chave de idempotência (veja abaixo).
</ResponseField>

<ResponseField name="type" type="string">
  O tipo do evento, ex.: `transaction.paid`. Lista completa em [Eventos](/pt-BR/webhooks/eventos).
</ResponseField>

<ResponseField name="data" type="object">
  O objeto do recurso relacionado ao evento (a transação, o reembolso, a assinatura, etc.).
  O formato de `data` depende do `type`.
</ResponseField>

<ResponseField name="occurredAt" type="string">
  Data e hora em que o evento **ocorreu**, em ISO 8601 com timezone (UTC). Não é o horário do
  envio: retentativas e reenvios da mesma entrega preservam o valor original. Use-o para
  **ordenar eventos** no seu lado — retentativas podem chegar fora de ordem (veja
  [Entrega e retentativas](/pt-BR/webhooks/entregas#ordem-de-entrega-n%C3%A3o-%C3%A9-garantida)).
</ResponseField>

<ResponseField name="companyId" type="string">
  Identificador da sua company (prefixo `comp_`).
</ResponseField>

<Warning>
  Valores monetários em `data` são sempre **inteiros em centavos**. `15000` significa
  R\$ 150,00. Não assuma casas decimais. Veja [Convenções](/pt-BR/convencoes).
</Warning>

## Cabeçalhos da requisição

Todo `POST` de webhook inclui:

| Cabeçalho             | Sempre presente     | Descrição                                                 |
| --------------------- | ------------------- | --------------------------------------------------------- |
| `Content-Type`        | Sim                 | `application/json`                                        |
| `User-Agent`          | Sim                 | `Z2Pay-Webhooks/1.0`                                      |
| `X-Webhook-Event-Id`  | Sim                 | Identificador do evento de origem. Usado para correlação. |
| `X-Webhook-Timestamp` | Apenas com `secret` | Momento da assinatura, em ISO 8601 com timezone.          |
| `X-Webhook-Signature` | Apenas com `secret` | Assinatura HMAC-SHA256 do corpo, em hexadecimal.          |

<Note>
  Os cabeçalhos `X-Webhook-Timestamp` e `X-Webhook-Signature` **só são enviados quando você
  cadastrou um `secret`** no webhook. Sem `secret`, não há assinatura para validar — por isso
  recomendamos sempre configurar um `secret`.
</Note>

<Note>
  O `X-Webhook-Timestamp` é **informativo** e **não entra no cálculo da assinatura** — a
  assinatura cobre apenas o corpo da requisição. Não há proteção anti-replay baseada em tempo:
  para se proteger contra o reenvio de uma requisição capturada, implemente deduplicação pelo
  `id` do envelope (prefixo `whd_`), como descrito em
  [Idempotência no seu receptor](#idempot%C3%AAncia-no-seu-receptor).
</Note>

## Verificando a assinatura

Quando o webhook tem `secret`, a Z2Pay calcula a assinatura assim:

```
X-Webhook-Signature = hex( HMAC-SHA256(secret, rawBody) )
```

Onde `rawBody` é o **corpo bruto** da requisição — os **bytes exatos** do envelope JSON (com `id`,
`type`, `data`, `occurredAt` e `companyId`) que a Z2Pay enviou. **Não** faça parse e re-serialize
(`JSON.stringify`): assine os bytes recebidos, exatamente como chegaram (veja o aviso abaixo).

<Warning>
  Compute o HMAC sobre o **corpo bruto (raw body)** da requisição, exatamente como recebido.
  Se você fizer parse para objeto e depois re-serializar (`JSON.stringify`), a ordem das chaves
  ou o espaçamento pode mudar e a assinatura **não vai bater**. Capture o raw body antes de
  qualquer middleware que faça parse de JSON.
</Warning>

### Exemplo em Node.js

```js theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

/**
 * Valida a assinatura de um webhook da Z2Pay.
 *
 * @param {string} rawBody  Corpo bruto da requisição (string), NÃO re-serializado.
 * @param {string} signature Valor do cabeçalho 'x-webhook-signature'.
 * @param {string} secret   O secret cadastrado no webhook.
 * @returns {boolean}
 */
function verifyWebhookSignature(rawBody, signature, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');

  // Comprimentos diferentes -> assinatura inválida (timingSafeEqual exige tamanhos iguais)
  if (expected.length !== signature.length) return false;

  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
```

Exemplo de uso com Express (capturando o raw body):

```js theme={null}
import express from 'express';

const app = express();

// Captura o raw body como string ANTES de qualquer parse de JSON.
app.use('/webhooks/z2pay', express.raw({ type: 'application/json' }));

app.post('/webhooks/z2pay', (req, res) => {
  const rawBody = req.body.toString('utf8');
  const signature = req.header('x-webhook-signature');

  // Sem o cabeçalho de assinatura, rejeite antes de qualquer processamento.
  if (!signature) {
    return res.status(401).send('missing signature');
  }

  const ok = verifyWebhookSignature(rawBody, signature, process.env.Z2PAY_WEBHOOK_SECRET);
  if (!ok) {
    return res.status(401).send('invalid signature');
  }

  const event = JSON.parse(rawBody);

  // TODO: enfileirar/processar de forma assíncrona; responda rápido.
  console.log('Evento recebido:', event.type, event.id);

  return res.status(200).send('ok');
});
```

<Tip>
  Use sempre `crypto.timingSafeEqual` (comparação em tempo constante) para evitar ataques de
  timing. Nunca compare assinaturas com `===` direto.
</Tip>

## Idempotência no seu receptor

O mesmo evento pode chegar mais de uma vez (por exemplo, quando você reenvia manualmente uma
entrega que falhou, ou quando o mesmo fato gera um novo evento interno). O seu endpoint
**deve ser idempotente**.

<Steps>
  <Step title="Use o id da entrega como chave">
    Guarde o `id` do envelope (prefixo `whd_`) ao processar o evento. Ele é estável entre
    tentativas da mesma entrega.
  </Step>

  <Step title="Ignore o que já foi processado">
    Antes de aplicar efeitos colaterais, verifique se aquele `id` já foi processado. Se sim,
    responda `2xx` e não faça nada de novo.
  </Step>

  <Step title="Responda rápido">
    Faça o trabalho pesado de forma assíncrona (fila/worker). Responda `2xx` assim que validar
    a assinatura e registrar o evento.
  </Step>
</Steps>

<Note>
  O `X-Webhook-Event-Id` identifica o evento de origem e também pode ser usado para correlação
  e deduplicação. Já o `id` do envelope é específico da entrega ao seu endpoint.
</Note>

<Note>
  Do lado da Z2Pay existe uma deduplicação de despacho: o **mesmo evento não gera duas entregas
  para o mesmo webhook dentro de 1 hora**. Ainda assim, mantenha a idempotência no seu receptor —
  reenvios manuais entregam o mesmo envelope de novo (mesmo `id`), e um evento reemitido depois
  dessa janela chega como uma entrega nova.
</Note>

## O que a Z2Pay espera da sua resposta

<CardGroup cols={2}>
  <Card title="Tempo limite" icon="clock">
    Cada `POST` tem um tempo limite de **30 segundos**. Se o seu servidor não responder dentro
    desse prazo, a tentativa conta como falha e entra no ciclo de retentativas.
  </Card>

  <Card title="O que conta como sucesso" icon="circle-check">
    Apenas respostas HTTP na faixa **2xx** contam como entrega bem-sucedida. Redirecionamentos
    (`3xx`) são **seguidos**, e o resultado é decidido pelo status **final**; `4xx` e `5xx`
    contam como falha.
  </Card>
</CardGroup>

Falhando, a entrega não se perde: a Z2Pay reenvia sozinha ao longo de \~31h30, e só depois disso
desiste. A tabela de intervalos, a desativação automática de um endpoint fora do ar, o reenvio
manual e como reconciliar depois de um incidente estão em
[Entrega e retentativas](/pt-BR/webhooks/entregas).

<Tip>
  Monitore periodicamente as entregas que falharam com
  [`GET /webhooks/deliveries?status=failed`](/pt-BR/webhooks/deliveries) — filtre por `webhookId`
  se você tiver vários endpoints — e [reenvie-as](/pt-BR/webhooks/deliveries-retry) depois de
  corrigir o seu servidor.
</Tip>

## Boas práticas

<AccordionGroup>
  <Accordion title="Sempre configure um secret" icon="key">
    Sem `secret`, não há cabeçalhos de assinatura e você não consegue garantir que a requisição
    veio da Z2Pay. Configure um `secret` forte ao cadastrar o webhook em
    [Referência: Webhooks](/pt-BR/webhooks).
  </Accordion>

  <Accordion title="Valide a assinatura antes de processar" icon="shield-check">
    Rejeite com `401` qualquer requisição cuja assinatura não bata. Não processe o evento antes
    de validar.
  </Accordion>

  <Accordion title="Processe de forma assíncrona" icon="layers">
    Enfileire o evento e responda `2xx` rapidamente. Processamento inline que passa de 30s
    vira timeout — e o Z2Pay vai **retentar algo que você já processou**. Por isso a
    deduplicação pelo `id` (acima) não é opcional.
  </Accordion>

  <Accordion title="Trate o evento como dica, não como verdade absoluta" icon="rotate-cw">
    Em fluxos críticos (ex.: liberar um pedido), confirme o estado consultando a
    [Referência: Transações](/pt-BR/transactions) com o `id` recebido em `data`.
  </Accordion>
</AccordionGroup>

## Veja também

<CardGroup cols={2}>
  <Card title="Entrega e retentativas" icon="rotate-cw" href="/pt-BR/webhooks/entregas">
    Retentativas automáticas, desativação automática e reconciliação.
  </Card>

  <Card title="Eventos" icon="bell" href="/pt-BR/webhooks/eventos">
    A lista de tipos de evento (`type`) que a Z2Pay envia.
  </Card>

  <Card title="Referência: Webhooks" icon="webhook" href="/pt-BR/webhooks">
    Cadastrar, listar e remover endpoints de webhook.
  </Card>

  <Card title="Erros" icon="triangle-alert" href="/pt-BR/erros">
    Formato e códigos de erro da API.
  </Card>

  <Card title="Convenções" icon="ruler" href="/pt-BR/convencoes">
    IDs com prefixo, valores em centavos e datas em ISO 8601.
  </Card>
</CardGroup>
