Livraison et signature des webhooks
Cette page décrit le fonctionnement commun à tous les webhooks Enkore : les headers envoyés,
la signature HMAC, la politique de livraison et les bonnes pratiques de réception. Les pages
dédiées à chaque événement (points_event/created, tier/changed, …) ne documentent que le
contenu de leur payload.
Principe
Un webhook est configuré dans le back-office par couple (URL, type d'événement). Un même store peut enregistrer plusieurs webhooks pour un même événement : chaque URL active reçoit alors sa propre requête. Un webhook désactivé ne reçoit plus rien.
Enkore envoie une requête POST en application/json vers chaque URL active. Le corps de la
requête est le payload propre à l'événement. Chaque URL est livrée indépendamment : l'échec de
l'une n'empêche pas l'envoi aux autres.
Headers envoyés par Enkore
Enkore ajoute les headers suivants à chaque requête webhook :
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | Enkore.io/1.0 |
X-Enkore-Event | Type de l'événement, ex : points_event/created, tier/changed |
X-Enkore-Delivery | UUID unique identifiant cet envoi (utile pour la déduplication) |
X-Enkore-Timestamp | Timestamp Unix (secondes) au moment de l'envoi |
X-Enkore-Signature | Signature HMAC-SHA256 du payload, et l'empreinte de la clé qui l'a produite (voir ci-dessous) |
Le corps contient également un champ event avec la même valeur. Le header
X-Enkore-Event reste la source de vérité recommandée pour router la requête avant même de
désérialiser son contenu.
Signature HMAC
Enkore signe chaque payload avec une de vos clés d'API. Une boutique peut en détenir
plusieurs, mais une seule signe ses webhooks : c'est celle que le back-office marque comme clé
de signature, dans la liste des clés d'API. La signature est transmise dans le header
X-Enkore-Signature au format :
X-Enkore-Signature: t=<timestamp>,v1=<hmac_hex>,kid=<4_derniers_caractères>
t: timestamp Unix en secondes (identique au headerX-Enkore-Timestamp)v1: HMAC-SHA256 calculé sur la chaîne<timestamp>.<body_json>, encodé en hexadécimalkid: les quatre derniers caractères de la clé de signature — les mêmes que ceux affichés dans le back-office. Il ne sert qu'à vous dire laquelle de vos clés vérifier ; ne l'utilisez jamais comme un secret, et ne rejetez pas une requête sur son seul contenu.
Lisez le header par paires clé=valeur séparées par des virgules, et ignorez les champs que
vous ne connaissez pas — c'est ce que font les deux exemples ci-dessous, et c'est ce qui permet
à ce format de s'enrichir sans casser votre intégration.
Le corps signé est la chaîne JSON exacte reçue, avant tout parsing ou reformatage. Si votre
framework désérialise automatiquement le corps, récupérez le corps brut (raw body) pour
calculer la signature — un JSON.stringify du corps parsé ne redonnera pas nécessairement les
mêmes octets.
Vérification — Node.js
const { createHmac, timingSafeEqual } = require('node:crypto');
function verifyEnkoreSignature(rawBody, signatureHeader, apiKey) {
if (!signatureHeader) return false;
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('=')),
);
const timestamp = parts.t;
const received = parts.v1;
if (!timestamp || !received) return false;
// Rejeter les requêtes trop anciennes (protection contre le rejeu)
const ageInSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (ageInSeconds > 300) return false;
const expected = createHmac('sha256', apiKey)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(received, 'hex');
return a.length === b.length && timingSafeEqual(a, b);
}
Vérification — PHP
function verifyEnkoreSignature(string $rawBody, ?string $signatureHeader, string $apiKey): bool
{
if ($signatureHeader === null) {
return false;
}
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = array_pad(explode('=', $part, 2), 2, null);
$parts[$key] = $value;
}
$timestamp = $parts['t'] ?? null;
$received = $parts['v1'] ?? null;
if ($timestamp === null || $received === null) {
return false;
}
// Rejeter les requêtes trop anciennes (protection contre le rejeu)
if (abs(time() - (int) $timestamp) > 300) {
return false;
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $apiKey);
return hash_equals($expected, $received);
}
Changer de clé de signature
La clé de signature ne peut pas être désactivée tant qu'elle signe : c'est ce qui garantit que vos webhooks ne partiront jamais sans signature. Pour la remplacer — parce qu'elle a fuité, ou par simple hygiène :
- générez une seconde clé d'API dans le back-office ;
- faites-en la clé de signature. Les webhooks émis à partir de cet instant sont signés avec
elle : votre vérification doit déjà connaître sa valeur, sans quoi elle les rejettera. Le
kiddu header vous dit lequel des deux secrets est en jeu ; - désactivez l'ancienne clé, une fois que vous avez constaté que les livraisons passent.
Tant que l'étape 2 n'a pas eu lieu, l'ancienne clé continue de signer : le basculement est un geste que vous choisissez, jamais une conséquence de la désactivation d'une autre clé.
Comportement de livraison
- L'envoi est asynchrone : le webhook part peu après l'opération qui l'a déclenché, sans garantie d'ordre d'arrivée par rapport à vos autres flux.
- Enkore attend une réponse dans un délai de 10 secondes. Au-delà, la requête est annulée et traitée comme un échec.
- Toute erreur réseau, tout timeout et toute réponse HTTP non-
2xxdéclenchent une nouvelle tentative automatique : jusqu'à 5 tentatives au total, espacées d'environ 1 seconde. Ces nouvelles tentatives ne visent que les URL qui n'ont pas encore répondu2xxpour l'événement. - Votre endpoint doit retourner un code HTTP
2xxrapidement pour confirmer la réception. Si un traitement long est nécessaire, accusez réception immédiatement et traitez en arrière-plan.
Un même événement métier peut malgré tout vous être livré plusieurs fois. Par exemple, si votre endpoint traite la requête mais répond après le délai de 10 secondes, Enkore la compte en échec et la renvoie. Votre traitement doit donc être idempotent — voir la section suivante.
Déduplication et idempotence
Le header X-Enkore-Delivery contient un UUID unique par envoi : en cas de réessai, un nouvel
UUID est généré. Il identifie donc une tentative de livraison, pas l'événement métier — deux
livraisons du même événement portent deux UUID différents.
Pour rendre votre traitement idempotent, appuyez-vous sur les données du payload qui identifient
l'événement métier lui-même (par exemple le couple customerId + orderId pour un gain de
points lié à une commande), et non sur l'UUID de livraison. Ce dernier reste utile pour tracer un
envoi précis et le signaler au support Enkore.
Bonnes pratiques
- Vérifiez toujours la signature HMAC avant de traiter le payload
- Utilisez
timingSafeEqual(Node.js) ouhash_equals(PHP) pour la comparaison, afin d'éviter les attaques par timing - Rejetez les requêtes dont le timestamp dépasse 5 minutes
- Votre endpoint doit être accessible uniquement en HTTPS
- Répondez
2xxen moins de 10 secondes et déportez le traitement métier en asynchrone - Rendez le traitement idempotent : un même événement peut être livré plusieurs fois