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:
- Polling, ficar perguntando a cada X segundos "já pagou? já pagou?" (custoso, lento, desnecessário).
- 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. callbackUrlpor transação: informe a URL no campocallbackUrldo 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:
| 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 |
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:
| Evento | Dispara quando |
|---|---|
INFRACTION_CHANGED | Uma 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_FRAUD | Reservado. Nenhum serviço emite este evento hoje. |
TRANSACTION_SUSPECTED_FRAUD_REVERSAL | Reservado. 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ç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
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, ou POST /user/webhooks/{id}/rotate-secret |
callbackUrl da transação | Segredo de callback da conta | POST /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
| 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
| 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)
| 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
| 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
| 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
| Campo | Tipo | Descrição |
|---|---|---|
withdrawPixKey | string | Chave Pix usada no pagamento |
withdrawPixType | string | cpf, cnpj, phone, email, evp |
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
| Campo | Tipo | Descrição |
|---|---|---|
createdAt | string | Timestamp de criação (ISO 8601) |
updatedAt | string | Timestamp de atualização (ISO 8601) |
Infração (disputa Pix)
| Campo | Tipo | Descrição |
|---|---|---|
infraction | object | Detalhes da infração quando aberta (ver MED) |
Boas práticas
- Responda rápido: devolva
2xxem menos de 5s. Processe pesado em fila/worker, não no handler. - Idempotência: deduplique por
idmais o evento, e não só porid+status. O mesmo callback pode chegar mais de uma vez (retentativa, mudanças sucessivas), e oINFRACTION_CHANGEDnão muda ostatus. 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
2xxpara encerrar a entrega: qualquer resposta fora da faixa2xx, inclusive4xx, e qualquer timeout entram no mesmo ciclo de até 40 tentativas. Para parar o reenvio, responda2xxe trate o erro do seu lado. - Mascare
payerDocumentnos 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:
POST /user/callbacks/resend/{transactionId}, uma transaçãoPOST /user/callbacks/resend, 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}, 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.
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:
GET /user/callbacks, lista paginadaGET /user/callbacks/{id}, detalhe com status code, response body, response time