# 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">
  Header obrigatório no `POST` enviado pela PayZu: `Content-Type: application/json`.
</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é 72 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 em 5s?&#x22;}
  C -->|HTTP 200 OK| D[&#x22;Entrega confirmada&#x22;]
  C -->|Erro ou timeout| E[&#x22;Aguarda backoff exponencial + jitter&#x22;]
  E --> F{&#x22;Tentativa menor que 72?&#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**. Se exceder esse tempo, o sistema
  considera timeout e inicia o processo de retentativa.
</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.

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

Cada webhook é assinado com o seu **webhook secret**, definido ao cadastrar o webhook em [`POST /user/webhooks`](/docs/pix-processamento/endpoints/webhooks/post_user_webhook). Sua aplicação 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. O resultado é hexadecimal de 64 caracteres.
  </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 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`                                                                    |
| `method`            | string | `PIX`, `BANK_SLIP`, `INTERNAL_TRANSFER`                                                  |
| `amount`            | number | Valor em BRL                                                                             |
| `serviceFeeCharged` | number | Tarifa cobrada                                                                           |

### 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: saques 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. |

### Saque via chave Pix [#saque-via-chave-pix]

| Campo             | Tipo   | Descrição                              |
| ----------------- | ------ | -------------------------------------- |
| `withdrawPixKey`  | string | Chave Pix usada no saque               |
| `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**: armazene `id` + `status` para deduplicar. O mesmo
  callback pode chegar mais de uma vez (retentativa, mudanças sucessivas).
* **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.
* **Não responda `4xx` por erro de validação interna**: a PayZu não
  retenta e o callback se perde.
* **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": "PAYZU20251123104518DF75D20A8F",
        "type": "DEPOSIT",
        "status": "COMPLETED",
        "amount": 99.90,
        "clientReference": "order-1234",
        "virtualAccount": "loja-rj-01",
        "paidAt": "2025-11-23T10: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: 'PAYZU20251123104518DF75D20A8F',
        type: 'DEPOSIT',
        status: 'COMPLETED',
        amount: 99.90,
        clientReference: 'order-1234',
        virtualAccount: 'loja-rj-01',
        paidAt: '2025-11-23T10: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': 'PAYZU20251123104518DF75D20A8F',
            'type': 'DEPOSIT',
            'status': 'COMPLETED',
            'amount': 99.90,
            'clientReference': 'order-1234',
            'virtualAccount': 'loja-rj-01',
            'paidAt': '2025-11-23T10:46:26.986Z',
        },
    )
    ```
  </Tab>
</Tabs>

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

Para reprocessar um callback que falhou no seu lado (depois de ter ajustado o handler), use os endpoints de reenvio:

* [`POST /user/callbacks/resend/{transactionId}`](/docs/pix-processamento/endpoints/callbacks/resend_user_callback_single), reenviar callback de uma transação
* [`POST /user/callbacks/resend`](/docs/pix-processamento/endpoints/callbacks/resend_user_callbacks), reenvio em lote

### 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>
