# ¿Por qué no coincide la firma de mi webhook?

> Soluciona los fallos de verificación de la firma de los webhooks. Firma el cuerpo en bruto con el secreto whsec_ completo y el valor de X-Emailit-Timestamp.

Este artículo te ayuda cuando tu endpoint rechaza los webhooks de Emailit porque la firma que calculas no es igual a la cabecera `X-Emailit-Signature`. En casi todos los casos, los bytes que firmas o el secreto que usas no son los que usó Emailit.

## Síntomas

- Tu gestor devuelve `401` o `400` con «invalid signature» en todos los webhooks, incluidos los eventos de **Send test** del panel.
- Las peticiones de la pestaña **Requests** del webhook muestran intentos fallidos con el código de estado de error de tu endpoint.
- La verificación funciona en una prueba unitaria con un payload hecho a mano, pero falla con las entregas reales.

## Causa

Emailit firma cada petición así:

```text
signature = hex( HMAC-SHA256( secret, "<X-Emailit-Timestamp>.<raw request body>" ) )
```

La firma falla cuando una de las tres entradas es distinta:

- **El cuerpo se volvió a serializar.** Los frameworks suelen analizar el JSON antes de que se ejecute tu gestor. Llamar a `JSON.stringify(req.body)` produce espacios en blanco y un orden de claves distintos de los bytes que envió Emailit. El cuerpo es un **array** JSON de hasta 100 eventos, así que el código que espera un objeto también puede alterarlo.
- **El secreto es incorrecto.** Usa el valor completo, incluido el prefijo `whsec_`. Cada webhook tiene su propio secreto, y restablecerlo invalida el anterior de inmediato, también para los reintentos pendientes.
- **Falta la marca de tiempo o es distinta.** Usa el valor exacto de la cabecera `X-Emailit-Timestamp` y únelo al cuerpo con un solo punto.
- **La comparación es incorrecta.** La cabecera está en hexadecimal en minúsculas, no en Base64, y no lleva el prefijo `sha256=`.
- **Algo delante de tu aplicación modificó el cuerpo**, como un proxy que lo vuelve a codificar o lo descomprime.

## Solución

1. **Lee el cuerpo en bruto.** Captura los bytes antes de que se ejecute cualquier analizador de JSON. En Express, monta `express.raw()` solo en la ruta del webhook:

```javascript title="webhook.js"
import crypto from 'node:crypto';

app.post('/webhooks/emailit', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-Emailit-Timestamp');
  const expected = crypto
    .createHmac('sha256', process.env.EMAILIT_WEBHOOK_SECRET) // whsec_...
    .update(`${timestamp}.${req.body.toString('utf8')}`)
    .digest('hex');
  const received = req.get('X-Emailit-Signature') ?? '';
  const valid = received.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received));
  if (!valid) return res.sendStatus(401);

  const events = JSON.parse(req.body); // an array of events
  res.sendStatus(200);
});
```

   En los route handlers de Next.js, usa `await request.text()`. En Laravel, usa `$request->getContent()`. En Flask, usa `request.get_data()`.

2. **Obtén el secreto correcto.** Llama a [Obtener un webhook](/es/docs/api-reference/webhooks/get/), que devuelve el `secret` actual. En el panel, el secreto solo se muestra una vez, así que, si lo has perdido, abre el webhook, elige **Webhook secret** para restablecerlo y actualiza tu variable de entorno de inmediato.

3. **Envía un evento de prueba.** En la página del webhook, elige **Send test** y selecciona un tipo de evento. La prueba se firma con el mismo secreto, y el cuadro de diálogo muestra el código de estado y el cuerpo de la respuesta de tu endpoint.

4. **Comprueba tu tolerancia de reloj.** Si rechazas las marcas de tiempo antiguas para evitar ataques de repetición, deja un margen de unos minutos y mantén sincronizado el reloj de tu servidor. Cada intento de entrega, incluidos los reintentos, lleva una marca de tiempo nueva.

5. **Reintenta lo que ha fallado.** Cuando la verificación funcione, elige **Retry failed** para volver a enviar las entregas fallidas de los últimos 7 días.

Para código en otros lenguajes, consulta [Firma de las peticiones de webhook](/es/docs/webhooks/request-signature/). Para el formato del cuerpo, consulta [Peticiones de webhook](/es/docs/webhooks/webhook-requests/).

## ¿Sigues teniendo problemas?

[Contacta con soporte](/contact/) o pregunta en [Discord](https://discord.emailit.com). Comparte el ID del webhook (`wh_…`) y el ID de un evento que falle, nunca el secreto.

---
Fuente: https://emailit.com/es/docs/kb/webhook-signature-mismatch/
