Aller au contenu principal

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 :

HeaderDescription
Content-Typeapplication/json
User-AgentEnkore.io/1.0
X-Enkore-EventType de l'événement, ex : points_event/created, tier/changed
X-Enkore-DeliveryUUID unique identifiant cet envoi (utile pour la déduplication)
X-Enkore-TimestampTimestamp Unix (secondes) au moment de l'envoi
X-Enkore-SignatureSignature 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 header X-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-2xx déclenchent une nouvelle tentative automatique : jusqu'à 5 tentatives au total, espacées d'environ 1 seconde.
  • Votre endpoint doit retourner un code HTTP 2xx rapidement pour confirmer la réception. Si un traitement long est nécessaire, accusez réception immédiatement et traitez en arrière-plan.
attention

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) ou hash_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 2xx en 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