# Webhooks (/docs/cartao/webhooks)



<QuickLinks>
  <QuickLink href="/docs/cartao/endpoints/charges/post_charges" title="Criar Cobrança" method="POST" path="/charges" />

  <QuickLink href="/docs/cartao/endpoints/charges/get_charges__chargeId_" title="Consultar Cobrança" method="GET" path="/charges/{chargeId}" />

  <QuickLink href="/docs/cartao/recurrence" title="Pagamentos Recorrentes" />

  <QuickLink href="/docs/cartao/transaction-status" title="Status de transação" />
</QuickLinks>

Em vez do seu sistema ficar perguntando "já pagou?", a PayZu **chama você** quando algo acontece: mudança de status da cobrança, atualização do antifraude, chargeback ou um novo ciclo de recorrência.

## Como configurar [#como-configurar]

Informe a `postbackUrl` na criação da cobrança ([`POST /charges`](/docs/cartao/endpoints/charges/post_charges)). Sempre que houver um evento, a PayZu envia uma requisição `POST` em JSON para essa URL.

## Eventos [#eventos]

| Tipos de evento    | Descrição                                                    |
| ------------------ | ------------------------------------------------------------ |
| `charge.update`    | Mudança no status de pagamento                               |
| `antifraud.update` | Mudança de status do Antifraude                              |
| `chargeback`       | Notificação de chargeback                                    |
| `recurrence.cycle` | Novo ciclo de [recorrência](/docs/cartao/recurrence) cobrado |

## Estrutura do payload [#estrutura-do-payload]

| Parâmetros | Descrição                     | Tipo                                                                                                  |
| ---------- | ----------------------------- | ----------------------------------------------------------------------------------------------------- |
| `event`    | Evento que chamou o webhook   | Ver a tabela de [eventos](#eventos)                                                                   |
| `data`     | Dados atualizados da cobrança | Mesmo valor retornado por [Consultar Cobrança](/docs/cartao/endpoints/charges/get_charges__chargeId_) |

```json
{
  "event": "charge.update",
  "data": {}
}
```

O objeto `data` tem exatamente o mesmo formato da resposta de [Consultar Cobrança](/docs/cartao/endpoints/charges/get_charges__chargeId_).

## Cabeçalhos da requisição [#cabeçalhos-da-requisição]

Cada `POST` chega com os seguintes cabeçalhos:

| Cabeçalho             | Descrição                                                          |
| --------------------- | ------------------------------------------------------------------ |
| `Content-Type`        | Sempre `application/json`                                          |
| `X-Webhook-Signature` | Assinatura HMAC SHA-256 do payload, em hexadecimal (64 caracteres) |
| `X-Webhook-Timestamp` | Instante do envio, em milissegundos desde a época Unix             |
| `X-Webhook-Nonce`     | Identificador único da requisição (32 caracteres hexadecimais)     |

Nomes de cabeçalho HTTP não diferenciam maiúsculas de minúsculas: dependendo do framework, eles chegam normalizados como `x-webhook-signature`, `x-webhook-timestamp` e `x-webhook-nonce`.

## Retentativas [#retentativas]

O primeiro envio acontece assim que o evento ocorre. A entrega só é considerada bem-sucedida se a sua URL responder com um status HTTP `2xx` em até **5 segundos**: qualquer outro status, ou uma resposta mais lenta que isso, conta como falha.

Depois de uma falha, o webhook faz até **5 retentativas**. A cada falha, o tempo até a próxima tentativa aumenta: as retentativas são feitas, respectivamente, depois de 1 minuto, 10 minutos, 1 hora, 6 horas e 24 horas. Depois disso, as tentativas param.

<Callout type="info">
  Responda o webhook rapidamente (um `200` simples basta) e processe o payload de forma assíncrona, para não estourar o limite de 5 segundos. Como um timeout pode gerar reenvio de um evento que você já processou, o consumo precisa ser idempotente: use o `id` da cobrança combinado com a transição de status como chave de deduplicação. Não use o `X-Webhook-Nonce` para isso, ele identifica a requisição HTTP e muda a cada reenvio.
</Callout>

<Mermaid
  chart="`
flowchart TD
  A[&#x22;Evento na cobrança&#x22;]
  A --> B[&#x22;PayZu envia POST postbackUrl&#x22;]
  B --> C{&#x22;Resposta 2xx em até 5s?&#x22;}
  C -->|Sim| D[&#x22;Entrega confirmada&#x22;]
  C -->|Não| E[&#x22;Espera: 1min, 10min, 1h, 6h, 24h&#x22;]
  E --> F{&#x22;Menos de 5 retentativas?&#x22;}
  F -->|Sim| B
  F -->|Não| G[&#x22;Para de tentar&#x22;]
`"
/>

## Verificação HMAC [#verificação-hmac]

Cada webhook é assinado com o seu **webhook secret**, fornecido pela PayZu junto com as suas [credenciais de API](/docs/cartao/authentication). Sua API deve validar a assinatura antes de processar o payload:

<Steps>
  <Step>
    Extraia os cabeçalhos `x-webhook-timestamp`, `x-webhook-nonce` e `x-webhook-signature`.
  </Step>

  <Step>
    Concatene os valores do timestamp, do nonce e do payload, separados por `.`, formando a string base de verificação: `timestamp.nonce.payload`.
  </Step>

  <Step>
    Gere uma assinatura HMAC com o algoritmo SHA-256 a partir dessa string, usando o seu webhook secret.
  </Step>

  <Step>
    Compare a assinatura gerada com o valor do cabeçalho `x-webhook-signature`. Se não coincidirem, rejeite o webhook.
  </Step>
</Steps>

Exemplo em Node.js, usando `crypto.timingSafeEqual` para comparar as assinaturas em tempo constante:

```js
const crypto = require("node:crypto");

function verifyWebhookSignature(request, webhookSecret) {
  const timestamp = request.headers["x-webhook-timestamp"];
  const nonce = request.headers["x-webhook-nonce"];
  const signature = request.headers["x-webhook-signature"];

  if (typeof signature !== "string" || !/^[0-9a-f]{64}$/i.test(signature)) {
    return false;
  }

  const baseString = `${timestamp}.${nonce}.${request.rawBody}`;
  const expectedSignature = crypto
    .createHmac("sha256", webhookSecret)
    .update(baseString)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, "hex"),
    Buffer.from(signature, "hex"),
  );
}
```

<Callout type="info">
  Calcule o HMAC sobre o corpo bruto da requisição (raw body), exatamente como recebido, antes de qualquer parse de JSON.
</Callout>

## Verificação do nonce (opcional) [#verificação-do-nonce-opcional]

O valor do cabeçalho `x-webhook-nonce` atua como identificador único e temporário de cada requisição. Após extraí-lo, verifique se esse nonce já foi registrado antes:

* Se o valor já tiver sido utilizado, rejeite a requisição para mitigar ataques de repetição (replay attacks).
* Se o nonce for novo, armazene-o como utilizado, garantindo que não possa ser reaproveitado em chamadas futuras.

## Verificação do timestamp (opcional) [#verificação-do-timestamp-opcional]

O valor do cabeçalho `x-webhook-timestamp` é o instante do envio em **milissegundos** desde a época Unix. Compare-o com o horário atual: se a diferença for superior a **5 minutos**, rejeite a requisição. Essa validação descarta webhooks expirados, evitando o processamento de mensagens antigas ou potencialmente maliciosas.
