Cómo verificar un webhook de Qvard

Una guía rápida sobre el encabezado X-Qvard-Signature: qué es y cómo verificarlo con unas pocas líneas de código.

Qué es una firma

Cuando el estado de un pago cambia, Qvard envía una notificación (webhook) a la dirección en tu configuración de tienda. Cualquiera puede enviar una solicitud a esa dirección, por lo que necesitas una forma de distinguir una notificación real de una falsa.

La firma es esa prueba: un código breve que Qvard calcula a partir del mensaje y tu clave secreta. Solo Qvard y tú conocen la clave, por lo que solo Qvard puede producir el código correcto.

Qué recibes

Cada webhook lleva un encabezado extra y un pequeño cuerpo JSON:

X-Qvard-Signature: t=1760950000,v1=3f2a…c91e {"id":"order_1024","status":"paid"}

t es la hora en que se envió el mensaje (Unix, segundos). v1 es el código. Durante 24 horas después de rotar tu clave secreta, el encabezado lleva dos valores v1: uno para la clave nueva y uno para la antigua.

Los campos id y status se mantienen como antes. Al mismo cuerpo firmado se añaden cuatro campos de importe:

  • amount y currency: el importe y su moneda, tal como los enviaste en la solicitud.
  • amountUsd: el importe contable en USD.
  • rate: unidades de moneda por 1 USD en el momento de crear el pago.
  • Las integraciones anteriores siguen funcionando: leen id y status e ignoran los campos nuevos. La firma se verifica sobre el mismo cuerpo que antes.

Cómo verificarlo

  1. 1
    Toma el cuerpo de la solicitud exactamente como llegó: el texto bruto, antes de cualquier análisis JSON.
  2. 2
    Lee t y cada v1 del encabezado X-Qvard-Signature.
  3. 3
    Calcula HMAC-SHA256 del texto t + "." + cuerpo con tu clave secreta (qvard_sk_…) y escríbelo en hexadecimal minúsculo.
  4. 4
    Si tu resultado coincide con cualquier v1 (compara en tiempo constante), el mensaje es genuino.
  5. 5
    Verifica que t no sea mayor a 5 minutos, para que un mensaje antiguo no pueda ser reproducido.
  6. 6
    Solo entonces usa los datos y confirma el monto pagado para ese pedido en tu propia base de datos.

Código listo

const crypto = require('crypto');

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

// Use the RAW body: express.raw, not express.json
app.post('/qvard-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifyQvardSignature(raw, req.get('X-Qvard-Signature') || '', process.env.QVARD_SECRET_KEY)) {
    return res.sendStatus(400);
  }
  const { id, status } = JSON.parse(raw);
  // Confirm the paid amount for order `id` in your own database before fulfilling it.
  res.sendStatus(200);
});

Durante 24 horas después de rotar tu clave secreta, el encabezado lleva dos valores v1: acepta el webhook si alguno coincide.

Errores comunes

  • Analizar el JSON y serializarlo nuevamente cambia el texto. Siempre usa el cuerpo bruto.
  • Usar la clave pública (qvard_pk_…) en lugar de la clave secreta.
  • Un reloj del servidor desincronizado hace fallar la verificación de 5 minutos. Mantenlo sincronizado.
  • Comparar con == en lugar de una comparación en tiempo constante.

Dónde encontrar tu clave secreta

En el panel: tu tienda, Configuración, Claves API. Guárdalo solo en tu servidor. Si se filtra, rótalo allí: la clave antigua sigue funcionando durante 24 horas para pagos (no para retiros).

Abrir panel

Referencia de la API

Claves antiguas

Si tu tienda aún tiene una clave de formato antiguo (sin el prefijo qvard_sk_), los webhooks también llevan un encabezado x-api-key con esa clave. Después de rotarla, ese encabezado desaparece. Cambia a verificar la firma.

¿Quieres probar la ventana de pago?

Abrir demostración