# Webhooks (/docs/pix-processamento/webhooks)

<QuickLinks>
  <QuickLink href="/docs/pix-processamento/endpoints/webhooks/post_user_webhook" title="Criar webhook" />

  <QuickLink href="/docs/pix-processamento/endpoints/callbacks/get_user_callbacks" title="Listar callbacks" />
</QuickLinks>

## O que é um webhook (callback) [#o-que-é-um-webhook-callback]

Um **webhook** (também chamado de **callback**) é uma requisição `POST` que **a PayZu envia para o seu servidor** quando algo acontece. Ao contrário da API normal (onde você chama a PayZu), aqui é o oposto: a PayZu chama você.

Pensa numa cobrança Pix. Você criou ela, exibiu o QR ao cliente, e agora precisa saber quando o cliente paga. Duas opções:

1. **Polling**, ficar perguntando a cada X segundos "já pagou? já pagou?" (custoso, lento, desnecessário).
2. **Webhook**, deixar a PayZu te avisar assim que o pagamento entrar (instantâneo, eficiente, recomendado).

<Mermaid
  chart="`
sequenceDiagram
  participant App as Sua aplicação
  participant PZ as PayZu
  participant Banco as Banco do cliente

  App->>PZ: POST /pix com callbackUrl
  PZ-->>App: id, qrCodeText, status PENDING
  Banco->>PZ: Cliente paga
  PZ->>App: POST callbackUrl status COMPLETED
  App-->>PZ: HTTP 200 OK
`"
/>

## Como configurar [#como-configurar]

Você pode receber as notificações de duas formas:

* **Webhook cadastrado** (recomendado): registre uma URL persistente em [`POST /user/webhooks`](/docs/pix-processamento/endpoints/webhooks/post_user_webhook), com segredo HMAC e seleção de eventos. A mesma URL vale para todas as transações.
* **`callbackUrl` por transação**: informe a URL no campo `callbackUrl` do body a cada transação criada:

```json
{
  "amount": 99.90,
  "callbackUrl": "https://seusite.com.br/webhooks/payzu",
  "clientReference": "pedido-2025-001"
}
```

A PayZu vai enviar o callback para essa URL **toda vez que aquela transação mudar de status** (PENDING → COMPLETED, COMPLETED → REFUNDED, etc).

<Steps>
  <Step>
    ### Crie um endpoint público no seu servidor [#crie-um-endpoint-público-no-seu-servidor]

    Algum lugar acessível pela internet que aceite `POST` com JSON. Exemplos: `https://seusite.com.br/webhooks/payzu`, `https://api.suaempresa.com/payzu/callback`.

    Durante desenvolvimento local, use túneis como [ngrok](https://ngrok.com) ou [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) pra expor o `localhost`.
  </Step>

  <Step>
    ### Passe a URL ao criar a transação [#passe-a-url-ao-criar-a-transação]

    Em todo `POST /pix`, `POST /withdraw`, `POST /internal-transfer`, inclua o campo `callbackUrl`. Pode ser a mesma URL pra todos.
  </Step>

  <Step>
    ### Implemente o handler [#implemente-o-handler]

    Receba o `POST`, leia o JSON, processe e responda `2xx` em até 5 segundos. Veja exemplos em [Receber Pix · passo 3](/docs/pix-processamento/tutoriais/receive-pix#receber-callback-quando-pago).
  </Step>
</Steps>

<Callout type="info">
  A PayZu envia `Content-Type: application/json`. Os demais cabeçalhos da entrega estão
  em [Cabeçalhos da entrega](#cabeçalhos-da-entrega).
</Callout>

## Eventos [#eventos]

O campo `events` de [`POST /user/webhooks`](/docs/pix-processamento/endpoints/webhooks/post_user_webhook)
define quais mudanças disparam a notificação. Deixe vazio para receber todas.

Sete eventos acompanham o `status` da transação, um para cada valor:

| Evento                           | Dispara quando                                                                                                                        | `status` no payload  |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
| `TRANSACTION_PENDING`            | A cobrança foi criada e aguarda pagamento, ou o pagamento Pix entrou em processamento. Transferência interna não passa por `PENDING`. | `PENDING`            |
| `TRANSACTION_COMPLETED`          | O pagamento foi confirmado. Em depósito, o cliente pagou; em pagamento Pix, o dinheiro saiu.                                          | `COMPLETED`          |
| `TRANSACTION_CANCELED`           | A transação foi cancelada antes de concluir, por ação manual ou por regra.                                                            | `CANCELED`           |
| `TRANSACTION_WAITING_FOR_REFUND` | O estorno entrou na fila de processamento, em geral após um MED aceito.                                                               | `WAITING_FOR_REFUND` |
| `TRANSACTION_REFUNDED`           | O valor foi devolvido ao pagador.                                                                                                     | `REFUNDED`           |
| `TRANSACTION_EXPIRED`            | A cobrança passou de `expiresIn` sem ser paga.                                                                                        | `EXPIRED`            |
| `TRANSACTION_ERROR`              | A transação falhou no processamento.                                                                                                  | `ERROR`              |

<Callout type="warn">
  Se o `status` da transação mudar antes de a fila processar o evento, a entrega do webhook
  cadastrado é descartada: não há envio, nem registro no histórico, nem retentativa. Em Pix
  rápido o `TRANSACTION_PENDING` costuma não chegar, então não exija um evento anterior para
  aceitar o `TRANSACTION_COMPLETED`.
</Callout>

Três eventos não espelham o `status`:

| Evento                                 | Dispara quando                                                                                                                                                                                                                                                 |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `INFRACTION_CHANGED`                   | Uma [infração do MED](/docs/pix-processamento/med) ligada a uma transação sua foi aberta, teve o status alterado ou foi encerrada. O corpo é o da transação com o objeto `infraction` junto: o id da transação vem em `id` e o da infração em `infraction.id`. |
| `TRANSACTION_SUSPECTED_FRAUD`          | Reservado. Nenhum serviço emite este evento hoje.                                                                                                                                                                                                              |
| `TRANSACTION_SUSPECTED_FRAUD_REVERSAL` | Reservado. Nenhum serviço emite este evento hoje.                                                                                                                                                                                                              |

<Callout type="info">
  Os dois eventos de suspeita de fraude podem ser assinados, mas nenhuma entrega é gerada
  com eles hoje. Um webhook que assina só esses dois não recebe nada.
</Callout>

## Sistema de retry [#sistema-de-retry]

Os webhooks da PayZu têm um sistema robusto de retentativa que garante a
entrega mesmo em falhas temporárias. A PayZu reenvia **até 40 vezes** o
mesmo callback com backoff exponencial e jitter, distribuindo melhor a
carga e evitando picos de requisições.

<Mermaid
  chart="`
flowchart TD
  A[&#x22;Mudança de status na transação&#x22;]
  A --> B[&#x22;PayZu envia POST callbackUrl&#x22;]
  B --> C{&#x22;Resposta 2xx em 5s?&#x22;}
  C -->|Sim| D[&#x22;Entrega confirmada&#x22;]
  C -->|Não| E[&#x22;Aguarda backoff exponencial + jitter&#x22;]
  E --> F{&#x22;Tentativa menor que 40?&#x22;}
  F -->|Sim| B
  F -->|Não| G[&#x22;Marca como falha definitiva&#x22;]

  click D &#x22;/docs/pix-processamento/best-practices/idempotency&#x22; &#x22;Idempotência de callback&#x22;
  click G &#x22;/docs/pix-processamento/endpoints/callbacks/resend_user_callback_single&#x22; &#x22;Reenviar manualmente&#x22;

  style A fill:#f59e0b,stroke:#d97706,color:#ffffff
  style D fill:#14ce71,stroke:#0eb464,color:#ffffff
  style G fill:#ef4444,stroke:#dc2626,color:#ffffff
`"
/>

<Callout type="warn">
  **Tempo de resposta:** o webhook deve responder com um `2xx` (por exemplo
  `200` ou `204`) em até **5 segundos**. Resposta fora da faixa `2xx`, inclusive
  `4xx`, e timeout entram na retentativa do mesmo jeito.
</Callout>

## Segurança [#segurança]

Para garantir integridade e segurança, **restrinja o acesso** ao seu
endpoint de webhook. Solicite o IP oficial da PayZu Processamento ao
suporte e aceite callbacks apenas dessa origem.

## Cabeçalhos da entrega [#cabeçalhos-da-entrega]

| Cabeçalho              | Valor                                                                                                             |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `Content-Type`         | `application/json`                                                                                                |
| `User-Agent`           | `Callback-Service/1.0`                                                                                            |
| `X-Callback-Attempt`   | Número da tentativa desta entrega.                                                                                |
| `X-Callback-Event`     | O evento que disparou a entrega. Só vem em webhook cadastrado.                                                    |
| `X-Callback-Signature` | Assinatura HMAC. Vem sempre que a entrega tem segredo: o do webhook cadastrado ou o segredo de callback da conta. |

O `INFRACTION_CHANGED` chega com o `status` da transação inalterado, então
`X-Callback-Event` é o que distingue as entregas quando você assina mais de um evento.

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

Toda entrega assinada traz o `X-Callback-Signature`. Qual segredo assina depende do destino:

| Destino da entrega         | Segredo que assina           | Onde criar                                                                                                                                                                                                                                 |
| -------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Webhook cadastrado         | Segredo do webhook           | `generateSecret: true` em [`POST /user/webhooks`](/docs/pix-processamento/endpoints/webhooks/post_user_webhook), ou [`POST /user/webhooks/{id}/rotate-secret`](/docs/pix-processamento/endpoints/webhooks/post_user_webhook_rotate_secret) |
| `callbackUrl` da transação | Segredo de callback da conta | `POST /v1/user/callbacks/secret`, trocado em `PATCH /v1/user/callbacks/secret/rotate`                                                                                                                                                      |

<Callout type="warn">
  A entrega para o `callbackUrl` da transação só é assinada se a conta tiver segredo de
  callback cadastrado. Sem esse segredo não há assinatura: crie o segredo ou proteja o
  endpoint por IP de origem.
</Callout>

Valide a assinatura antes de processar o corpo:

<Steps>
  <Step>
    Leia o cabeçalho `X-Callback-Signature`. O valor vem como `t=<timestamp>, v1=<assinatura>`,
    com o horário do envio em segundos (Unix) e a assinatura em hexadecimal de 64 caracteres.
  </Step>

  <Step>
    Monte a string base concatenando o timestamp e o corpo cru da requisição, separados
    por `.`, formando `<timestamp>.<corpo>`.
  </Step>

  <Step>
    Gere um HMAC SHA-256 dessa string usando o segredo do destino, o do webhook ou o
    segredo de callback da conta, e compare com o valor de `v1` em tempo constante. Se
    não coincidirem, rejeite a entrega.
  </Step>

  <Step>
    Rejeite também timestamps fora de uma janela de tolerância. Cada tentativa é assinada
    no momento do envio, então o timestamp de uma retentativa é sempre recente.
  </Step>
</Steps>

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

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

const TOLERANCE_SECONDS = 300;

function verifyCallbackSignature(request, webhookSecret) {
  const header = request.headers["x-callback-signature"];
  if (typeof header !== "string") return false;

  const parts = Object.fromEntries(
    header.split(",").map((part) => part.trim().split("=")),
  );
  const timestamp = Number(parts.t);
  const signature = parts.v1;

  if (!Number.isInteger(timestamp) || !/^[0-9a-f]{64}$/i.test(signature ?? "")) {
    return false;
  }

  const age = Math.abs(Math.floor(Date.now() / 1000) - timestamp);
  if (age > TOLERANCE_SECONDS) return false;

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

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

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

## Campos do payload [#campos-do-payload]

### Identificação [#identificação]

| Campo             | Tipo   | Descrição                                                                                                |
| ----------------- | ------ | -------------------------------------------------------------------------------------------------------- |
| `id`              | string | ID da transação                                                                                          |
| `clientReference` | string | Referência externa que você forneceu                                                                     |
| `virtualAccount`  | string | Subconta virtual (até 50 caracteres). Volta no callback para correlacionar lojas, filiais, marketplaces. |
| `callbackUrl`     | string | URL configurada para receber este webhook                                                                |

### Status e valores [#status-e-valores]

| Campo               | Tipo   | Descrição                                                                                |
| ------------------- | ------ | ---------------------------------------------------------------------------------------- |
| `status`            | string | `PENDING`, `COMPLETED`, `CANCELED`, `WAITING_FOR_REFUND`, `REFUNDED`, `EXPIRED`, `ERROR` |
| `type`              | string | `DEPOSIT`, `WITHDRAW`, `COMMISSION`                                                      |
| `method`            | string | `PIX`, `BANK_SLIP`, `INTERNAL_TRANSFER`                                                  |
| `amount`            | number | Valor em BRL                                                                             |
| `serviceFeeCharged` | number | Tarifa cobrada                                                                           |

`COMMISSION` identifica um lançamento de comissão creditado à sua conta e chega com `TRANSACTION_COMPLETED`.

### Cobrança gerada (depósito) [#cobrança-gerada-depósito]

| Campo               | Tipo   | Descrição                           |
| ------------------- | ------ | ----------------------------------- |
| `qrCodeText`        | string | Código Pix copia-e-cola             |
| `qrCodeUrl`         | string | URL da imagem do QR Code            |
| `qrCodeBase64`      | string | Imagem do QR Code em formato Base64 |
| `generatedName`     | string | Nome de referência                  |
| `generatedDocument` | string | CPF ou CNPJ                         |
| `generatedEmail`    | string | Email vinculado à transação         |

### Pagador [#pagador]

| Campo                  | Tipo   | Descrição                                                                                                                  |
| ---------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------- |
| `payerName`            | string | Nome do pagador                                                                                                            |
| `payerDocument`        | string | Documento do pagador                                                                                                       |
| `payerInstitutionIspb` | string | ISPB do banco do pagador                                                                                                   |
| `payerInstitutionName` | string | Nome do banco do pagador                                                                                                   |
| `payerAccountNumber`   | string | Conta PayZu do pagador (6 dígitos). Preenchida quando a conta PayZu é quem paga: pagamentos Pix e transferências internas. |

### Recebedor [#recebedor]

| Campo                     | Tipo   | Descrição                                                                                                                    |
| ------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------- |
| `receiverName`            | string | Nome do destinatário                                                                                                         |
| `receiverDocument`        | string | Documento do destinatário                                                                                                    |
| `receiverInstitutionIspb` | string | ISPB do banco do destinatário                                                                                                |
| `receiverInstitutionName` | string | Nome do banco do destinatário                                                                                                |
| `receiverAccountNumber`   | string | Conta PayZu do destinatário (6 dígitos). Preenchida quando a conta PayZu é quem recebe: depósitos e transferências internas. |

### Pagamento Pix via chave [#pagamento-pix-via-chave]

| Campo             | Tipo   | Descrição                              |
| ----------------- | ------ | -------------------------------------- |
| `withdrawPixKey`  | string | Chave Pix usada no pagamento           |
| `withdrawPixType` | string | `cpf`, `cnpj`, `phone`, `email`, `evp` |

### Liquidação e estorno [#liquidação-e-estorno]

| Campo                | Tipo   | Descrição                          |
| -------------------- | ------ | ---------------------------------- |
| `endToEndId`         | string | EndToEnd ID do Pix                 |
| `paidAt`             | string | Timestamp do pagamento (ISO 8601)  |
| `cancellationReason` | string | Motivo do cancelamento             |
| `refundEndToEndId`   | string | EndToEnd ID do estorno             |
| `refundAmount`       | string | Valor estornado                    |
| `refundStatus`       | string | `PENDING`, `COMPLETED`, `CANCELED` |
| `refundReason`       | string | Motivo do estorno                  |
| `refundDescription`  | string | Descrição do estorno               |
| `refundedAt`         | string | Timestamp do estorno (ISO 8601)    |

### Timestamps [#timestamps]

| Campo       | Tipo   | Descrição                           |
| ----------- | ------ | ----------------------------------- |
| `createdAt` | string | Timestamp de criação (ISO 8601)     |
| `updatedAt` | string | Timestamp de atualização (ISO 8601) |

### Infração (disputa Pix) [#infração-disputa-pix]

| Campo        | Tipo   | Descrição                                                                   |
| ------------ | ------ | --------------------------------------------------------------------------- |
| `infraction` | object | Detalhes da infração quando aberta (ver [MED](/docs/pix-processamento/med)) |

## Boas práticas [#boas-práticas]

* **Responda rápido**: devolva `2xx` em menos de 5s. Processe pesado em
  fila/worker, não no handler.
* **Idempotência**: deduplique por `id` mais o evento, e não só por `id` + `status`.
  O mesmo callback pode chegar mais de uma vez (retentativa, mudanças sucessivas), e o
  `INFRACTION_CHANGED` não muda o `status`. Ver [Dedupe de callbacks](/docs/pix-processamento/best-practices/idempotency#dedupe-de-callbacks).
* **Use `clientReference`**: passe um identificador externo na criação da
  transação. Volta no callback e facilita correlacionar com seu pedido.
* **Restrinja por IP**: aceite callbacks apenas do IP oficial da PayZu.
* **Responda `2xx` para encerrar a entrega**: qualquer resposta fora da faixa `2xx`,
  inclusive `4xx`, e qualquer timeout entram no mesmo ciclo de até 40 tentativas. Para
  parar o reenvio, responda `2xx` e trate o erro do seu lado.
* **Mascare `payerDocument` nos logs**: imprimir o payload sem mascarar
  dados pessoais é risco LGPD.

## Testar e reenviar [#testar-e-reenviar]

### Testar localmente [#testar-localmente]

Exponha seu localhost via [ngrok](https://ngrok.com) ou [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) e dispare o payload manualmente:

<Tabs items="['curl', 'Node.js', 'Python']">
  <Tab value="curl">
    ```bash
    curl -X POST https://seu-tunel.ngrok.io/webhooks/payzu \
      -H "Content-Type: application/json" \
      -d '{
        "id": "PAYZU20260811K7M2X9QP4T000000",
        "type": "DEPOSIT",
        "status": "COMPLETED",
        "amount": 99.90,
        "clientReference": "order-1234",
        "virtualAccount": "loja-rj-01",
        "paidAt": "2026-08-11T10:46:26.986Z"
      }'
    ```
  </Tab>

  <Tab value="Node.js">
    ```ts
    await fetch('https://seu-tunel.ngrok.io/webhooks/payzu', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        id: 'PAYZU20260811K7M2X9QP4T000000',
        type: 'DEPOSIT',
        status: 'COMPLETED',
        amount: 99.90,
        clientReference: 'order-1234',
        virtualAccount: 'loja-rj-01',
        paidAt: '2026-08-11T10:46:26.986Z',
      }),
    });
    ```
  </Tab>

  <Tab value="Python">
    ```python
    import requests

    requests.post(
        'https://seu-tunel.ngrok.io/webhooks/payzu',
        headers={'Content-Type': 'application/json'},
        json={
            'id': 'PAYZU20260811K7M2X9QP4T000000',
            'type': 'DEPOSIT',
            'status': 'COMPLETED',
            'amount': 99.90,
            'clientReference': 'order-1234',
            'virtualAccount': 'loja-rj-01',
            'paidAt': '2026-08-11T10:46:26.986Z',
        },
    )
    ```
  </Tab>
</Tabs>

### Reenviar um callback real [#reenviar-um-callback-real]

O endpoint de reenvio depende de onde a URL de destino está configurada.

**`callbackUrl` informado na transação:**

* [`POST /user/callbacks/resend/{transactionId}`](/docs/pix-processamento/endpoints/callbacks/resend_user_callback_single), uma transação
* [`POST /user/callbacks/resend`](/docs/pix-processamento/endpoints/callbacks/resend_user_callbacks), lote por filtro, com janela de datas obrigatória

Os dois alcançam apenas transações com `callbackUrl` preenchido e não geram entrega para webhook cadastrado.

**Webhook cadastrado:**

* [`POST /user/callbacks/resend/webhook/{webhookId}`](/docs/pix-processamento/endpoints/callbacks/resend_user_callbacks_webhook), reenfileira as entregas que falharam nesse webhook

O reenvio por webhook reprocessa uma entrega por par de transação e evento, e considera falha a resposta a partir de `300` e a ausência de resposta. Evento cujo `status` já não corresponde ao atual da transação continua sendo descartado no reenvio.

A resposta vem em `enqueued`, com `count` (total aceito para reenvio), `truncated` e `items`. A lista `items` para em 500 entradas. Quando `truncated` é `true`, o reenvio continua cobrindo todas as `count`, só a lista da resposta é que foi cortada.

<Callout type="warn">
  `200` significa aceito para reenvio, não entrega enfileirada: o enfileiramento acontece
  depois da resposta. E a rota deixou de responder `200` com `count: 0`. Sem webhook ativo
  para o `{webhookId}` ela responde `404 PZW300`, e sem nenhum callback falho
  nesse webhook, `404 PZW310`.
</Callout>

### Inspecionar o histórico [#inspecionar-o-histórico]

A PayZu guarda todas as tentativas de entrega. Útil para investigar falha:

* [`GET /user/callbacks`](/docs/pix-processamento/endpoints/callbacks/get_user_callbacks), lista paginada
* [`GET /user/callbacks/{id}`](/docs/pix-processamento/endpoints/callbacks/get_user_callback_by_id), detalhe com status code, response body, response time

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

<QuickLinks>
  <QuickLink href="/docs/pix-processamento/best-practices/idempotency" title="Idempotência" />

  <QuickLink href="/docs/pix-processamento/best-practices/security" title="Segurança" />
</QuickLinks>