Webhooks

Guía de webhooks de pago

Los hooks PAYMENT_PAID notifican pagos de forma asíncrona. Tu receptor debe guardar el aviso de forma durable y responder rápidamente con un código 2xx.

Payload

El contrato 360Pay utiliza un array, incluso para un solo cupón. Un pago de varios documentos puede incluir todos sus cupones en una entrega. El mapeo y los campos adicionales del hook pueden cambiar cada elemento.

[
  {
    "event": "ticket.paid",
    "_id": "ID_DEL_CUPON",
    "external_ref": "ORD-9988",
    "payment_reference": "REFERENCIA_DEL_CUPON",
    "amount": 50,
    "status": "paid",
    "paid_at": "2026-10-16T15:00:00.000Z",
    "operation_number": "OPERACION_DEL_BANCO",
    "bank_tx_id": "BCP"
  }
]

Headers

HeaderUso
X-360Pay-Event-IdIdentificador del evento; úsalo para idempotencia.
X-360Pay-Delivery-IdIdentificador de la entrega.
X-360Pay-Hook-IdHook al que pertenece la configuración de firma.
X-360Pay-TimestampFecha ISO incluida en la firma HMAC.
X-360Pay-SignatureFirma HMAC sha256=<hex>, cuando el hook tiene signing secret.
X-360Pay-AttemptNúmero de intento; no es una clave de idempotencia.

Verificar la firma HMAC

Usa el cuerpo crudo recibido, sin volver a serializar el JSON. La fórmula es HMAC-SHA256 sobre timestamp + "." + rawBody, con el secreto del hook.

import crypto from 'node:crypto';

function verify360PayWebhook(rawBody, headers, signingSecret) {
  const timestamp = headers['x-360pay-timestamp'];
  const signature = headers['x-360pay-signature'];
  if (typeof timestamp !== 'string' || typeof signature !== 'string') return false;
  if (typeof signingSecret !== 'string' || !signingSecret) return false;
  if (!Buffer.isBuffer(rawBody) && typeof rawBody !== 'string') return false;
  const date = Date.parse(timestamp);
  if (!Number.isFinite(date) || Math.abs(Date.now() - date) > 5 * 60 * 1000) return false;
  const expected = crypto.createHmac('sha256', signingSecret)
    .update(timestamp + '.').update(rawBody).digest();
  return signature.split(',').some(part => {
    const match = /^sha256=([a-f0-9]{64})$/i.exec(part.trim());
    if (!match) return false;
    const received = Buffer.from(match[1], 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Algunas integraciones históricas usan un header de firma estático, que puede coexistir con el HMAC. Conserva el mecanismo acordado con tu receptor: ese valor estático no es el signing secret HMAC. El ejemplo anterior verifica únicamente la parte HMAC.

Reintentos e idempotencia

Una entrega puede repetirse por timeout, error de red o respuesta no 2xx. El procesamiento automático contempla hasta cuatro intentos. Guarda X-360Pay-Event-Id y responde 200 ante un evento ya procesado, sin repetir sus efectos.

Una corrección de metadatos bancarios puede producir otro evento sobre el mismo cupón. Concilia por cupón y operación; recibir nuevamente un estado paid no representa otro cobro.

Configuración y secretos

Configura la URL HTTPS desde el portal del negocio. Un partner con el permiso correspondiente puede incluir hooks en POST /partners/v1/businesses. La respuesta devuelve hook_signing_secrets una sola vez; guárdalos por hook_id.

La rotación de partner usa POST /partners/v1/businesses/{businessId}/hooks/{hookId}/rotate-secret. Consulta su contrato en Partner API. El acceso depende del negocio afiliado y los permisos de tu clave. Una API key y un signing secret son credenciales diferentes.

Durante una rotación planificada, las entregas que ya existen pueden conservar el secreto anterior. Coordina la verificación y revisa las entregas pendientes antes de retirarlo. Si el secreto fue comprometido, no mantengas su aceptación.

Historial de entregas

Consulta estado, intentos, respuesta y errores desde el portal del negocio. Un aviso pendiente no es un cobro por repetir; verifica primero la entrega y el estado del cupón.