PayZuDocs

Webhooks

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.

CaminhoRecebeSegredo que assina
Endpoint cadastradoOs eventos escolhidos em eventsO secret do endpoint (whsec_…)
callbackUrl da operaçãoTodos os eventos daquela operaçãoO 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 secret da 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: false em PUT /transactions/webhooks/{webhookId}. Endpoint que já teve entrega não pode ser excluído: o DELETE responde 409 WEBHOOK_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 com callbackUrl é recusada com 412 CALLBACK_SECRET_MISSING.
  • O segredo só aparece nessa resposta. GET /transactions/callback-secret diz apenas se ele existe, e POST /transactions/callback-secret/rotate gera 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 externalRef ou Idempotency-Key e outra callbackUrl devolve a operação original, com a URL original.
  • A resposta da operação traz a callbackUrl aceita. Um campo com outro nome, como callback_url, é ignorado, e ela volta null.

Eventos

O corpo de cada evento, campo a campo, está na página dele.

EventoQuando chega
PAYMENT_CREATEDA cobrança foi registrada e o QR Code existe. Ainda não é pagamento.
PAYMENT_PAIDA cobrança foi paga. Libere o pedido aqui.
PAYMENT_REFUNDEDO banco estornou o recebimento por conta própria. O valor cheio sai da conta, e a tarifa não volta.
PAYMENT_EXPIREDA cobrança venceu sem pagamento.
WITHDRAW_CREATEDO saque ou o pagamento de Pix copia e cola foi pedido, e o valor e a tarifa saíram do saldo disponível.
WITHDRAW_COMPLETEDO dinheiro chegou ao destino.
WITHDRAW_FAILEDO saque falhou, e o valor e a tarifa voltaram ao saldo.
REFUND_COMPLETEDO estorno pedido pela API, pelo painel ou pelo suporte foi concluído: o valor voltou ao pagador.
REFUND_FAILEDO estorno foi recusado, e o valor voltou ao saldo.
DEPOSIT_RECEIVEDUm Pix caiu numa chave da conta sem cobrança. O valor já está creditado.
INTERNAL_TRANSFER_SENTA conta enviou uma transferência.
INTERNAL_TRANSFER_RECEIVEDA conta recebeu uma transferência.
WITHDRAW_REFUND_RECEIVEDQuem recebeu um Pix da conta devolveu o valor, inteiro ou em parte.
INFRACTION_OPENEDUma contestação MED foi aberta contra a conta.
INFRACTION_CLOSEDA contestação foi encerrada ou cancelada. O status do corpo diz qual.
INFRACTION_DEADLINEO prazo de resposta da contestação está chegando: faltam 48, 24 ou 6 horas.
ACCOUNT_BLOCKEDO banco bloqueou operações da conta, ou a lista de bloqueios mudou. blockedOperations diz o que a API vai recusar.
ACCOUNT_UNBLOCKEDOs 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çalhoValor
X-Payzu-EventO evento, o mesmo de event no corpo.
X-Payzu-DeliveryIdentificador da entrega, o mesmo de id no corpo. Igual em todas as tentativas e no reenvio.
X-Payzu-TimestampInstante desta tentativa, em milissegundos (Unix). Entra na assinatura.
X-Payzu-Signaturesha256= seguido do HMAC-SHA256 em hexadecimal.
  • data traz o corpo do evento, com valores em centavos. Campo sem valor não vem: nenhum chega como null.
  • 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 externalRef e metadata, que vêm nos quatro eventos PAYMENT_*.
  • Para consultar a operação, use o identificador de data: paymentId, withdrawId, transferId ou depositId. Ele não é o id devolvido na criação, mas as rotas de consulta aceitam os dois. No extrato, ele aparece em originId.
  • refundId aponta o estorno na lista refunds da 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 2xx em 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_DEADLINE chega até três vezes por contestação, uma por hoursRemaining; ACCOUNT_BLOCKED e ACCOUNT_UNBLOCKED nã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: um PAYMENT_CREATED que chega depois do PAYMENT_PAID não desfaz o pagamento. Nos webhooks de bloqueio, vale o changedAt mais recente.

Nesta página