Saltar al contenido principal

Empezar con webhooks

Esta guía cubre el setup mínimo para empezar a recibir eventos de Max Pay en tu sistema: crear un webhook, recibir el secret, verificar la firma HMAC, hacer test delivery y manejar deduplicación.

Para entender el modelo y la semántica, ver Webhooks (conceptos). Para la referencia operativa completa (rotación de secret, pausar/reanudar, diagnóstico), ver Configuración de webhooks.

1. Crear el webhook

Configurá un endpoint HTTPS público en tu sistema. Después registralo:

POST /v1/webhooks
Idempotency-Key: setup-erp-prod-001
Content-Type: application/json

{
"url": "https://erp.distribuidora-demo.com/maxpay/events",
"events": [
"receivable-account.activated",
"receivable.paid",
"receivable.settled",
"movement.created",
"settlement.completed"
]
}

La respuesta incluye el secret una sola vez:

201 Created

{
"id": 301,
"url": "https://erp.distribuidora-demo.com/maxpay/events",
"events": [...],
"status": "ACTIVE",
"secret": "whsec_aB3xY9mNpQ2rTzV5wK7jH8nM4fdK",
...
}
Guardá el secret ahora

El secret solo se devuelve en el response de creación o rotación. Después solo vas a ver secretMaskedTail. Si lo perdés, hay que rotar (ver Configuración → Rotar secret).

2. Verificar la firma HMAC

Cada entrega llega con X-MaxPay-Signature: t=<timestamp>,v1=<hmac>. Tu endpoint debe recalcular el HMAC y rechazar la entrega si no matchea.

const crypto = require('crypto');

function verifyMaxpayWebhook(secret, signatureHeader, rawBody) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(s => s.split('='))
);
const timestamp = parts.t;
const signature = parts.v1;

// 1. Rechazar timestamps viejos (replay attack)
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp, 10));
if (age > 300) return false;

// 2. Recalcular HMAC sobre `<timestamp>.<body>`
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

// 3. Comparar en tiempo constante
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex')
);
}
Usar el body crudo, no el JSON parseado

El HMAC se calcula sobre el cuerpo HTTP exacto que envió Max Pay. Si tu framework parsea el JSON antes de pasártelo, perdés bytes (orden de campos, espacios). Configurá el middleware para guardar el rawBody.

3. Hacer un test delivery

Para validar que tu endpoint funciona antes de exponerlo a tráfico real:

POST /v1/webhooks/301/deliveries
Content-Type: application/json

{
"eventType": "webhook.test"
}

Max Pay envía un evento webhook.test a tu URL y te devuelve el resultado. Si tu endpoint responde 2xx y la firma verifica, el setup está listo.

4. Manejar deduplicación

Por el modelo "at-least-once", podés recibir el mismo evento más de una vez. El event.id es único e inmutable entre reintentos del mismo evento.

async function handleEvent(req, res) {
if (!verifyMaxpayWebhook(secret, req.headers['x-maxpay-signature'], req.rawBody)) {
return res.status(401).end();
}

const event = req.body;

// Idempotencia: si ya procesamos este event.id, devolvemos 200 sin reprocesar
const seen = await db.events.findOne({ eventId: event.id });
if (seen) return res.status(200).end();

// Responder rápido, procesar en background
await db.events.insert({ eventId: event.id, type: event.type, receivedAt: new Date() });
res.status(200).end();

// Procesamiento async
await processEvent(event);
}

Checklist de listo para producción

  • Endpoint HTTPS público con certificado válido.
  • Body crudo disponible para verificar la firma.
  • Verificación HMAC implementada y rechazo de timestamps viejos.
  • Deduplicación por event.id.
  • Respuesta 2xx rápida (procesamiento async si la lógica es pesada).
  • Logging del X-MaxPay-Delivery-Id para correlacionar con soporte.
  • Plan para manejar rollbacks (movement.created con operationType: ROLLBACK).
  • Suscripción al evento receivable-account.cvu-failed si emitís cuentas recaudadoras.

Próximos pasos