Skip to main content
Cada delivery incluye un header X-Waspy-Signature:
X-Waspy-Signature: t=1745067296,v1=ab12cd34...
Donde:
  • t = timestamp Unix en segundos cuando Waspy generó la firma para este intento
  • v1 = HMAC-SHA256 hex de <t>.<rawBody> usando tu secret (el que devolvió POST /webhooks al crear la suscripción)
Importante — t vs. edad del payload: t se regenera en cada reintento. Su único propósito es la ventana anti-replay (5 min) sobre la firma. Si querés conocer la edad del payload original (útil para retries que pueden llegar hasta 12h después), leé el header X-Waspy-First-Attempt-At (timestamp Unix en segundos del primer intento de esta delivery). Ese header se preserva a través de todos los reintentos.

Algoritmo

  1. Tomá el body raw (string, antes de parsear JSON).
  2. Parseá el header: extraé t y v1.
  3. Calculá expected = HMAC_SHA256(secret, t + "." + rawBody).
  4. Compará expected con v1 usando una comparación constante (timing-safe).
  5. Verificá que t no sea más viejo de 5 minutos (protege contra replay del intento actual).
  6. Opcional: si querés reglas de validez del payload (por ejemplo, ignorar eventos de más de N horas), validá contra X-Waspy-First-Attempt-At en lugar de t.

Node.js

import crypto from 'node:crypto';

function verifyWaspySignature(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((p) => p.split('=')),
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;

  // Replay protection: reject if older than 5 minutes
  const age = Math.abs(Math.floor(Date.now() / 1000) - parseInt(t, 10));
  if (age > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

// Express ejemplo (con express.raw para preservar bytes exactos)
app.post('/webhook/waspy',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const ok = verifyWaspySignature(
      req.body.toString(),
      req.header('X-Waspy-Signature') || '',
      process.env.WASPY_WEBHOOK_SECRET,
    );
    if (!ok) return res.status(401).end();
    const event = JSON.parse(req.body.toString());
    // ... procesar event
    res.status(200).end();
  },
);

Python

import hmac, hashlib, time

def verify_waspy_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    parts = dict(p.split('=', 1) for p in signature_header.split(','))
    t, v1 = parts.get('t'), parts.get('v1')
    if not t or not v1:
        return False
    if abs(int(time.time()) - int(t)) > 300:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{t}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, v1)

Recomendaciones

  • Respondé 2xx lo antes posible (Waspy considera entregado cualquier 2xx; el body es ignorado).
  • Procesá el evento de forma asíncrona si toma más de unos segundos.
  • Guardá data.id + event para deduplicar entregas repetidas.
  • Si tu firma falla, devolvé 401 para que Waspy registre el error en lastErrorMessage.