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.
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
401ou400avec « 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 :
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-Timestampet 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
-
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 :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, utilisezrequest.get_data(). -
Obtenez le bon secret. Appelez Récupérer un webhook, qui renvoie le
secretactuel. 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. -
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.
-
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.
-
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.