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.
Enkore envoie une requête POST en application/json vers chaque URL configurée. Le corps de la
requête est le payload propre à l'événement.
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 (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 votre API key (clé API de votre store, visible dans le
back-office). La signature est transmise dans le header X-Enkore-Signature au format :
X-Enkore-Signature: t=<timestamp>,v1=<hmac_hex>
t: timestamp Unix en secondes (identique au headerX-Enkore-Timestamp)v1: HMAC-SHA256 calculé sur la chaîne<timestamp>.<body_json>, encodé en hexadécimal
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);
}
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. - 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 vous être livré plusieurs fois, y compris à une URL qui avait
déjà répondu 2xx. 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