Saltar al contenido principal
Los webhooks son notificaciones HTTP que Z2Pay envía a tu servidor cada vez que algo ocurre en tu cuenta (una transacción se paga, un reembolso se completa, una suscripción se cancela, etc.). En lugar de consultar la API repetidamente, Z2Pay realiza un POST a una URL que tú registras.
Esta página explica cómo recibir y validar los eventos. Para registrar, listar y eliminar endpoints de webhook, consulta Core API: Webhooks. Para la lista de eventos disponibles, consulta Eventos.

Cómo funciona

1

Registras un endpoint

Registra una URL pública (HTTPS) y, opcionalmente, un secret. También eliges qué eventos quieres recibir — una lista vacía significa “todos los eventos”. Consulta Core API: Webhooks.
2

Ocurre un evento

Algo cambia en tu cuenta (ej.: una transacción se paga). Z2Pay selecciona tus webhooks activos que escuchan ese tipo de evento.
3

Z2Pay hace POST en tu endpoint

Por cada webhook correspondiente, Z2Pay envía un POST con el envelope JSON del evento en el cuerpo de la solicitud y cabeceras de identificación/firma.
4

Tu servidor responde 2xx

Responde con un estado 2xx lo más rápido posible. Cualquier respuesta fuera del rango 2xx se trata como un fallo de entrega.

El envelope del evento

Todo webhook llega en el mismo formato de envelope. El contenido específico del recurso siempre se encuentra dentro de 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
Identificador único de esta entrega (prefijo whd_). Cada intento de entregar el mismo evento al mismo endpoint usa el mismo id — úsalo como clave de idempotencia (ver más abajo).
type
string
El tipo del evento, ej.: transaction.paid. Lista completa en Eventos.
data
object
El objeto del recurso relacionado con el evento (la transacción, el reembolso, la suscripción, etc.). El formato de data depende del type.
occurredAt
string
Fecha y hora en que el evento fue despachado, en ISO 8601 con timezone (UTC).
companyId
string
Identificador de tu company (prefijo comp_).
Los valores monetarios en data son siempre enteros en centavos. 15000 significa R$ 150,00. No asumas decimales. Consulta Convenciones.

Cabeceras de la solicitud

Todo POST de webhook incluye:
CabeceraSiempre presenteDescripción
Content-Typeapplication/json
User-AgentZ2Pay-Webhooks/1.0
X-Webhook-Event-IdIdentificador del evento de origen. Usado para correlación.
X-Webhook-TimestampSolo con secretMomento de la firma, en ISO 8601 con timezone.
X-Webhook-SignatureSolo con secretFirma HMAC-SHA256 del cuerpo, en hexadecimal.
Las cabeceras X-Webhook-Timestamp y X-Webhook-Signature solo se envían cuando registraste un secret en el webhook. Sin secret, no hay firma que validar — por eso recomendamos siempre configurar un secret.

Verificando la firma

Cuando el webhook tiene secret, Z2Pay calcula la firma de la siguiente manera:
X-Webhook-Signature = HMAC-SHA256(secret, JSON.stringify(payload))   // en hexadecimal
Donde payload es el envelope completo (el JSON con id, type, data, occurredAt y companyId) — exactamente el cuerpo que llega en la solicitud.
Calcula el HMAC sobre el cuerpo sin procesar (raw body) de la solicitud, exactamente como fue recibido. Si haces parse a objeto y luego re-serializas (JSON.stringify), el orden de las claves o el espaciado puede cambiar y la firma no coincidirá. Captura el raw body antes de cualquier middleware que haga parse de JSON.

Ejemplo en Node.js

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

/**
 * Valida a assinatura de um webhook do Z2Pay.
 *
 * @param {string} rawBody  Corpo bruto da requisição (string), NÃO re-serializado.
 * @param {string} signature Valor do cabeçalho 'x-webhook-signature'.
 * @param {string} secret   O secret cadastrado no webhook.
 * @returns {boolean}
 */
function verifyWebhookSignature(rawBody, signature, secret) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');

  // Comprimentos diferentes -> assinatura inválida (timingSafeEqual exige tamanhos iguais)
  if (expected.length !== signature.length) return false;

  return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}
Ejemplo de uso con Express (capturando el raw body):
import express from 'express';

const app = express();

// Captura el raw body como string ANTES de cualquier parse de JSON.
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: enfileirar/processar de forma assíncrona; responda rápido.
  console.log('Evento recebido:', event.type, event.id);

  return res.status(200).send('ok');
});
Usa siempre crypto.timingSafeEqual (comparación en tiempo constante) para evitar ataques de timing. Nunca compares firmas con === directamente.

Idempotencia en tu receptor

El mismo evento puede llegar más de una vez (por reprocesamiento interno o porque tu respuesta tardó demasiado). Tu endpoint debe ser idempotente.
1

Usa el id de la entrega como clave

Guarda el id del envelope (prefijo whd_) al procesar el evento. Es estable entre intentos de la misma entrega.
2

Ignora lo que ya fue procesado

Antes de aplicar efectos secundarios, verifica si ese id ya fue procesado. Si es así, responde 2xx y no hagas nada más.
3

Responde rápido

Realiza el trabajo pesado de forma asíncrona (cola/worker). Responde 2xx tan pronto como valides la firma y registres el evento.
El X-Webhook-Event-Id identifica el evento de origen y también puede usarse para correlación y deduplicación. El id del envelope es específico de la entrega a tu endpoint.

Tiempo límite e intentos

Tiempo límite

Cada POST tiene un tiempo límite de 30 segundos. Si tu servidor no responde dentro de ese plazo, el intento se considera fallido.

Qué cuenta como éxito

Solo las respuestas HTTP en el rango 2xx se consideran entrega exitosa. Redirecciones, 4xx y 5xx cuentan como fallo.
Z2Pay registra el resultado de cada intento de entrega (estado HTTP retornado por tu servidor, cuerpo de la respuesta y número de intentos). La entrega solo requiere que respondas 2xx para ser marcada como exitosa.

Buenas prácticas

Sin secret, no hay cabeceras de firma y no puedes garantizar que la solicitud provino de Z2Pay. Configura un secret sólido al registrar el webhook en Core API: Webhooks.
Rechaza con 401 cualquier solicitud cuya firma no coincida. No proceses el evento antes de validar.
Encola el evento y responde 2xx rápidamente. El procesamiento lento aumenta el riesgo de superar el tiempo límite de 30s y generar reentregas.
En flujos críticos (ej.: liberar un pedido), confirma el estado consultando la Core API: Transactions con el id recibido en data.

Ver también

Eventos

La lista de tipos de evento (type) que Z2Pay envía.

Core API: Webhooks

Registrar, listar y eliminar endpoints de webhook.

Errores

Formato y códigos de error de la API.

Convenciones

IDs con prefijo, valores en centavos y fechas en ISO 8601.