VipterCentro de Ayuda
Desarrolladores

Verificar la firma

Cómo firma Vipter cada webhook, código probado en Node.js, PHP y Python para comprobar la firma, cómo rotar el secreto sin perder eventos y los errores que más hacen fallar la verificación.

Admin o PropietarioTodos los planes

Cualquier persona que descubra la URL de tu endpoint puede enviarle un POST. La firma prueba que el evento vino de Vipter y que el cuerpo no se alteró en el camino. Verifica la firma en cada solicitud, antes de leer el JSON.

Antes de empezar

  • El secreto del endpoint, que empieza con whsec_. Aparece una sola vez, justo después de crear el endpoint o de rotar el secreto. Consulta Recibir eventos en tu sistema.
  • Acceso al cuerpo sin procesar de la solicitud en tu framework, antes de cualquier conversión a JSON.

Cómo se calcula la firma

En cada intento de entrega, Vipter:

  1. Toma la hora actual en segundos Unix: t.
  2. Arma el texto <t>.<cuerpo>: el valor de t, un punto y el cuerpo exactamente como se envía.
  3. Calcula el HMAC SHA-256 de ese texto, con el secreto del endpoint como clave, y escribe el resultado en hexadecimal en minúsculas: 64 caracteres.
  4. Envía el encabezado Vipter-Signature: t=<t>,v1=<hmac>.

La clave del HMAC es el secreto completo, como texto, incluido el prefijo whsec_. No quites el prefijo ni decodifiques el secreto.

Durante la rotación del secreto, el encabezado trae un v1= por cada secreto válido, el actual primero:

Vipter-Signature: t=1790604192,v1=<hmac con el secreto nuevo>,v1=<hmac con el secreto anterior>

Para comprobarla, vuelve a calcular el HMAC con tu secreto y compáralo con cada v1=. Basta con que uno coincida. Después, rechaza una t muy lejos del reloj de tu servidor, para que una solicitud antigua capturada no se pueda repetir. Los ejemplos de abajo aceptan hasta 5 minutos, la misma tolerancia del fragmento que muestra el panel en Fragmento de verificación (Node.js). Como t se recalcula en cada intento, los reintentos no chocan con esa tolerancia.

Código de verificación

Los tres ejemplos se probaron con firmas generadas de la misma forma en que las genera Vipter: cuerpo con acentos, uno y dos v1=, secreto anterior durante la rotación, cuerpo alterado, cuerpo reformateado y t con 10 minutos de retraso. Cada uno es un servidor completo, que responde 200 para una firma válida y 400 para las demás.

La función es la misma del panel. Recibe el cuerpo como texto. El servidor usa solo la biblioteca estándar de Node.js 18 o más reciente.

webhook.mjs
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyVipterSignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return header.split(',').filter((p) => p.startsWith('v1=')).some((p) => {
    const given = Buffer.from(p.slice(3), 'hex');
    return given.length === expected.length / 2 && timingSafeEqual(given, Buffer.from(expected, 'hex'));
  });
}

createServer((req, res) => {
  const chunks = [];
  req.on('data', (c) => chunks.push(c));
  req.on('end', () => {
    const rawBody = Buffer.concat(chunks).toString('utf8');
    if (!verifyVipterSignature(rawBody, req.headers['vipter-signature'] ?? '', process.env.VIPTER_WEBHOOK_SECRET)) {
      res.writeHead(400).end('firma inválida');
      return;
    }
    const event = JSON.parse(rawBody);
    // Registra event.id e ignora el evento si ya se procesó.
    res.writeHead(200).end(`ok ${event.type}`);
  });
}).listen(Number(process.env.PORT ?? 8000));

Con Express, usa express.raw({ type: 'application/json' }) en la ruta del webhook y pasa req.body.toString('utf8') a la función, como en el ejemplo de Recibir eventos en tu sistema. En un route handler de Next.js, lee el cuerpo con await request.text().

Probar sin esperar un evento

Para probar tu código localmente, genera una firma con openssl, de la misma forma en que lo hace Vipter, y envíala con curl:

SECRET="$VIPTER_WEBHOOK_SECRET"
URL="http://localhost:8000/"
BODY='{"id":"evt_000000000000000000000000","object":"event","type":"customer.created","created":1790604190,"livemode":false,"api_version":"2026-09-01","data":{"object":{"object":"customer","id":"cust_test","email":"test@example.com","name":"Test Customer","test":true}}}'
T=$(date +%s)
SIG=$(printf '%s' "$T.$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')

curl -sS -X POST "$URL" \
  -H 'Content-Type: application/json' \
  -H "Vipter-Signature: t=$T,v1=$SIG" \
  -H 'Vipter-Event-Id: evt_000000000000000000000000' \
  -H 'Vipter-Event-Type: customer.created' \
  --data-binary "$BODY" -w ' HTTP %{http_code}\n'

Los tres servidores de arriba responden ok customer.created HTTP 200. Con el endpoint publicado, usa Enviar evento de prueba en el panel para la prueba de punta a punta.

Rotar el secreto

El botón Rotar secreto genera un secreto nuevo y lo muestra una sola vez. El secreto anterior sigue valiendo durante 24 horas a partir de la rotación.

Durante esas 24 horas, cada entrega trae dos v1=: uno con el secreto nuevo y otro con el anterior. Como el código de arriba acepta cualquier v1= que coincida, tu servidor sigue aceptando los eventos con el secreto antiguo y pasa a aceptarlos con el nuevo en cuanto cambies la variable de entorno. Para rotarlo sin perder eventos:

  1. Haz clic en Rotar secreto y copia el secreto nuevo.
  2. Actualiza el secreto en tu servidor y publica, dentro de las 24 horas.
  3. Envía un evento de prueba y revisa el estado succeeded en Entregas recientes.

Después de las 24 horas, solo firma el secreto nuevo. Rotar de nuevo dentro de ese plazo invalida en el momento el secreto más antiguo: solo el actual y el inmediatamente anterior valen al mismo tiempo. Una entrega reenviada se firma en el momento del envío, con los secretos válidos en ese momento.

Problemas comunes

  • La firma nunca coincide

    El cuerpo se convirtió a JSON y de vuelta a texto antes de la verificación. Cualquier diferencia, como espacios, el orden de las claves o é en lugar de é, cambia el HMAC. Verifica el cuerpo sin procesar. En Express, un express.json() global consume el cuerpo antes de tu ruta: registra la ruta del webhook antes de él o usa express.raw solo en ella.

  • Coincide en las pruebas, falla con acentos

    El cuerpo se leyó o se convirtió con otra codificación. El HMAC se calcula sobre los bytes UTF-8 del cuerpo. En Node.js, junta los Buffer y convierte con utf8 al final, no fragmento por fragmento.

  • Falla después de algunos minutos, o en todos los eventos de un servidor

    El reloj del servidor está atrasado o adelantado, y t sale de la tolerancia de 5 minutos. Sincroniza el reloj por NTP. Aumentar la tolerancia esconde el problema y debilita la protección contra la repetición.

  • Falla con el secreto correcto

    El secreto es de otro endpoint, se quitó el prefijo whsec_, o hay un espacio o salto de línea al final de la variable de entorno. Cada endpoint tiene su secreto. La tarjeta del endpoint muestra el final del secreto actual para que lo compruebes.

  • Verifica created en lugar de t

    La firma usa la t del encabezado. El created del cuerpo es la hora del evento y no cambia en los reintentos.

  • Compara con ==

    Una comparación común de texto puede filtrar, por el tiempo de respuesta, cuántos caracteres coinciden. Usa timingSafeEqual en Node.js, hash_equals en PHP y hmac.compare_digest en Python.

Qué hacer después

En esta página