Skip to main content
Chaque webhook est signé. Vérifiez systématiquement la signature avant de traiter une requête : c’est ce qui garantit que la livraison vient bien de Le Commis et n’a pas été altérée. Une requête dont la signature est invalide doit être rejetée et ignorée.

En-têtes reçus

Le Commis ajoute ces en-têtes à chaque POST :
En-têteDescription
X-LeCommis-SignatureSignature HMAC au format sha256=<hex>.
X-LeCommis-TimestampDate d’émission, en secondes epoch. Sert à reconstruire la charge signée et à détecter le rejeu.
X-LeCommis-DeliveryIdentifiant unique de la livraison (ex. whd_3f9a...). Sert à l’idempotence.
User-AgentLeCommis-Webhook/1.0.
Content-Typeapplication/json.

Comment la signature est calculée

La signature est un HMAC-SHA256, encodé en hexadécimal et préfixé sha256=. La charge signée combine le timestamp et le corps brut de la requête :
signed_payload = "{X-LeCommis-Timestamp}.{corps brut JSON}"
signature      = "sha256=" + HMAC_SHA256(signing_secret, signed_payload)  // en hexadécimal
Utilisez le corps brut (raw body) tel qu’il a été reçu, pas une version re-sérialisée après parsing JSON. Le moindre changement d’espaces ou d’ordre des clés casserait la comparaison. Lisez le corps brut avant que votre framework ne le parse.

Vérification pas à pas

1

Récupérez le corps brut et les en-têtes

Lisez le corps de la requête en chaîne brute, plus X-LeCommis-Signature et X-LeCommis-Timestamp.
2

Reconstituez la charge signée

Concaténez "{timestamp}.{corps brut}".
3

Calculez le HMAC attendu

HMAC_SHA256(signing_secret, charge_signée) encodé en hexadécimal, préfixé sha256=.
4

Comparez à temps constant

Comparez votre valeur à X-LeCommis-Signature avec une comparaison à temps constant (crypto.timingSafeEqual). N’utilisez jamais ==.
5

Rejetez les requêtes trop anciennes

Si |now - timestamp| > 5 min, rejetez la requête : c’est probablement un rejeu.

Exemple de vérification

Node.js (Express)
import express from "express";
import crypto from "node:crypto";

const SIGNING_SECRET = process.env.LECOMMIS_SIGNING_SECRET;
const TOLERANCE = 5 * 60; // secondes

const app = express();

// Important : récupérer le corps BRUT, pas du JSON re-sérialisé.
app.post(
  "/webhooks/lecommis",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body; // Buffer
    const signature = req.get("X-LeCommis-Signature") || "";
    const timestamp = req.get("X-LeCommis-Timestamp") || "";

    // Anti-rejeu.
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE) {
      return res.status(401).send("stale timestamp");
    }

    const signedPayload = `${timestamp}.${rawBody.toString("utf8")}`;
    const digest = crypto
      .createHmac("sha256", SIGNING_SECRET)
      .update(signedPayload)
      .digest("hex");
    const expected = `sha256=${digest}`;

    const ok =
      expected.length === signature.length &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));

    if (!ok) return res.status(401).send("invalid signature");

    // Signature valide -> répondre vite, traiter en async.
    res.status(200).send("ok");
  }
);
Si la signature est invalide ou le timestamp trop ancien, répondez 401 et ignorez la requête. Ne faites aucun traitement, ne re-fetch rien : une requête non signée ou périmée ne doit jamais déclencher d’action.

Le secret de signature

Le signing_secret est propre à chaque endpoint webhook, configuré dans la page « réglages API » de l’établissement. Vous pouvez le régénérer (regenerate_secret) à tout moment depuis ces mêmes réglages API — pensez alors à mettre à jour la valeur côté votre serveur. Stockez-le comme un secret (variable d’environnement, coffre-fort), jamais en clair dans le code.
Régénérer le secret depuis les réglages API invalide immédiatement l’ancienne valeur : les livraisons signées avec l’ancien secret échoueront jusqu’à ce que votre serveur utilise le nouveau. Régénérez quand vous suspectez une fuite, puis déployez la nouvelle valeur sans délai.