Receba no seu servidor um webhook assinado a cada mudança na conta, como cobrança paga, saque concluído ou contestação aberta.
Os webhooks chegam por dois caminhos, que podem conviver. Com endpoint cadastrado e callbackUrl na operação, o webhook chega nos dois.
| Caminho | Recebe | Segredo que assina |
|---|---|---|
| Endpoint cadastrado | Os eventos escolhidos em events | O secret do endpoint (whsec_…) |
callbackUrl da operação | Todos os eventos daquela operação | O segredo de callback da conta (cbsec_…) |
Endpoint cadastrado
Cadastre a URL e os eventos em POST /transactions/webhooks, escopo WEBHOOK_WRITE, ou no painel.
curl -X POST https://api.hub.payzu.com.br/api/v1/transactions/webhooks \
-H "Authorization: Bearer $PAYZU_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://sualoja.com.br/webhooks/payzu",
"events": ["PAYMENT_PAID", "PAYMENT_EXPIRED", "WITHDRAW_COMPLETED", "WITHDRAW_FAILED"]
}'const res = await fetch('https://api.hub.payzu.com.br/api/v1/transactions/webhooks', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.PAYZU_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://sualoja.com.br/webhooks/payzu',
events: ['PAYMENT_PAID', 'PAYMENT_EXPIRED', 'WITHDRAW_COMPLETED', 'WITHDRAW_FAILED'],
}),
});
const endpoint = await res.json();- A URL precisa ser HTTPS, pública e ter até 2048 caracteres. Não pode repetir a de outro endpoint da conta.
- Mande ao menos um evento. O endpoint nasce ativo.
- Guarde o
secretda resposta: ele só aparece ali. No painel, o botão de novo segredo gera outro, e o anterior para de valer na hora. - Para parar de receber, mande
isActive: falseemPUT /transactions/webhooks/{webhookId}. Endpoint que já teve entrega não pode ser excluído: oDELETEresponde409WEBHOOK_HAS_DELIVERIES.
callbackUrl da operação
Mande callbackUrl no corpo da cobrança, do saque, do pagamento de Pix copia e cola ou da transferência para receber todos os webhooks daquela operação. Aqui não há escolha de eventos.
- Antes, emita o segredo de callback da conta em
POST /transactions/callback-secret. Sem ele, a operação comcallbackUrlé recusada com412CALLBACK_SECRET_MISSING. - O segredo só aparece nessa resposta.
GET /transactions/callback-secretdiz apenas se ele existe, ePOST /transactions/callback-secret/rotategera outro; o anterior para de valer na hora. - A URL segue as regras do endpoint: HTTPS, pública, até 2048 caracteres.
- Chegam todos os eventos da operação, inclusive estorno e contestação, menos
WITHDRAW_REFUND_RECEIVED, que vai só aos endpoints cadastrados. - Na transferência, a URL é de quem envia: o
INTERNAL_TRANSFER_RECEIVED, da conta de destino, não vai para ela. - A URL fica fixada na criação. Repetir a operação com a mesma
externalRefouIdempotency-Keye outracallbackUrldevolve a operação original, com a URL original. - A resposta da operação traz a
callbackUrlaceita. Um campo com outro nome, comocallback_url, é ignorado, e ela voltanull.
Eventos
O corpo de cada evento, campo a campo, está na página dele.
| Evento | Quando chega |
|---|---|
PAYMENT_CREATED | A cobrança foi registrada e o QR Code existe. Ainda não é pagamento. |
PAYMENT_PAID | A cobrança foi paga. Libere o pedido aqui. |
PAYMENT_REFUNDED | O banco estornou o recebimento por conta própria. O valor cheio sai da conta, e a tarifa não volta. |
PAYMENT_EXPIRED | A cobrança venceu sem pagamento. |
WITHDRAW_CREATED | O saque ou o pagamento de Pix copia e cola foi pedido, e o valor e a tarifa saíram do saldo disponível. |
WITHDRAW_COMPLETED | O dinheiro chegou ao destino. |
WITHDRAW_FAILED | O saque falhou, e o valor e a tarifa voltaram ao saldo. |
REFUND_COMPLETED | O estorno pedido pela API, pelo painel ou pelo suporte foi concluído: o valor voltou ao pagador. |
REFUND_FAILED | O estorno foi recusado, e o valor voltou ao saldo. |
DEPOSIT_RECEIVED | Um Pix caiu numa chave da conta sem cobrança. O valor já está creditado. |
INTERNAL_TRANSFER_SENT | A conta enviou uma transferência. |
INTERNAL_TRANSFER_RECEIVED | A conta recebeu uma transferência. |
WITHDRAW_REFUND_RECEIVED | Quem recebeu um Pix da conta devolveu o valor, inteiro ou em parte. |
INFRACTION_OPENED | Uma contestação MED foi aberta contra a conta. |
INFRACTION_CLOSED | A contestação foi encerrada ou cancelada. O status do corpo diz qual. |
INFRACTION_DEADLINE | O prazo de resposta da contestação está chegando: faltam 48, 24 ou 6 horas. |
ACCOUNT_BLOCKED | O banco bloqueou operações da conta, ou a lista de bloqueios mudou. blockedOperations diz o que a API vai recusar. |
ACCOUNT_UNBLOCKED | Os bloqueios saíram. |
O estorno total pedido pela API leva a cobrança a REFUNDED, mas o webhook é REFUND_COMPLETED, não PAYMENT_REFUNDED.
Requisição
POST /webhooks/payzu
Content-Type: application/json
X-Payzu-Event: PAYMENT_PAID
X-Payzu-Delivery: cmu1r7x2k000a01s6h4f2b9qd
X-Payzu-Timestamp: 1791210790441
X-Payzu-Signature: sha256=8f3b2c1d...{
"event": "PAYMENT_PAID",
"id": "cmu1r7x2k000a01s6h4f2b9qd",
"sentAt": "2026-10-05T14:33:10.441Z",
"accountId": "cmu0z8k2a000001s6acct0001",
"data": {
"paymentId": "cmu2wbljx0000e8gtlic8q1gi",
"status": "PAID",
"amount": 1500,
"serviceFee": 105,
"netAmount": 1395,
"metadata": { "pedido": "4821", "canal": "checkout-web" },
"externalRef": "pedido-4821",
"endToEndId": "E99999999202610051433a1b2c3d4e5f"
}
}| Cabeçalho | Valor |
|---|---|
X-Payzu-Event | O evento, o mesmo de event no corpo. |
X-Payzu-Delivery | Identificador da entrega, o mesmo de id no corpo. Igual em todas as tentativas e no reenvio. |
X-Payzu-Timestamp | Instante desta tentativa, em milissegundos (Unix). Entra na assinatura. |
X-Payzu-Signature | sha256= seguido do HMAC-SHA256 em hexadecimal. |
datatraz o corpo do evento, com valores em centavos. Campo sem valor não vem: nenhum chega comonull.sentAté quando a primeira tentativa foi montada e não muda na nova tentativa nem no reenvio.accountIdé a conta que produziu o evento.- Para casar o webhook com o seu pedido, use
externalRefemetadata, que vêm nos quatro eventosPAYMENT_*. - Para consultar a operação, use o identificador de
data:paymentId,withdrawId,transferIdoudepositId. Ele não é oiddevolvido na criação, mas as rotas de consulta aceitam os dois. No extrato, ele aparece emoriginId. refundIdaponta o estorno na listarefundsda cobrança ou do depósito.
Assinatura
A assinatura é o HMAC-SHA256 de <X-Payzu-Timestamp>.<corpo cru>, com o segredo do destino: o secret do endpoint cadastrado ou, nos webhooks enviados à callbackUrl, o segredo de callback da conta.
Leia X-Payzu-Timestamp, X-Payzu-Signature e o corpo exatamente como chegou. Reserializar o JSON muda espaços e ordem de chaves, e a assinatura não fecha.
Calcule HMAC-SHA256(segredo, "<timestamp>.<corpo>") em hexadecimal e compare, em tempo constante, com o valor depois de sha256=.
Recuse timestamp fora de uma janela de tolerância. Cada tentativa é assinada no envio, então o timestamp de uma nova tentativa é sempre recente.
import crypto from 'node:crypto';
const TOLERANCE_MS = 5 * 60 * 1000;
function verifyPayzuSignature(rawBody, headers, secret) {
const timestamp = headers['x-payzu-timestamp'];
const signature = headers['x-payzu-signature'];
if (typeof timestamp !== 'string' || typeof signature !== 'string') return false;
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() - Number(timestamp)) > TOLERANCE_MS) return false;
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const received = /^sha256=([0-9a-f]{64})$/i.exec(signature);
if (!received) return false;
return crypto.timingSafeEqual(Buffer.from(received[1], 'hex'), Buffer.from(expected, 'hex'));
}Resposta e nova tentativa
- Responda com qualquer
2xxem até 10 segundos. Depois disso, a tentativa conta como falha. Se o processamento for demorado, responda antes e processe depois. - São 10 tentativas no total, com espera crescente, de minutos a horas; as duas últimas ficam a 6 horas uma da outra. Do começo ao fim, cerca de 18 horas. Depois da décima, a entrega é abandonada, e o endpoint segue ativo.
- Os webhooks de uma conta saem na ordem em que aconteceram. Enquanto um webhook aguarda nova tentativa, os seguintes da mesma conta esperam, inclusive os de outros endpoints.
No painel, a aba de entregas mostra cada tentativa e a resposta do seu servidor. Uma entrega com falha ou abandonada pode ser reenviada pelo botão Reenviar, com o PIN de operação: vai o mesmo corpo, com o mesmo X-Payzu-Delivery.
Webhook repetido ou fora de ordem
- O mesmo webhook pode chegar mais de uma vez. Descarte o que já foi processado pelo
X-Payzu-Delivery, que é igual em todas as tentativas e no reenvio. - Para deduplicar pela operação, use o par evento + identificador de
data, com duas exceções:INFRACTION_DEADLINEchega até três vezes por contestação, uma porhoursRemaining;ACCOUNT_BLOCKEDeACCOUNT_UNBLOCKEDnão têm identificador. - A ordem se perde quando uma entrega abandonada é reenviada depois, e não existe entre contas. Onde há
data.status, ele é o estado no momento do evento: umPAYMENT_CREATEDque chega depois doPAYMENT_PAIDnão desfaz o pagamento. Nos webhooks de bloqueio, vale ochangedAtmais recente.