Vérifier la signature d'un webhook
Cette page s'adresse à la personne qui développe la réception des webhooks chez vous. Vérifier la signature garantit que la requête vient bien de la plateforme, qu'elle n'a pas été modifiée et qu'elle n'est pas rejouée.
La requête
Chaque envoi est un POST au format JSON, avec ces en-têtes :
| En-tête | Contenu |
|---|---|
X-SaasCore-Event | le type d'événement, par exemple member.invited |
X-SaasCore-Delivery | l'identifiant de l'envoi, le même à chaque nouvelle tentative |
X-SaasCore-Signature | la signature, voir ci-dessous |
Le corps :
{
"id": "identifiant de l'envoi (identique à X-SaasCore-Delivery)",
"type": "member.invited",
"version": 1,
"createdAt": "2026-09-24T08:30:00.000Z",
"data": { "email": "…", "organizationName": "…", "inviterEmail": "…" }
}
L'en-tête de signature
X-SaasCore-Signature: t=1790000000,v1=5257a869e7…
test l'horodatage de l'envoi, en secondes depuis le 1er janvier 1970 (UTC) ;v1est une signature HMAC-SHA256, en hexadécimal, calculée avec votre secret sur la chaîne<t>.<corps brut>(l'horodatage, un point, puis le corps exact reçu).
Pendant une rotation du secret, l'en-tête porte
deux valeurs v1 : une par secret. Il suffit que l'une corresponde.
Vérifier
- Lisez
tet toutes les valeursv1de l'en-tête. - Refusez la requête si
ts'écarte de plus de 5 minutes de votre horloge : c'est la protection contre le rejeu. - Calculez HMAC-SHA256(secret,
t+.+ corps brut), en hexadécimal. - Comparez-le à chaque
v1avec une comparaison à temps constant. Acceptez si l'une correspond.
Calculez la signature sur le corps tel qu'il est reçu, avant toute transformation : un JSON relu puis réécrit ne donne plus la même signature.
Exemple en Node.js :
import { createHmac, timingSafeEqual } from "node:crypto";
function verifier(corpsBrut, entete, secret, tolerance = 300) {
let t = NaN;
const v1s = [];
for (const partie of entete.split(",")) {
const [cle, valeur] = partie.split("=");
if (cle === "t") t = Number(valeur);
if (cle === "v1") v1s.push(valeur);
}
if (!Number.isFinite(t) || v1s.length === 0) return false;
if (Math.abs(Date.now() / 1000 - t) > tolerance) return false;
const attendu = createHmac("sha256", secret).update(`${t}.${corpsBrut}`).digest();
return v1s.some((v1) => {
const recu = Buffer.from(v1, "hex");
return recu.length === attendu.length && timingSafeEqual(recu, attendu);
});
}
Répondre
Répondez par un code 2xx dans les 10 secondes. Toute autre réponse, ou un délai dépassé, compte comme un échec et déclenche une nouvelle tentative (voir Surveiller les envois).
Un même événement peut arriver plus d'une fois, par exemple quand votre réponse
s'est perdue. Toutes les tentatives d'un événement vers un même endpoint portent
le même identifiant d'envoi (id du corps et X-SaasCore-Delivery) : gardez
les identifiants déjà traités et ignorez une requête qui en répète un. Deux
endpoints qui reçoivent le même événement reçoivent deux identifiants différents.