Integrar webhooks
Esta guía está dirigida a los equipos de desarrollo y arquitectura que van a recibir y procesar los webhooks de la plataforma en sus propios sistemas o servidores backend.
Si solo necesitas dar de alta o consultar una suscripción desde la interfaz gráfica del panel de administración, consulta Administrar webhooks desde el panel.
1. Cómo funciona el servicio de webhooks
- Alta de suscripción: Defines una URL HTTPS donde escucharás eventos, el alcance deseado (Tenant completo o Comercio específico) y una lista explícita de tipos de eventos públicos (
event_types). - Despacho en tiempo real: Cuando ocurre una operación (venta, actualización de estado, alta de terminal o comercio), el despachador de webhooks consume el evento en Kafka, lo transforma a un DTO público limpio (sin datos sensibles ni de telemetría interna) y ejecuta un
POSTHTTP firmado hacia tu endpoint. - Confirmación y reintentos: Tu endpoint debe responder con un código HTTP
2xxen pocos segundos. Si responde con error, timeout o el servidor no está disponible, la plataforma aplicará reintentos automáticos con retroceso exponencial.
2. Alcance de la suscripción (scope_kind)
El servicio soporta dos niveles de alcance:
Alcance (scope_kind) | merchant_id | Descripción |
|---|---|---|
tenant (Global) | Omitido o null | Recibe todos los eventos generados en cualquier comercio o terminal de todo el tenant (tenant_wuzi). Es el modo recomendado para socios integradores que administran una red completa de comercios y TPVs. |
merchant (Comercio) | Obligatorio | Recibe exclusivamente los eventos originados en el comercio indicado. Útil para comercios independientes o integraciones específicas de una sola tienda. |
Por diseño estricto, una suscripción con scope_kind: "tenant" rechaza peticiones que incluyan merchant_id. Igualmente, una suscripción scope_kind: "merchant" exige obligatoriamente un merchant_id válido.
3. Catálogo de eventos disponibles
La suscripción debe incluir explícitamente en el arreglo event_types los eventos que desea recibir. Si event_types está vacío, por seguridad la suscripción falla cerrada y no recibe nada.
A. Ventas y Transacciones (sale.*)
| Evento | Cuándo se dispara | Estado de la venta |
|---|---|---|
sale.created | Se inicia y procesa una venta en la plataforma (moneda MXN). | pending / inicial |
sale.approved | La venta fue aprobada y autorizada satisfactoriamente por el procesador adquirente. | approved |
sale.declined | La venta fue declinada o rechazada (fondos insuficientes, sospecha de fraude, error de banco emisor, etc.). | declined |
sale.cancelled | La venta fue cancelada mediante una operación directa de cancelación o reverso el mismo día. | cancelled |
sale.refunded | Se procesó una devolución o reembolso parcial o total de la venta. | refunded |
B. Terminales (terminal.*)
| Evento | Cuándo se dispara |
|---|---|
terminal.created | Se registra y aprovisiona una nueva terminal punto de venta lógica asociada a un comercio. |
terminal.updated | Cambia el estado público de una terminal (ej. activa, inactiva, bloqueada). |
C. Comercios (merchant.*)
| Evento | Cuándo se dispara |
|---|---|
merchant.created | Se da de alta un nuevo comercio en la plataforma. |
merchant.updated | Cambia el estado público de un comercio (ej. activo, suspendido). |
4. Crear suscripciones vía API
Puedes gestionar suscripciones programáticamente mediante la API de Webhooks (POST /subscriptions).
Suscripción a nivel Tenant (Todos los comercios de Wuzi)
POST /subscriptions HTTP/1.1
Host: dev-partner-api-wuzi.paymentnexus.com.mx
Content-Type: application/json
Authorization: Bearer <TU_TOKEN>
{
"tenant_id": "tenant_wuzi",
"scope_kind": "tenant",
"endpoint_url": "https://api.tu-servidor.com/webhooks/nexus",
"event_types": [
"sale.created",
"sale.approved",
"sale.declined",
"sale.cancelled",
"sale.refunded",
"terminal.created",
"terminal.updated",
"merchant.created",
"merchant.updated"
]
}
Suscripción a nivel Comercio específico
POST /subscriptions HTTP/1.1
Host: dev-partner-api-wuzi.paymentnexus.com.mx
Content-Type: application/json
Authorization: Bearer <TU_TOKEN>
{
"tenant_id": "tenant_wuzi",
"scope_kind": "merchant",
"merchant_id": "TECMERT000001",
"endpoint_url": "https://api.tu-servidor.com/webhooks/nexus",
"event_types": [
"sale.created",
"sale.approved",
"sale.declined"
]
}
Respuesta de creación (incluye el secreto de firma):
{
"id": "sub_d7cc813e-4d9f-4ec0-93c9-f5c27c99fd6f",
"tenant_id": "tenant_wuzi",
"scope_kind": "tenant",
"endpoint_url": "https://api.tu-servidor.com/webhooks/nexus",
"secret": "sec_wuzi_d7a8f3b20c914e6e",
"event_types": [
"sale.created",
"sale.approved",
"sale.declined",
"sale.cancelled",
"sale.refunded"
],
"active": true,
"created_at": "2026-09-18T21:40:00Z",
"updated_at": "2026-09-18T21:40:00Z"
}
El campo secret se retorna únicamente al momento de la creación (o tras rotarlo explícitamente mediante POST /subscriptions/{id}/rotate-secret). No podrá ser consultado posteriormente en texto plano.
5. Forma de la solicitud y encabezados
Cada entrega es un POST HTTP con Content-Type: application/json.
Encabezados HTTP (Headers)
| Header | Descripción |
|---|---|
X-PNX-Signature-V1 | Firma HMAC-SHA256 estándar calculada sobre los metadatos de la entrega y el cuerpo exacto. |
X-PNX-Timestamp | Marca de tiempo ISO 8601 / RFC 3339 en UTC cuando se firmó la entrega. |
X-PNX-Delivery-Id | Identificador único de este intento de entrega. |
X-PNX-Event-Id | Identificador único del evento origen (usar para deduplicar). |
X-PNX-Event-Type | Tipo de evento público (ej. sale.approved). |
X-PNX-Event-Version | Versión del contrato de evento (v1). |
X-PNX-Tenant-ID | Tenant origen (tenant_wuzi). |
X-PNX-Signature | (Legacy) Firma HMAC del cuerpo para retrocompatibilidad. |
6. Estructura de los Payloads
Ejemplo de evento de creación de venta (sale.created):
{
"id": "evt_txn_1785427916_created",
"type": "sale.created",
"event_version": "v1",
"occurred_at": "2026-09-18T21:45:00Z",
"tenant_id": "tenant_wuzi",
"merchant_id": "TECMERT000001",
"data": {
"sale_id": "txn_1785427916",
"display_transaction_id": "TXN-001048",
"display_reference": "REF-992104",
"merchant_id": "TECMERT000001",
"branch_id": "SUC001",
"terminal_id": "TERM_001",
"operation": "sale",
"channel": "tpv",
"status": "pending",
"amount_minor": 25000,
"currency": "MXN",
"created_at": "2026-09-18T21:45:00Z"
}
}
Ejemplo de evento de ciclo de vida (sale.approved, sale.declined, sale.cancelled, sale.refunded):
{
"id": "evt_txn_1785427916_lifecycle_approved",
"type": "sale.approved",
"event_version": "v1",
"occurred_at": "2026-09-18T21:45:08Z",
"tenant_id": "tenant_wuzi",
"merchant_id": "TECMERT000001",
"data": {
"sale_id": "txn_1785427916",
"display_transaction_id": "TXN-001048",
"display_reference": "REF-992104",
"merchant_id": "TECMERT000001",
"branch_id": "SUC001",
"terminal_id": "TERM_001",
"operation": "sale",
"channel": "tpv",
"status": "approved",
"lifecycle_type": "approved",
"amount_minor": 25000,
"currency": "MXN"
}
}
Ejemplo de evento de terminal (terminal.created / terminal.updated):
{
"id": "evt_term_001_created",
"type": "terminal.created",
"event_version": "v1",
"occurred_at": "2026-09-18T20:10:00Z",
"tenant_id": "tenant_wuzi",
"merchant_id": "TECMERT000001",
"data": {
"terminal_id": "TERM_001",
"merchant_id": "TECMERT000001",
"status": "active",
"created_at": "2026-09-18T20:10:00Z",
"updated_at": "2026-09-18T20:10:00Z"
}
}
Ejemplo de evento de comercio (merchant.created / merchant.updated):
{
"id": "evt_mer_001_updated",
"type": "merchant.updated",
"event_version": "v1",
"occurred_at": "2026-09-18T19:30:00Z",
"tenant_id": "tenant_wuzi",
"merchant_id": "TECMERT000001",
"data": {
"merchant_id": "TECMERT000001",
"status": "active",
"created_at": "2026-09-10T15:00:00Z",
"updated_at": "2026-09-18T19:30:00Z"
}
}
Por regulación y seguridad estricta, el payload público de los webhooks NUNCA incluye:
- Datos de tarjeta (PAN, CVV/CVC, Fecha de expiración, Track 2, PIN block).
- Llaves criptográficas (DUKPT, IPEK, KSN, llaves maestras de tokenización).
- Identificadores de hardware físico de terminal (IMEI, ICCID o números de serie).
- Respuestas ISO 8583 o payloads crudos del procesador adquirente.
- Datos bancarios de dispersión ni secretos de API.
7. Verificar la firma (X-PNX-Signature-V1)
Para garantizar que la petición proviene genuinamente de Payment Nexus y no ha sido alterada, debes validar el encabezado X-PNX-Signature-V1.
Algoritmo de validación:
- Obtén el cuerpo crudo en bytes (
rawBody) antes de parsearlo como JSON. - Extrae los encabezados
X-PNX-Timestamp,X-PNX-Delivery-Id,X-PNX-Event-IdyX-PNX-Signature-V1. - Valida que el
Timestampno tenga más de 5 minutos de diferencia con tu reloj local (previene ataques de repetición / replay attacks). - Calcula el hash SHA-256 en hexadecimal del cuerpo crudo:
bodyHash = sha256(rawBody) - Concatena la cadena de firma:
signingInput = "v1." + timestamp + "." + deliveryId + "." + eventId + "." + bodyHash - Calcula el HMAC-SHA256 de
signingInputusando tu secreto de suscripción (secret). - Compara la firma calculada con
X-PNX-Signature-V1utilizando una comparación de tiempo constante (timing safe).
Implementación en Node.js / TypeScript
import crypto from "crypto";
export function isValidSignatureV1(
secret: string,
rawBody: Buffer | string,
headers: Record<string, string | undefined>,
maxSkewMs = 5 * 60 * 1000
): boolean {
const timestamp = headers["x-pnx-timestamp"];
const deliveryId = headers["x-pnx-delivery-id"];
const eventId = headers["x-pnx-event-id"];
const signature = headers["x-pnx-signature-v1"];
if (!timestamp || !deliveryId || !eventId || !signature) {
return false;
}
// Prevenir replay attacks verificando la ventana de tiempo
const signedAt = Date.parse(timestamp);
if (!Number.isFinite(signedAt) || Math.abs(Date.now() - signedAt) > maxSkewMs) {
return false;
}
// Hash del cuerpo exacto recibido
const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex");
const signingInput = `v1.${timestamp}.${deliveryId}.${eventId}.${bodyHash}`;
const expected = crypto.createHmac("sha256", secret).update(signingInput).digest("hex");
const expectedBuf = Buffer.from(expected, "hex");
const receivedBuf = Buffer.from(signature, "hex");
if (expectedBuf.length !== receivedBuf.length) {
return false;
}
return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}
Implementación en Python
import hashlib
import hmac
from datetime import datetime, timezone, timedelta
def is_valid_signature_v1(
secret: str,
raw_body: bytes,
headers: dict,
max_skew_seconds: int = 300
) -> bool:
timestamp = headers.get("X-PNX-Timestamp")
delivery_id = headers.get("X-PNX-Delivery-Id")
event_id = headers.get("X-PNX-Event-Id")
signature = headers.get("X-PNX-Signature-V1")
if not timestamp or not delivery_id or not event_id or not signature:
return False
signed_at = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
if abs(datetime.now(timezone.utc) - signed_at) > timedelta(seconds=max_skew_seconds):
return False
body_hash = hashlib.sha256(raw_body).hexdigest()
signing_input = f"v1.{timestamp}.{delivery_id}.{event_id}.{body_hash}".encode("utf-8")
expected = hmac.new(secret.encode("utf-8"), signing_input, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Si tu framework parsea el body a un objeto JSON antes de calcular la firma, los cambios en orden de propiedades o espacios en blanco invalidarán la firma. Asegúrate de capturar el raw body (bytes o buffer original).
8. Ciclo de vida de entregas, reintentos e idempotencia
Estados de una entrega (webhook_deliveries):
| Estado | Significado |
|---|---|
pending | Evento encolado, listo para su primer intento de entrega. |
dispatching | Envío HTTP en progreso hacia tu endpoint. |
delivered | Tu servidor respondió con un código HTTP exitoso (2xx). |
failed | El intento falló (error 4xx/5xx, timeout o conexión caída). Se programará un reintento automático. |
Política de reintentos
Si tu endpoint no devuelve 2xx, el despachador ejecuta reintentos automáticos con retroceso exponencial (1m, 2m, 4m, 8m, 16m... hasta un tope de 2 horas entre intentos) hasta un máximo de 8 intentos. Tras agotarlos, la entrega se marca como fallida terminal.
Idempotencia en tu receptor
Debes diseñar tu manejador de webhooks para ser idempotente:
- Guarda el
X-PNX-Event-Id(o campoiddel JSON) en tu base de datos antes de procesar la lógica de negocio. - Si recibes un evento con un
Event-Idya procesado previamente, responde de inmediato con200 OKsin duplicar transacciones, créditos o registros contables.