Ir al contenido

Seguridad de webhooks

Ver como Markdown Lee esta página como texto plano Abrir en ChatGPT Haz preguntas sobre esta página Abrir en Claude Haz preguntas sobre esta página Abrir en Perplexity Haz preguntas sobre esta página

Cada webhook que envía Verificamex Firma va firmado. Validar esa firma te asegura tres cosas:

  • Lo envió Verificamex Firma, y no alguien que conoce la URL de tu webhook.
  • Nadie modificó el contenido en el camino.
  • No es un aviso viejo que alguien capturó y volvió a enviar.

No necesitas activar nada: la firma viene en todos los avisos. Validarla es opcional, pero te lo recomendamos en cualquier integración en producción, sobre todo si un aviso dispara acciones importantes en tu sistema (liberar un pago, activar una cuenta, entregar un producto).

Aplica a todos los webhooks: los de documentos y los de contenedores.


  1. Inicia sesión en el panel de Verificamex Firma.
  2. Entra a Configuración → API desarrolladores.
  3. En la sección Clave de webhooks, presiona Copiar.

La clave empieza con whsec_ seguida de 64 caracteres. Se genera sola la primera vez; no hay que solicitarla.

Cada cuenta tiene su propia clave, y es distinta de tu API key.

  • Documentos: se firman con la clave de la cuenta que creó el documento, es decir, la dueña de la API key con la que hiciste la petición. Si trabajas en una carpeta compartida, usa tu propia clave, no la del dueño de la carpeta.
  • Si el documento no tiene registrado quién lo creó, se usa la clave de la cuenta dueña de la carpeta de trabajo.
  • Contenedores: se firman con la clave de la cuenta dueña del contenedor.

Además del cuerpo JSON, cada aviso trae estos encabezados:

EncabezadoEjemploQué es
X-Verificamex-Firma-Signaturet=1760306642,v1=672644ed…01fe1097t es el momento del envío en segundos Unix. v1 es la firma: un HMAC-SHA256 en hexadecimal (64 caracteres en minúsculas).
X-Verificamex-Firma-Event-Idac110005-a022-1c20-81a0-22ba06f60011Identificador del aviso. Es el mismo en todos los reintentos de un aviso, así que te sirve para detectar duplicados.
Content-Typeapplication/jsonFormato del cuerpo.

  1. Lee el cuerpo tal cual llegó, como texto o bytes, antes de convertirlo a JSON.
  2. Separa t y v1 del encabezado X-Verificamex-Firma-Signature.
  3. Arma el texto firmado: el valor de t, un punto y el cuerpo. Por ejemplo: 1760306642.{"id":"…","signed":3,…}.
  4. Calcula un HMAC-SHA256 de ese texto usando tu clave completa, incluido el whsec_, como llave. Exprésalo en hexadecimal.
  5. Compara tu resultado con v1 usando una comparación de tiempo constante (las funciones de los ejemplos ya lo hacen).
  6. Revisa la antigüedad: si t tiene más de 5 minutos de diferencia con tu hora actual, descarta el aviso.
  7. Si todo coincide, responde 2xx y procesa el aviso. Si no, responde 401 y descártalo.

Cada ejemplo trae una función de validación que puedes copiar tal cual, y su uso en un servidor.

const crypto = require('crypto');
const TOLERANCE_SECONDS = 300; // 5 minutos
function verifyVerificamexFirmaSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader) return false;
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('='))
);
const timestamp = Number(parts.t);
if (!Number.isInteger(timestamp) || !parts.v1) return false;
// Rechaza avisos viejos que alguien haya capturado y reenviado.
if (Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(rawBody) // el cuerpo tal cual llegó (Buffer o string)
.digest();
const received = Buffer.from(parts.v1, 'hex');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}

Con Express, usa express.raw en la ruta del webhook para recibir el cuerpo sin convertir:

const express = require('express');
const WEBHOOK_SECRET = process.env.VERIFICAMEX_FIRMA_WEBHOOK_SECRET; // whsec_...
const app = express();
app.post('/webhooks/verificamex-firma', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.get('X-Verificamex-Firma-Signature');
if (!verifyVerificamexFirmaSignature(req.body, signature, WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const document = JSON.parse(req.body.toString('utf8'));
const eventId = req.get('X-Verificamex-Firma-Event-Id');
res.sendStatus(200);
// Procesa `document`. Usa `eventId` para ignorar avisos repetidos.
});

Con estos datos, tu cálculo del paso 4 debe dar exactamente el valor de v1:

DatoValor
Clavewhsec_test
t1760306642
Cuerpo{"id":"doc-1","signed":3}
v1 esperado672644edb5a94127822b5390b7714887895895d01e320718872dbb4c01fe1097

También puedes calcularlo desde la terminal:

Ventana de terminal
printf '%s' '1760306642.{"id":"doc-1","signed":3}' | openssl dgst -sha256 -hmac 'whsec_test'

  • Cada reintento se firma de nuevo, con un t actualizado. Un reintento legítimo nunca se rechaza por antigüedad.
  • X-Verificamex-Firma-Event-Id se repite en los reintentos de un mismo aviso. Guárdalo y, si ya lo procesaste, responde 200 sin volver a procesarlo.
  • Si respondes con un error, por ejemplo 401 por firma inválida, el aviso se reintenta hasta completar 3 intentos en total.

Si crees que tu clave se filtró, genera una nueva en Configuración → API desarrolladores → Regenerar clave de webhooks.