> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waspytech.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verificación de firma

> Cómo validar que un webhook viene realmente de Waspy.

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

```js theme={null}
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

```python theme={null}
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`.
