Verificación HMAC

Todas las notificaciones enviadas por Forpay incluyen un campo signature en el cuerpo del JSON. Este campo contiene una firma HMAC-SHA256 que debes validar para garantizar que la notificación proviene legítimamente de Forpay y no ha sido alterada.

Importante: Si la firma calculada por tu sistema no coincide con el valor recibido en signature, debes rechazar la notificación.

Clave HMAC

Cada empresa recibe una clave HMAC exclusiva para su integración. Esta clave se entrega junto con las credenciales del ambiente correspondiente.

Ejemplo (ambiente de desarrollo):

WGsMT5*****$CAr

Cómo validar la firma

El proceso de validación consiste en:

  1. Concatenar los campos del mensaje en el orden definido según el tipo de webhook y el método de pago.
  2. Cifrar el string resultante usando HMAC-SHA256 con tu clave HMAC.
  3. Codificar el resultado en Base64.
  4. Comparar el valor obtenido con el campo signature del mensaje recibido. Si son iguales, el mensaje es válido.

Ejemplo de implementación (Node.js)

const crypto = require('crypto');

function validateHmac(payload, secretKey, receivedSignature) {
  // 1. Construir el string de concatenación según el tipo de webhook
  const concatenated = buildConcatenatedString(payload);

  // 2. Generar la firma HMAC-SHA256 en Base64
  const computedSignature = crypto
    .createHmac('sha256', secretKey)
    .update(concatenated)
    .digest('base64');

  // 3. Comparar con la firma recibida
  return computedSignature === receivedSignature;
}

Webhook de Activación — Campos a concatenar

Los campos a concatenar varían según el método de pago enrolado por el cliente, identificado por paymentMethod.id.

Tarjeta de Crédito, Débito o Prepago (IDs: 3, 4, 11)

Concatenar los siguientes valores en este orden exacto (sin separadores):

businessPartner.businessPartnerId
businessPartner.identificationDocument.documentNumber
businessPartner.identificationDocument.verificationNumber
customer.internalUserId
customer.identificationDocument.documentNumber
customer.identificationDocument.verificationNumber
product.internalId
product.id                    // Si es null, usar string vacío ""
product.serviceId             // Si es null, usar string vacío ""
product.dateTime
product.state.id
paymentMethod.card.last4digits
subscription.internalId
subscription.externalId

Ejemplo de string a cifrar:

277209349719527202964214368831benja12Z3688ZM2024-04-12T16:10:50.977Z16623527[externalId]

Débito Directo, Fintoc o EPAC Directo (IDs: 5, 13, 14)

Concatenar los siguientes valores en este orden exacto:

businessPartner.businessPartnerId
businessPartner.identificationDocument.documentNumber
businessPartner.identificationDocument.verificationNumber
customer.internalUserId
customer.identificationDocument.documentNumber
customer.identificationDocument.verificationNumber
product.internalId
product.id                    // Si es null, usar string vacío ""
product.serviceId             // Si es null, usar string vacío ""
product.dateTime
product.state.id
paymentMethod.id
paymentMethod.account.number
subscription.internalId
subscription.externalId

EPAC / ForPAC (ID: 6)

Concatenar los siguientes valores en este orden exacto:

businessPartner.businessPartnerId
businessPartner.identificationDocument.documentNumber
businessPartner.identificationDocument.verificationNumber
customer.internalUserId
customer.identificationDocument.documentNumber
customer.identificationDocument.verificationNumber
product.internalId
product.id                    // Si es null, usar string vacío ""
product.serviceId             // Si es null, usar string vacío ""
product.dateTime
product.state.id
paymentMethod.id
paymentMethod.account.number
paymentMethod.account.document.id
paymentMethod.account.document.state.id
subscription.internalId
subscription.externalId

Webhook de Pagos — Campos a concatenar

Para las notificaciones de pago (tanto exitosas como rechazadas), concatenar los siguientes valores:

businessPartner.businessPartnerId
businessPartner.identificationDocument.documentNumber
businessPartner.identificationDocument.verificationNumber
customer.internalUserId
customer.identificationDocument.documentNumber
customer.identificationDocument.verificationNumber
product.internalId
product.id
product.serviceId
payment.id                    // Solo si installment.state.id = 2 (PAGADA)
payment.authorizationCode     // Solo si installment.state.id = 2 (PAGADA)
payment.amount                // Solo si installment.state.id = 2 (PAGADA)
payment.dateTime              // Solo si installment.state.id = 2 (PAGADA)

Nota: Los campos de payment solo se incluyen en la concatenación cuando la cuota está en estado PAGADA (installment.state.id = 2). Para otros estados (rechazada, en proceso, etc.), se omiten.


Implementación completa de referencia (Node.js)

const crypto = require('crypto');

const HMAC_SECRET = 'WGsMT5*****$CAr'; // Tu clave HMAC

/**
 * Valida la firma HMAC de un webhook de activación con Tarjeta (IDs 3, 4, 11)
 */
function validateCardActivation(payload, receivedSignature) {
  const p = payload;
  const pm = p.paymentMethod;
  const sub = p.subscription;

  const str = [
    p.businessPartner.businessPartnerId,
    p.businessPartner.identificationDocument.documentNumber,
    p.businessPartner.identificationDocument.verificationNumber,
    p.customer.internalUserId,
    p.customer.identificationDocument.documentNumber,
    p.customer.identificationDocument.verificationNumber,
    p.product.internalId,
    p.product.id ?? '',
    p.product.serviceId ?? '',
    p.product.dateTime,
    p.product.state.id,
    pm.card.last4digits,
    sub.internalId,
    sub.externalId,
  ].join('');

  const computed = crypto
    .createHmac('sha256', HMAC_SECRET)
    .update(str)
    .digest('base64');

  return computed === receivedSignature;
}

/**
 * Valida la firma HMAC de un webhook de pago
 */
function validatePaymentWebhook(payload, receivedSignature) {
  const p = payload;
  const isPaid = p.installment.state.id === 2;

  const fields = [
    p.businessPartner.businessPartnerId,
    p.businessPartner.identificationDocument.documentNumber,
    p.businessPartner.identificationDocument.verificationNumber,
    p.customer.internalUserId,
    p.customer.identificationDocument.documentNumber,
    p.customer.identificationDocument.verificationNumber,
    p.product.internalId,
    p.product.id,
    p.product.serviceId,
  ];

  if (isPaid) {
    fields.push(
      p.payment.id,
      p.payment.authorizationCode,
      p.payment.amount,
      p.payment.dateTime
    );
  }

  const str = fields.join('');

  const computed = crypto
    .createHmac('sha256', HMAC_SECRET)
    .update(str)
    .digest('base64');

  return computed === receivedSignature;
}