Integrar webhooks
Esta guía es para el equipo de desarrollo que va a recibir y procesar los webhooks de la plataforma en un sistema propio. Si solo necesitas dar de alta una suscripción desde el panel, consulta Administrar webhooks desde el panel.
Cómo funciona, en resumen
- Das de alta una suscripción (comercio + URL de tu endpoint + un secreto +, opcionalmente, qué tipos de evento quieres).
- Cuando ocurre un evento de ese comercio (una venta, un cambio de estado de terminal, etc.), la plataforma hace un
POSTa tu endpoint con el evento completo en el cuerpo de la solicitud. - Tu endpoint debe responder con un código
2xxpara confirmar que lo recibiste. Cualquier otra respuesta (incluyendo timeouts o errores de conexión) se considera una entrega fallida y se reintenta automáticamente.
Eventos disponibles
Si no seleccionas ningún evento al crear la suscripción, recibes todos los que aplican a tu comercio. Si seleccionas uno o más, solo recibes esos.
Transacciones
| Evento | Cuándo ocurre |
|---|---|
transaction.created | Se creó una transacción (venta, reembolso, cancelación o reversión). |
transaction.status_changed | La transacción cambió de estado (p. ej. de pending a approved). |
transaction.route_resolved | Se resolvió a qué procesador se va a enrutar la transacción. |
transaction.risk_evaluated | Se evaluó el riesgo base de la transacción. |
transaction.fraud_evaluated | Se evaluó el resultado final antifraude. |
transaction.fraud_plugin_evaluated | El motor antifraude (si está activo) emitió su decisión. |
transaction.processor_completed | El procesador respondió (aprobada o declinada). |
fee.calculated | Se calculó (o revirtió, en el caso de un reembolso/cancelación) la comisión de la transacción. |
dispute.opened | Se abrió una disputa sobre una transacción. |
dispute.resolved | Se resolvió una disputa (a favor o en contra del comercio). |
Terminales
| Evento | Cuándo ocurre |
|---|---|
terminal.created | Se registró una terminal nueva. |
terminal.updated | Cambió algún dato de la terminal (marca, modelo, modo de operación, etc.). |
terminal.device_assigned | Se asignó (o reactivó) un dispositivo físico a un comercio desde el backoffice. |
terminal.secure_signin | El dispositivo completó el enrolamiento seguro que establece o renueva su llave de terminal. |
terminal.health_signal_reported | El dispositivo reportó su telemetría de salud/conectividad (batería, señal, red, etc.). |
Comercios
| Evento | Cuándo ocurre |
|---|---|
merchant.created | Se dio de alta un comercio nuevo. |
merchant.updated | Cambió el perfil de un comercio. |
branch.created / branch.updated | Se creó o actualizó una sucursal. |
franchise.created / franchise.updated | Se creó o actualizó una franquicia. |
La plataforma entrega automáticamente cualquier evento nuevo que se agregue en el futuro, siempre que pertenezca a un comercio - no necesitas hacer nada para empezar a recibirlos, salvo que hayas limitado tu suscripción a una lista específica de eventos (en cuyo caso solo verás los que ya seleccionaste, hasta que agregues el nuevo tipo).
Forma de la solicitud
Cada entrega es un POST con Content-Type: application/json. El cuerpo es el evento completo, tal como se generó internamente - no es un resumen ni una versión simplificada.
Headers:
| Header | Qué contiene |
|---|---|
X-PNX-Signature | La firma HMAC-SHA256 del cuerpo exacto de la solicitud (ver Verificar la firma). |
X-PNX-Event-Type | El tipo de evento (p. ej. transaction.created). |
X-PNX-Event-Id | El identificador único del evento origen - úsalo para deduplicar (ver Idempotencia). |
X-PNX-Delivery-Id | El identificador único de este intento de entrega. |
Ejemplo real (transaction.created):
{
"EventID": "evt_txn_1785427916_created",
"EventType": "transaction.created",
"EventVersion": "v1",
"TenantID": "tenant_demo",
"CorrelationID": "corr_txn_1785427916",
"OccurredAt": "2026-07-30T16:11:56.813160715Z",
"Payload": {
"TransactionID": "txn_1785427916",
"MerchantID": "merchant_1782289074",
"TerminalID": "terminal_1782289074",
"OperationType": "sale",
"Channel": "ecommerce",
"Amount": { "Amount": 2500, "Currency": "MXN" },
"Status": "pending"
},
"Metadata": { "command_id": "cmd_3b6709aa7b4665bd", "command_type": "transaction.create" }
}
Cada tipo de evento tiene su propia forma de Payload (una venta trae Amount/Channel, un cambio de terminal trae Brand/Model, etc.) - siempre incluye al menos MerchantID, que es lo que la plataforma usa para decidir a qué suscripciones entregarlo.
Verificar la firma
X-PNX-Signature es el HMAC-SHA256 del cuerpo exacto de la solicitud (los bytes crudos, antes de parsear el JSON), usando el secreto que definiste al crear la suscripción, codificado en hexadecimal. Recalcúlalo y compáralo antes de confiar en cualquier webhook - así confirmas que vino realmente de la plataforma y no de alguien más.
Node.js:
const crypto = require("crypto");
function isValidSignature(secret, rawBody, signatureHeader) {
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
Python:
import hashlib
import hmac
def is_valid_signature(secret: str, raw_body: bytes, signature_header: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)
Si tu framework ya parseó el body a un objeto antes de que puedas firmarlo tú mismo, y luego lo vuelves a convertir a texto para comparar, es fácil que el resultado no sea byte por byte igual al original (orden de llaves, espacios, etc.) y la firma no coincida aunque el webhook sea legítimo. Verifica contra los bytes crudos de la solicitud.
Reintentos y estados de una entrega
| Estado | Qué significa |
|---|---|
pending | Creada, esperando su primer intento. |
dispatching | Intento en curso. |
delivered | Tu endpoint respondió 2xx. |
failed | Tu endpoint respondió algo distinto de 2xx, no respondió, o hubo un error de conexión. |
Una entrega failed se reintenta automáticamente con espera creciente (1, 2, 4, 8... minutos, hasta un máximo de 2 horas entre intentos) hasta un límite de intentos, tras el cual se deja de reintentar. Tu endpoint debe responder rápido (unos segundos) y con 2xx en cuanto reciba la solicitud - si necesitas procesar el evento de forma lenta, hazlo de forma asíncrona después de responder.
Idempotencia
Como cualquier entrega puede reintentarse, tu endpoint debe ser capaz de recibir el mismo evento más de una vez sin duplicar su efecto (por ejemplo, sin registrar dos veces la misma venta). Usa X-PNX-Event-Id (o el campo EventID del cuerpo) como llave de deduplicación - la plataforma nunca reintenta el mismo evento hacia la misma suscripción como una entrega nueva, pero tu propio sistema también podría recibirlo dos veces si tu endpoint respondió 2xx pero la confirmación se perdió en tránsito.
Qué sigue
- Administrar webhooks desde el panel — cómo dar de alta y consultar suscripciones.
- Notificaciones por correo