Aller au contenu
Docs

Dépannage

Pourquoi la signature de mon webhook ne correspond-elle pas ?

Corrigez les échecs de vérification de signature des webhooks. Signez le corps brut avec le secret whsec_ complet et la valeur de X-Emailit-Timestamp.

Mis à jour le 1 oct. 2026

Cet article vous aide quand votre endpoint rejette les webhooks d’Emailit parce que la signature que vous calculez ne correspond pas à l’en-tête X-Emailit-Signature. Dans presque tous les cas, les octets que vous signez ou le secret que vous utilisez diffèrent de ceux qu’a utilisés Emailit.

Symptômes

  • Votre gestionnaire renvoie 401 ou 400 avec « invalid signature » pour chaque webhook, y compris les événements Send test du tableau de bord.
  • Les requêtes de l’onglet Requests du webhook affichent des tentatives en échec avec le code de statut d’erreur de votre endpoint.
  • La vérification fonctionne dans un test unitaire avec un payload écrit à la main, mais échoue avec les vraies livraisons.

Cause

Emailit signe chaque requête ainsi :

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

La signature échoue lorsque l’une des trois entrées diffère :

  • Le corps a été resérialisé. Les frameworks analysent souvent le JSON avant l’exécution de votre gestionnaire. Appeler JSON.stringify(req.body) produit des espaces et un ordre des clés différents des octets envoyés par Emailit. Le corps est un tableau JSON de 100 événements au maximum : un code qui attend un objet risque donc aussi de le remanier.
  • Le secret est incorrect. Utilisez la valeur complète, préfixe whsec_ compris. Chaque webhook a son propre secret, et sa réinitialisation invalide immédiatement l’ancien, y compris pour les nouvelles tentatives en attente.
  • L’horodatage est absent ou différent. Utilisez la valeur exacte de l’en-tête X-Emailit-Timestamp et joignez-la au corps avec un seul point.
  • La comparaison est incorrecte. L’en-tête est en hexadécimal minuscule, pas en Base64, et n’a pas de préfixe sha256=.
  • Un élément placé devant votre application a modifié le corps, comme un proxy qui le réencode ou le décompresse.

Solution

  1. Lisez le corps brut. Capturez les octets avant l’exécution de tout analyseur JSON. Dans Express, montez express.raw() uniquement sur la route du 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);
    });

    Dans les gestionnaires de route Next.js, utilisez await request.text(). Dans Laravel, utilisez $request->getContent(). Dans Flask, utilisez request.get_data().

  2. Obtenez le bon secret. Appelez Récupérer un webhook, qui renvoie le secret actuel. Dans le tableau de bord, le secret n’est affiché qu’une seule fois : si vous l’avez perdu, ouvrez le webhook, choisissez Webhook secret pour le réinitialiser et mettez immédiatement à jour votre variable d’environnement.

  3. Envoyez un événement de test. Sur la page du webhook, choisissez Send test et sélectionnez un type d’événement. Le test est signé avec le même secret, et la boîte de dialogue affiche le code de statut et le corps de réponse de votre endpoint.

  4. Vérifiez votre tolérance d’horloge. Si vous rejetez les horodatages anciens pour empêcher les rejeux, accordez une marge de quelques minutes et gardez l’horloge de votre serveur synchronisée. Chaque tentative de livraison, nouvelles tentatives comprises, porte un nouvel horodatage.

  5. Relancez ce qui a échoué. Une fois la vérification fonctionnelle, choisissez Retry failed pour renvoyer les livraisons en échec des 7 derniers jours.

Pour du code dans d’autres langages, consultez Signature des requêtes de webhook. Pour le format du corps, consultez Requêtes de webhook.

Le problème persiste ?

Contactez le support ou posez votre question sur Discord. Indiquez l’ID du webhook (wh_…) et l’ID d’un événement en échec, jamais le secret.

Cette page vous a-t-elle été utile ?

Merci pour votre retour.

Merci, nous lisons chaque message.