Saltar al contenido
Docs

Solución de problemas

¿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.

Actualizado el 1 oct 2026

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:

    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, 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. Para el formato del cuerpo, consulta Peticiones de webhook.

¿Sigues teniendo problemas?

Contacta con soporte o pregunta en Discord. Comparte el ID del webhook (wh_…) y el ID de un evento que falle, nunca el secreto.

¿Te ha resultado útil esta página?

Gracias por tu opinión.

Gracias. Leemos todos los mensajes.