Skip to main content
Webhooks are HTTP notifications that Z2Pay sends to your server whenever something happens in your account (a transaction is paid, a refund is completed, a subscription is cancelled, etc.). Instead of polling the API repeatedly, Z2Pay makes a POST request to a URL you register.
This page explains how to receive and validate events. To create, list, and delete webhook endpoints, see Core API: Webhooks. For the full list of available events, see Events.

How it works

1

Register an endpoint

Register a public URL (HTTPS) and, optionally, a secret. You also choose which events you want to receive — an empty list means “all events”. See Core API: Webhooks.
2

An event occurs

Something changes in your account (e.g., a transaction is paid). Z2Pay selects your active webhooks that are listening for that event type.
3

Z2Pay POSTs to your endpoint

For each matching webhook, Z2Pay sends a POST with the event’s JSON envelope in the request body along with identification and signature headers.
4

Your server responds with 2xx

Respond with a 2xx status as quickly as possible. Any response outside the 2xx range is treated as a delivery failure.

The event envelope

Every webhook arrives in the same envelope format. The resource-specific content is always nested inside data.
{
  "id": "whd_3f8a2c1d9e4b5a6c7d8e9f01",
  "type": "transaction.paid",
  "data": {
    "id": "txn_a1b2c3d4e5f6a7b8c9d0e1f2",
    "status": "paid",
    "amount": 15000,
    "paymentMethod": "credit_card"
  },
  "occurredAt": "2026-06-24T18:32:05.184Z",
  "companyId": "comp_9a8b7c6d5e4f3a2b1c0d9e8f"
}
id
string
Unique identifier for this delivery (prefix whd_). Every attempt to deliver the same event to the same endpoint uses the same id — use it as an idempotency key (see below).
type
string
The event type, e.g., transaction.paid. Full list in Events.
data
object
The resource object related to the event (the transaction, refund, subscription, etc.). The shape of data depends on the type.
occurredAt
string
The date and time the event was dispatched, in ISO 8601 with timezone (UTC).
companyId
string
Your company identifier (prefix comp_).
Monetary values in data are always integers in cents. 15000 means R$ 150.00. Do not assume decimal places. See Conventions.

Request headers

Every webhook POST includes:
HeaderAlways presentDescription
Content-TypeYesapplication/json
User-AgentYesZ2Pay-Webhooks/1.0
X-Webhook-Event-IdYesIdentifier of the originating event. Used for correlation.
X-Webhook-TimestampOnly with secretSignature timestamp, in ISO 8601 with timezone.
X-Webhook-SignatureOnly with secretHMAC-SHA256 signature of the body, in hexadecimal.
The X-Webhook-Timestamp and X-Webhook-Signature headers are only sent when you registered a secret on the webhook. Without a secret, there is no signature to validate — which is why we recommend always configuring a secret.

Verifying the signature

When the webhook has a secret, Z2Pay computes the signature as follows:
X-Webhook-Signature = HMAC-SHA256(secret, JSON.stringify(payload))   // in hexadecimal
Where payload is the entire envelope (the JSON with id, type, data, occurredAt, and companyId) — exactly the body received in the request.
Compute the HMAC over the raw body of the request, exactly as received. If you parse it into an object and then re-serialize it (JSON.stringify), the key order or whitespace may change and the signature will not match. Capture the raw body before any middleware that parses JSON.

Node.js example

import { createHmac, timingSafeEqual } from 'node:crypto';

/**
 * Validates the signature of a Z2Pay webhook.
 *
 * @param {string} rawBody  Raw request body (string), NOT re-serialized.
 * @param {string} signature Value of the 'x-webhook-signature' header.
 * @param {string} secret   The secret registered on the webhook.
 * @returns {boolean}
 */
function verifyWebhookSignature(rawBody, signature, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');

  // Different lengths -> invalid signature (timingSafeEqual requires equal sizes)
  if (expected.length !== signature.length) return false;

  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Usage example with Express (capturing the raw body):
import express from 'express';

const app = express();

// Capture the raw body as a string BEFORE any JSON parsing.
app.use('/webhooks/z2pay', express.raw({ type: 'application/json' }));

app.post('/webhooks/z2pay', (req, res) => {
  const rawBody = req.body.toString('utf8');
  const signature = req.header('x-webhook-signature');

  const ok = verifyWebhookSignature(rawBody, signature, process.env.Z2PAY_WEBHOOK_SECRET);
  if (!ok) {
    return res.status(401).send('invalid signature');
  }

  const event = JSON.parse(rawBody);

  // TODO: enqueue/process asynchronously; respond quickly.
  console.log('Evento recebido:', event.type, event.id);

  return res.status(200).send('ok');
});
Always use crypto.timingSafeEqual (constant-time comparison) to prevent timing attacks. Never compare signatures directly with ===.

Idempotency in your receiver

The same event may arrive more than once (due to internal reprocessing or a slow response from your server). Your endpoint must be idempotent.
1

Use the delivery id as a key

Store the id from the envelope (prefix whd_) when processing the event. It is stable across retries of the same delivery.
2

Skip already-processed events

Before applying side effects, check whether that id has already been processed. If so, respond with 2xx and do nothing further.
3

Respond quickly

Do the heavy lifting asynchronously (queue/worker). Respond with 2xx as soon as you validate the signature and record the event.
The X-Webhook-Event-Id identifies the originating event and can also be used for correlation and deduplication. The envelope id, on the other hand, is specific to the delivery to your endpoint.

Timeout and retries

Timeout

Each POST has a timeout of 30 seconds. If your server does not respond within that window, the attempt is considered a failure.

What counts as success

Only HTTP responses in the 2xx range are considered a successful delivery. Redirects, 4xx, and 5xx all count as failures.
Z2Pay logs the result of each delivery attempt (the HTTP status returned by your server, the response body, and the attempt count). A delivery only needs a 2xx response from you to be marked as successful.

Best practices

Without a secret, there are no signature headers and you cannot guarantee that the request actually came from Z2Pay. Set a strong secret when registering the webhook in Core API: Webhooks.
Reject with 401 any request whose signature does not match. Do not process the event before validating it.
Enqueue the event and respond with 2xx promptly. Slow processing increases the risk of hitting the 30s timeout and triggering redeliveries.
In critical flows (e.g., fulfilling an order), confirm the state by querying Core API: Transactions with the id received in data.

See also

Events

The list of event types (type) that Z2Pay sends.

Core API: Webhooks

Create, list, and delete webhook endpoints.

Errors

API error format and error codes.

Conventions

Prefixed IDs, values in cents, and ISO 8601 dates.