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é 72 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.
Header obrigatório no POST enviado pela PayZu: Content-Type: application/json.
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.
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.
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
Cada webhook é assinado com o seu webhook secret, definido ao cadastrar o webhook em POST /user/webhooks. Sua aplicação deve validar a assinatura antes de processar o payload:
Extraia os cabeçalhos x-webhook-timestamp, x-webhook-nonce e x-webhook-signature.
Concatene os valores do timestamp, do nonce e do payload, separados por ., formando a string base de verificação: timestamp.nonce.payload.
Gere uma assinatura HMAC com o algoritmo SHA-256 a partir dessa string, usando o seu webhook secret. O resultado é hexadecimal de 64 caracteres.
Compare a assinatura gerada com o valor do cabeçalho x-webhook-signature. Se não coincidirem, rejeite o webhook.
Exemplo em Node.js, usando crypto.timingSafeEqual para comparar as assinaturas em tempo constante:
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"),
);
}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 |
method | string | PIX, BANK_SLIP, INTERNAL_TRANSFER |
amount | number | Valor em BRL |
serviceFeeCharged | number | Tarifa cobrada |
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: saques 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. |
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
| 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: armazene
id+statuspara 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
4xxpor erro de validação interna: a PayZu não retenta e o callback se perde. - 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": "PAYZU20251123104518DF75D20A8F",
"type": "DEPOSIT",
"status": "COMPLETED",
"amount": 99.90,
"clientReference": "order-1234",
"virtualAccount": "loja-rj-01",
"paidAt": "2025-11-23T10:46:26.986Z"
}'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',
}),
});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',
},
)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}, reenviar callback de uma transaçãoPOST /user/callbacks/resend, reenvio em lote
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
Próximos passos
Autenticação
Cada chamada leva um Bearer token que vale como senha: veja como enviar, onde guardar em segurança e o que fazer quando a resposta volta 401 ou 403.
Conceitos
O mapa mental antes da primeira chamada: o que a API cobre, como as operações se agrupam e o que é uma transação na PayZu, tudo em reais e sobre HTTPS.