PayZuDocs

Instead of polling to check if the payment landed, we notify your server the instant the status changes, and keep retrying with spaced-out attempts up to 40 times if your system doesn't respond.

What is a webhook (callback)

A webhook (also called a callback) is a POST request that PayZu sends to your server when something happens. Unlike the normal API (where you call PayZu), here it's the opposite: PayZu calls you.

Think about a Pix charge. You created it, displayed the QR to the customer, and now you need to know when the customer pays. Two options:

  1. Polling, asking every X seconds "did they pay yet? did they pay yet?" (costly, slow, unnecessary).
  2. Webhook, letting PayZu notify you as soon as the payment comes in (instant, efficient, recommended).

How to configure

You can receive notifications in two ways:

  • Registered webhook (recommended): register a persistent URL at POST /user/webhooks, with HMAC secret and event selection. The same URL applies to all transactions.
  • Per-transaction callbackUrl: provide the URL in the callbackUrl field of the body on each created transaction:
{
  "amount": 99.90,
  "callbackUrl": "https://yoursite.com/webhooks/payzu",
  "clientReference": "pedido-2025-001"
}

PayZu will send the callback to this URL every time that transaction changes status (PENDING → COMPLETED, COMPLETED → REFUNDED, etc).

Create a public endpoint on your server

Somewhere accessible over the internet that accepts POST with JSON. Examples: https://yoursite.com/webhooks/payzu, https://api.yourcompany.com/payzu/callback.

During local development, use tunnels like ngrok or Cloudflare Tunnel to expose localhost.

Pass the URL when creating the transaction

In every POST /pix, POST /withdraw, POST /internal-transfer, include the callbackUrl field. It can be the same URL for all.

Implement the handler

Receive the POST, read the JSON, process it, and respond 2xx within 5 seconds. See examples in Receive Pix · step 3.

PayZu sends Content-Type: application/json. The other delivery headers are in Delivery headers.

Events

The events field of POST /user/webhooks defines which changes trigger the notification. Leave it empty to receive all.

Seven events track the transaction's status, one for each value:

EventTriggers whenstatus in payload
TRANSACTION_PENDINGThe charge was created and awaits payment, or the Pix payment entered processing. Internal transfer does not go through PENDING.PENDING
TRANSACTION_COMPLETEDThe payment was confirmed. For a deposit, the customer paid; for a Pix payment, the money went out.COMPLETED
TRANSACTION_CANCELEDThe transaction was canceled before completing, by manual action or by rule.CANCELED
TRANSACTION_WAITING_FOR_REFUNDThe refund entered the processing queue, usually after an accepted MED.WAITING_FOR_REFUND
TRANSACTION_REFUNDEDThe amount was returned to the payer.REFUNDED
TRANSACTION_EXPIREDThe charge passed expiresIn without being paid.EXPIRED
TRANSACTION_ERRORThe transaction failed in processing.ERROR

If the transaction's status changes before the queue processes the event, the delivery of the registered webhook is discarded: there is no send, no history record, no retry. In fast Pix, TRANSACTION_PENDING typically doesn't arrive, so don't require a prior event to accept TRANSACTION_COMPLETED.

Three events don't mirror the status:

EventTriggers when
INFRACTION_CHANGEDA MED infraction linked to one of your transactions was opened, had its status changed, or was closed. The body is the transaction's, with the infraction object attached: the transaction id comes in id and the infraction's in infraction.id.
TRANSACTION_SUSPECTED_FRAUDReserved. No service emits this event today.
TRANSACTION_SUSPECTED_FRAUD_REVERSALReserved. No service emits this event today.

The two suspected-fraud events can be subscribed to, but no delivery is generated for them today. A webhook that subscribes only to these two receives nothing.

Retry system

PayZu webhooks have a robust retry system that guarantees delivery even under temporary failures. PayZu resends the same callback up to 40 times with exponential backoff and jitter, better distributing the load and avoiding request spikes.

Response time: the webhook must respond with a 2xx (for example 200 or 204) within 5 seconds. A response outside the 2xx range, including 4xx, and timeout enter the retry cycle just the same.

Security

To ensure integrity and security, restrict access to your webhook endpoint. Request the official PayZu Processamento IP from support and only accept callbacks from that origin.

Delivery headers

HeaderValue
Content-Typeapplication/json
User-AgentCallback-Service/1.0
X-Callback-AttemptAttempt number of this delivery.
X-Callback-EventThe event that triggered the delivery. Only present in registered webhooks.
X-Callback-SignatureHMAC signature. Present whenever the delivery has a secret: the registered webhook's or the account's callback secret.

INFRACTION_CHANGED arrives with the transaction's status unchanged, so X-Callback-Event is what distinguishes the deliveries when you subscribe to more than one event.

HMAC verification

Every signed delivery carries X-Callback-Signature. Which secret signs depends on the destination:

Delivery destinationSigning secretWhere to create
Registered webhookWebhook secretgenerateSecret: true in POST /user/webhooks, or POST /user/webhooks/{id}/rotate-secret
Transaction's callbackUrlAccount callback secretPOST /v1/user/callbacks/secret, rotated via PATCH /v1/user/callbacks/secret/rotate

Delivery to the transaction's callbackUrl is only signed if the account has a callback secret registered. Without this secret there is no signature: create the secret or protect the endpoint by source IP.

Validate the signature before processing the body:

Read the X-Callback-Signature header. The value comes as t=<timestamp>, v1=<signature>, with the send time in seconds (Unix) and the signature in 64-character hexadecimal.

Build the base string by concatenating the timestamp and the raw request body, separated by ., forming <timestamp>.<body>.

Generate an HMAC SHA-256 of this string using the destination's secret, the webhook's or the account's callback secret, and compare with the v1 value in constant time. If they don't match, reject the delivery.

Also reject timestamps outside a tolerance window. Each attempt is signed at send time, so a retry's timestamp is always recent.

Node.js example, using crypto.timingSafeEqual to compare signatures in constant time:

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

Compute the HMAC over the raw request body, exactly as received, before any JSON parsing.

Payload fields

Identification

FieldTypeDescription
idstringTransaction ID
clientReferencestringExternal reference you provided
virtualAccountstringVirtual subaccount (up to 50 characters). Returned in the callback to correlate stores, branches, marketplaces.
callbackUrlstringURL configured to receive this webhook

Status and amounts

FieldTypeDescription
statusstringPENDING, COMPLETED, CANCELED, WAITING_FOR_REFUND, REFUNDED, EXPIRED, ERROR
typestringDEPOSIT, WITHDRAW, COMMISSION
methodstringPIX, BANK_SLIP, INTERNAL_TRANSFER
amountnumberAmount in BRL
serviceFeeChargednumberFee charged

COMMISSION identifies a commission entry credited to your account and arrives with TRANSACTION_COMPLETED.

Generated charge (deposit)

FieldTypeDescription
qrCodeTextstringPix copy-and-paste code
qrCodeUrlstringQR Code image URL
qrCodeBase64stringQR Code image in Base64 format
generatedNamestringReference name
generatedDocumentstringCPF or CNPJ
generatedEmailstringEmail linked to the transaction

Payer

FieldTypeDescription
payerNamestringPayer's name
payerDocumentstringPayer's document
payerInstitutionIspbstringISPB of the payer's bank
payerInstitutionNamestringName of the payer's bank
payerAccountNumberstringPayer's PayZu account (6 digits). Filled in when the PayZu account is the one paying: Pix payments and internal transfers.

Receiver

FieldTypeDescription
receiverNamestringRecipient's name
receiverDocumentstringRecipient's document
receiverInstitutionIspbstringISPB of the recipient's bank
receiverInstitutionNamestringName of the recipient's bank
receiverAccountNumberstringRecipient's PayZu account (6 digits). Filled in when the PayZu account is the one receiving: deposits and internal transfers.

Pix payment via key

FieldTypeDescription
withdrawPixKeystringPix key used in the payment
withdrawPixTypestringcpf, cnpj, phone, email, evp

Settlement and refund

FieldTypeDescription
endToEndIdstringPix EndToEnd ID
paidAtstringPayment timestamp (ISO 8601)
cancellationReasonstringCancellation reason
refundEndToEndIdstringRefund EndToEnd ID
refundAmountstringRefunded amount
refundStatusstringPENDING, COMPLETED, CANCELED
refundReasonstringRefund reason
refundDescriptionstringRefund description
refundedAtstringRefund timestamp (ISO 8601)

Timestamps

FieldTypeDescription
createdAtstringCreation timestamp (ISO 8601)
updatedAtstringUpdate timestamp (ISO 8601)

Infraction (Pix dispute)

FieldTypeDescription
infractionobjectInfraction details when opened (see MED)

Best practices

  • Respond fast: return 2xx in under 5s. Do heavy processing in a queue/worker, not in the handler.
  • Idempotency: dedupe by id plus the event, not just by id + status. The same callback may arrive more than once (retry, successive changes), and INFRACTION_CHANGED doesn't change the status. See Callback dedupe.
  • Use clientReference: pass an external identifier when creating the transaction. It comes back in the callback and makes it easier to correlate with your order.
  • Restrict by IP: only accept callbacks from PayZu's official IP.
  • Respond 2xx to end delivery: any response outside the 2xx range, including 4xx, and any timeout enter the same cycle of up to 40 attempts. To stop the resend, respond 2xx and handle the error on your side.
  • Mask payerDocument in logs: printing the payload without masking personal data is an LGPD risk.

Test and resend

Test locally

Expose your localhost via ngrok or Cloudflare Tunnel and trigger the payload manually:

curl -X POST https://your-tunnel.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://your-tunnel.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://your-tunnel.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',
    },
)

Resend a real callback

The resend endpoint depends on where the destination URL is configured.

callbackUrl provided in the transaction:

Both only reach transactions with callbackUrl filled in and do not generate delivery for a registered webhook.

Registered webhook:

The webhook resend reprocesses one delivery per pair of transaction and event, and considers a failure any response from 300 on and the absence of a response. An event whose status no longer matches the transaction's current one is still discarded on resend.

The response comes in enqueued, with count (total accepted for resend), truncated, and items. The items list stops at 500 entries. When truncated is true, the resend still covers all count, only the response's list was cut.

200 means accepted for resend, not queued delivery: queuing happens after the response. And the route no longer responds 200 with count: 0. Without an active webhook for the {webhookId} it responds 404 PZW300, and without a failed callback in the period, 404 PZW310.

Inspect the history

PayZu stores all delivery attempts. Useful for investigating failures:

Next steps

On this page