Aller au contenu principal

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êteContenu
X-SaasCore-Eventle type d'événement, par exemple member.invited
X-SaasCore-Deliveryl'identifiant de l'envoi, le même à chaque nouvelle tentative
X-SaasCore-Signaturela 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…
  • t est l'horodatage de l'envoi, en secondes depuis le 1er janvier 1970 (UTC) ;
  • v1 est 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​

  1. Lisez t et toutes les valeurs v1 de l'en-tête.
  2. Refusez la requête si t s'écarte de plus de 5 minutes de votre horloge : c'est la protection contre le rejeu.
  3. Calculez HMAC-SHA256(secret, t + . + corps brut), en hexadécimal.
  4. Comparez-le à chaque v1 avec 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.

Voir aussi​