Ir al contenido
FINORA Academy
SistemaFINORA Checkout

FINORA Checkout · Guía

Verifica la firma del aviso

ImplementadoDocumentado

Cada aviso de FINORA llega con la cabecera X-Finora-Signature. Si tu servidor no la verifica, cualquiera que conozca tu webhookUrl puede marcar pedidos como pagados. Esta guía deja la verificación funcionando.

La cabecera tiene tres partes separadas por coma:

X-Finora-Signature: t=1759590000,v1=5e0c…,v0=9a41…
  • t: el momento del envío, en segundos desde 1970.
  • v1: HMAC-SHA256(secreto, t + "." + cuerpo), en hexadecimal. Es la que verificas.
  • v0: la firma del formato anterior, solo sobre el cuerpo. Se mantiene por compatibilidad; no la uses en código nuevo.
  1. Lee el cuerpo crudo de la petición, tal como llegó, en bytes. No lo conviertas a objeto y de vuelta a texto: cualquier cambio de espacios u orden rompe la firma.

  2. Separa t y v1 de la cabecera. Si falta alguno, rechaza con 401.

  3. Compara t con tu reloj. Si la diferencia es mayor de 300 segundos, rechaza: es un aviso viejo o reenviado por alguien más. FINORA firma cada reintento con un t nuevo, así que un reintento legítimo nunca falla por esto.

  4. Calcula HMAC-SHA256 con tu secreto sobre el texto t + . + cuerpo crudo y pásalo a hexadecimal.

  5. Compara tu resultado con v1 usando una comparación de tiempo constante. Si no coinciden, rechaza con 401.

  6. Si X-Finora-Entrega ya la procesaste, responde 200 sin hacer nada más: es un reintento del mismo aviso.

  7. Procesa el aviso y responde 2xx en menos de 10 segundos. Si tu proceso es largo, encólalo y responde primero.

import crypto from 'node:crypto';
export function firmaValida(secreto, cuerpoCrudo, cabecera, ahora = Date.now() / 1000) {
const partes = Object.fromEntries((cabecera ?? '').split(',').map((p) => p.trim().split('=')));
const t = Number(partes.t);
if (!Number.isFinite(t) || !partes.v1) return false;
if (Math.abs(ahora - t) > 300) return false;
const esperada = crypto.createHmac('sha256', secreto).update(`${t}.`).update(cuerpoCrudo).digest('hex');
const a = Buffer.from(partes.v1);
const b = Buffer.from(esperada);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Con Express, el cuerpo crudo se obtiene con express.raw en esta ruta.
app.post('/webhooks/finora', express.raw({ type: 'application/json' }), manejarAviso);

Comprueba tu código con el vector de prueba

Sección titulada «Comprueba tu código con el vector de prueba»

Antes de conectarte a FINORA, verifica que tu función calcula lo mismo que nosotros. Con estos datos, tu HMAC debe dar exactamente este resultado (llama a tu función pasando ahora = 1700000000 para que la tolerancia no la rechace):

DatoValor
Secretokey
CuerpoThe quick brown fox jumps over the lazy dog
t1700000000
v1 esperado2f658d6aef4f246e91cd741bbcded7479e9605f9d41c9e248122a117e0e1765b
v0 esperado (formato anterior)f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8

Desde una terminal puedes calcularlo con OpenSSL:

Ventana de terminal
printf '%s' "1700000000.The quick brown fox jumps over the lazy dog" | openssl dgst -sha256 -hmac key

Después, pide a FINORA un aviso de prueba real a tu endpoint. La respuesta incluye la firma y el cuerpo exactos que se enviaron:

Ventana de terminal
curl -s -X POST "<URL_DEL_MERCHANT>/api/v1/checkout/webhooks/prueba" \
-H "X-API-Key: <TU_CLAVE_API>" -H "Content-Type: application/json" \
-d '{"url": "https://mitienda.example/webhooks/finora"}'
SíntomaCausa más comúnArreglo
Falla siempre, también con el vectorOlvidaste el t. delante del cuerpoFirma t + "." + cuerpo, no solo el cuerpo
El vector pasa, los avisos reales fallanTu framework ya interpretó el JSON y firmas el texto regeneradoLee el cuerpo crudo en esa ruta
Fallaba después de regenerar el secretoTu servidor sigue con el secreto anteriorActualiza la variable de entorno; FINORA firma los reintentos con el secreto vigente
Falla de forma intermitenteEl reloj de tu servidor está desfasadoSincroniza la hora con NTP