Notifications sent to your system when the charge status changes.
Instead of your system polling "has it been paid yet?", PayZu calls you when something happens: a charge status change, a fraud analysis update, a chargeback or a new recurrence cycle.
How to configure
Provide the postbackUrl when creating the charge (POST /charges). Whenever an event occurs, PayZu sends a POST request in JSON to that URL.
Events
| Event types | Description |
|---|---|
charge.update | Payment status change |
antifraud.update | Fraud analysis completed, reflected in the charge's status and reasonCode |
chargeback | Chargeback notification |
recurrence.cycle | New recurrence cycle charged |
Payload structure
| Parameters | Description | Type |
|---|---|---|
event | Event that triggered the webhook | See the events table |
data | Updated charge data | Same value returned by Get charge |
{
"event": "charge.update",
"data": {}
}The data object has exactly the same format as the Get charge response.
Request headers
Every POST arrives with the following headers:
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-Webhook-Signature | HMAC SHA-256 signature of the payload, in hexadecimal (64 characters) |
X-Webhook-Timestamp | Time of sending, in milliseconds since the Unix epoch |
X-Webhook-Nonce | Unique identifier of the request (32 hexadecimal characters) |
Retries
The first delivery happens as soon as the event occurs. The delivery is only considered successful if your URL responds with an HTTP 2xx status within 5 seconds: any other status, or a slower response, counts as a failure.
After a failure, the webhook makes up to 5 retries. After each failure, the time until the next attempt increases: the retries are made, respectively, after 1 minute, 10 minutes, 1 hour, 6 hours and 24 hours. After that, the attempts stop.
Respond to the webhook quickly (a simple 200 is enough) and process the payload asynchronously, so you do not exceed the 5-second limit. Since a timeout can trigger a redelivery of an event you already processed, consumption must be idempotent: use the charge id combined with the status transition as your deduplication key. Do not use X-Webhook-Nonce for this, it identifies the HTTP request and changes on every redelivery.
HMAC verification
Every webhook is signed with your webhook secret, provided by PayZu along with your API credentials. Your API must validate the signature before processing the payload:
Extract the x-webhook-timestamp, x-webhook-nonce and x-webhook-signature headers.
Concatenate the timestamp, nonce and payload values, separated by ., forming the verification base string: timestamp.nonce.payload.
Generate an HMAC signature with the SHA-256 algorithm from that string, using your webhook secret.
Compare the generated signature with the value of the x-webhook-signature header. If they do not match, reject the webhook.
Example in Node.js, using crypto.timingSafeEqual to compare the signatures in constant time:
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"),
);
}Compute the HMAC over the raw request body, exactly as received, before any JSON parsing.
Nonce verification (optional)
The value of the x-webhook-nonce header acts as a unique, temporary identifier for each request. After extracting it, check whether that nonce has been recorded before:
- If the value has already been used, reject the request to mitigate replay attacks.
- If the nonce is new, store it as used, ensuring it cannot be reused in future calls.
Timestamp verification (optional)
The value of the x-webhook-timestamp header is the time of sending in milliseconds since the Unix epoch. Compare it with the current time: if the difference is greater than 5 minutes, reject the request. This validation discards expired webhooks, preventing the processing of old or potentially malicious messages.