PayZuDocs

Webhooks

Em vez de ficar perguntando se o pagamento caiu, a gente avisa o seu servidor no instante em que o status muda, e insiste com novas tentativas espaçadas por até 40 vezes se o seu sistema não responder.

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).

Como configurar

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

  • Webhook cadastrado (recomendado): registre uma URL persistente em POST /user/webhooks, 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:
{
  "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).

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 ou Cloudflare Tunnel pra expor o localhost.

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.

Implemente o handler

Receba o POST, leia o JSON, processe e responda 2xx em até 5 segundos. Veja exemplos em Receber Pix · passo 3.

A PayZu envia Content-Type: application/json. Os demais cabeçalhos da entrega estão em Cabeçalhos da entrega.

Eventos

O campo events de POST /user/webhooks 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:

EventoDispara quandostatus no payload
TRANSACTION_PENDINGA cobrança foi criada e aguarda pagamento, ou o pagamento Pix entrou em processamento. Transferência interna não passa por PENDING.PENDING
TRANSACTION_COMPLETEDO pagamento foi confirmado. Em depósito, o cliente pagou; em pagamento Pix, o dinheiro saiu.COMPLETED
TRANSACTION_CANCELEDA transação foi cancelada antes de concluir, por ação manual ou por regra.CANCELED
TRANSACTION_WAITING_FOR_REFUNDO estorno entrou na fila de processamento, em geral após um MED aceito.WAITING_FOR_REFUND
TRANSACTION_REFUNDEDO valor foi devolvido ao pagador.REFUNDED
TRANSACTION_EXPIREDA cobrança passou de expiresIn sem ser paga.EXPIRED
TRANSACTION_ERRORA transação falhou no processamento.ERROR

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.

Três eventos não espelham o status:

EventoDispara quando
INFRACTION_CHANGEDUma infração do 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_FRAUDReservado. Nenhum serviço emite este evento hoje.
TRANSACTION_SUSPECTED_FRAUD_REVERSALReservado. Nenhum serviço emite este evento hoje.

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.

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.

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.

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çalhoValor
Content-Typeapplication/json
User-AgentCallback-Service/1.0
X-Callback-AttemptNúmero da tentativa desta entrega.
X-Callback-EventO evento que disparou a entrega. Só vem em webhook cadastrado.
X-Callback-SignatureAssinatura 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

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

Destino da entregaSegredo que assinaOnde criar
Webhook cadastradoSegredo do webhookgenerateSecret: true em POST /user/webhooks, ou POST /user/webhooks/{id}/rotate-secret
callbackUrl da transaçãoSegredo de callback da contaPOST /v1/user/callbacks/secret, trocado em PATCH /v1/user/callbacks/secret/rotate

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.

Valide a assinatura antes de processar o corpo:

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.

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

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.

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.

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

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"),
  );
}

Calcule o HMAC sobre o corpo cru da requisição, exatamente como recebido, antes de qualquer parse de JSON.

Campos do payload

Identificação

CampoTipoDescrição
idstringID da transação
clientReferencestringReferência externa que você forneceu
virtualAccountstringSubconta virtual (até 50 caracteres). Volta no callback para correlacionar lojas, filiais, marketplaces.
callbackUrlstringURL configurada para receber este webhook

Status e valores

CampoTipoDescrição
statusstringPENDING, COMPLETED, CANCELED, WAITING_FOR_REFUND, REFUNDED, EXPIRED, ERROR
typestringDEPOSIT, WITHDRAW, COMMISSION
methodstringPIX, BANK_SLIP, INTERNAL_TRANSFER
amountnumberValor em BRL
serviceFeeChargednumberTarifa cobrada

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

Cobrança gerada (depósito)

CampoTipoDescrição
qrCodeTextstringCódigo Pix copia-e-cola
qrCodeUrlstringURL da imagem do QR Code
qrCodeBase64stringImagem do QR Code em formato Base64
generatedNamestringNome de referência
generatedDocumentstringCPF ou CNPJ
generatedEmailstringEmail vinculado à transação

Pagador

CampoTipoDescrição
payerNamestringNome do pagador
payerDocumentstringDocumento do pagador
payerInstitutionIspbstringISPB do banco do pagador
payerInstitutionNamestringNome do banco do pagador
payerAccountNumberstringConta PayZu do pagador (6 dígitos). Preenchida quando a conta PayZu é quem paga: pagamentos Pix e transferências internas.

Recebedor

CampoTipoDescrição
receiverNamestringNome do destinatário
receiverDocumentstringDocumento do destinatário
receiverInstitutionIspbstringISPB do banco do destinatário
receiverInstitutionNamestringNome do banco do destinatário
receiverAccountNumberstringConta PayZu do destinatário (6 dígitos). Preenchida quando a conta PayZu é quem recebe: depósitos e transferências internas.

Pagamento Pix via chave

CampoTipoDescrição
withdrawPixKeystringChave Pix usada no pagamento
withdrawPixTypestringcpf, cnpj, phone, email, evp

Liquidação e estorno

CampoTipoDescrição
endToEndIdstringEndToEnd ID do Pix
paidAtstringTimestamp do pagamento (ISO 8601)
cancellationReasonstringMotivo do cancelamento
refundEndToEndIdstringEndToEnd ID do estorno
refundAmountstringValor estornado
refundStatusstringPENDING, COMPLETED, CANCELED
refundReasonstringMotivo do estorno
refundDescriptionstringDescrição do estorno
refundedAtstringTimestamp do estorno (ISO 8601)

Timestamps

CampoTipoDescrição
createdAtstringTimestamp de criação (ISO 8601)
updatedAtstringTimestamp de atualização (ISO 8601)

Infração (disputa Pix)

CampoTipoDescrição
infractionobjectDetalhes da infração quando aberta (ver MED)

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.
  • 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 localmente

Exponha seu localhost via ngrok ou Cloudflare Tunnel e dispare o payload manualmente:

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"
  }'
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',
  }),
});
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',
    },
)

Reenviar um callback real

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

callbackUrl informado na transação:

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

Webhook cadastrado:

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.

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.

Inspecionar o histórico

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

Próximos passos

Nesta página