# Webhooks (/en/docs/cartao/webhooks)

<QuickLinks>
  <QuickLink href="/docs/cartao/endpoints/charges/post_charges" title="Create charge" method="POST" path="/charges" />

  <QuickLink href="/docs/cartao/endpoints/charges/get_charges__chargeId_" title="Get charge" method="GET" path="/charges/{chargeId}" />

  <QuickLink href="/docs/cartao/recurrence" title="Recurring payments" />

  <QuickLink href="/docs/cartao/transaction-status" title="Transaction status" />
</QuickLinks>

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 [#how-to-configure]

Provide the `postbackUrl` when creating the charge ([`POST /charges`](/docs/cartao/endpoints/charges/post_charges)). Whenever an event occurs, PayZu sends a `POST` request in JSON to that URL.

## Events [#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](/docs/cartao/recurrence) cycle charged                       |

## Payload structure [#payload-structure]

| Parameters | Description                      | Type                                                                                       |
| ---------- | -------------------------------- | ------------------------------------------------------------------------------------------ |
| `event`    | Event that triggered the webhook | See the [events](#events) table                                                            |
| `data`     | Updated charge data              | Same value returned by [Get charge](/docs/cartao/endpoints/charges/get_charges__chargeId_) |

```json
{
  "event": "charge.update",
  "data": {}
}
```

The `data` object has exactly the same format as the [Get charge](/docs/cartao/endpoints/charges/get_charges__chargeId_) response.

## Request headers [#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 [#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.

<Callout type="info">
  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.
</Callout>

<Mermaid
  chart="`
flowchart TD
  A[&#x22;Event on the charge&#x22;]
  A --> B[&#x22;PayZu sends POST postbackUrl&#x22;]
  B --> C{&#x22;2xx response within 5s?&#x22;}
  C -->|Yes| D[&#x22;Delivery confirmed&#x22;]
  C -->|No| E[&#x22;Wait: 1min, 10min, 1h, 6h, 24h&#x22;]
  E --> F{&#x22;Fewer than 5 retries?&#x22;}
  F -->|Yes| B
  F -->|No| G[&#x22;Stops retrying&#x22;]
`"
/>

## HMAC verification [#hmac-verification]

Every webhook is signed with your **webhook secret**, provided by PayZu along with your [API credentials](/docs/cartao/authentication). Your API must validate the signature before processing the payload:

<Steps>
  <Step>
    Extract the `x-webhook-timestamp`, `x-webhook-nonce` and `x-webhook-signature` headers.
  </Step>

  <Step>
    Concatenate the timestamp, nonce and payload values, separated by `.`, forming the verification base string: `timestamp.nonce.payload`.
  </Step>

  <Step>
    Generate an HMAC signature with the SHA-256 algorithm from that string, using your webhook secret.
  </Step>

  <Step>
    Compare the generated signature with the value of the `x-webhook-signature` header. If they do not match, reject the webhook.
  </Step>
</Steps>

Example in Node.js, using `crypto.timingSafeEqual` to compare the signatures in constant time:

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

<Callout type="info">
  Compute the HMAC over the raw request body, exactly as received, before any JSON parsing.
</Callout>

## Nonce verification (optional) [#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) [#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.