Webhooks et idempotence des événements
TL; DR
Un bon webhook est signé (HMAC/mTLS), un événement résumé et idempotent livré sur le modèle at-least-once avec un backoff exponentiel et une déduplication chez le destinataire. Négociez une enveloppe ('event _ id', 'type', 'ts', 'version', 'attempt', 'signature'), une fenêtre temporelle (≤5 min), des codes de réponse, des rétroactions, DLQ et un statut d'endpoint.
1) Rôles et modèle de livraison
Expéditeur (vous/fournisseur) : génère un événement, signe, essaie de livrer jusqu'à 2xx, rétrograde à 3xx/4xx/5xx (à l'exception des « n'acceptez pas » explicites), conduit DLQ, donne une API de replay.
Destinataire (partenaire/votre service) : vérifie la signature/fenêtre temporelle, fait le dedup et le traitement idempotent, répond avec le code correct, fournit/status et/ack replay par 'event _ id'.
Garanties : at-least-once. Le destinataire doit être capable de traiter les doublons et les changements d'ordre.
2) Enveloppe de l'événement (envelope)
json
{
"event_id": "01HF7H9J9Q3E7DYT5Y6K3ZFD6M",
"type": "payout.processed",
"version": "2025-01-01",
"ts": "2025-11-03T12:34:56.789Z",
"attempt": 1,
"producer": "payments",
"tenant": "acme",
"data": {
"payout_id": "p_123",
"status": "processed",
"amount_minor": 10000,
"currency": "EUR"
}
}
Champs obligatoires : 'event _ id', 'type', 'version', 't',' attempt '.
Règles d'évolution : nous ajoutons des champs ; suppression/changement de type - uniquement avec la nouvelle version.
3) Sécurité : Signatures et ancrage
3. 1 signature HMAC (recommandée par défaut)
Titres :
X-Signature: v1=base64(hmac_sha256(<secret>, <canonical>))
X-Timestamp: 2025-11-03T12:34:56Z
X-Event-Id: 01HF7...
Chaîne canonique :
<timestamp>\n<method>\n<path>\n<sha256(body)>
Vérification auprès du destinataire :
- abs(now − `X-Timestamp`) ≤ 300s
- 'X-Event-Id'n'a pas été traité précédemment (dedup)
- 'X-Signature 'correspond à une comparaison temporelle sécurisée
3. 2 Dop. mesures prises
mTLS pour les webhooks très sensibles.
IP/ASN allow-list.
DPoP (facultatif) pour sender-constrained si le webhook déclenche des callbacks.
4) Idempotence et déduplication
4. 1 Idempotence de l'événement
L'événement avec le même 'event _ id' ne doit pas changer à nouveau d'état. Destinataire :- Stocke 'event _ id' dans un kesh idempotent (KV/Redis/OBD) sur TTL ≥ 24-72 h ;
- conserve le résultat du traitement (succès/erreur, artefacts) pour le retour répété.
4. 2 Idempotence des commandes (appels en arrière)
Si le webhook force le client à renverser l'API (par exemple, « confirmer payout »), utilisez 'Idempotency-Key' sur le volume REST, stockez le résultat sur le côté du service (exactly-once outcome).
Modèle KV (minimum) :
key: idempotency:event:01HF7...
val: { status: "ok", processed_at: "...", handler_version: "..." }
TTL: 3d
5) Retrai et backoff
Graphique recommandé (exponentiel avec gitter) :- « 5s, 15s, 30s, 1m, 2m, 5m, 10m, 30m, 1h, 3h, 6h, 12h, 24h » (plus loin que les jours N)
- 2xx - succès, arrêter les retraits.
- « 400/ 401/403/404/422 » n'est pas rétroactif si la signature/le format ok (erreur client).
- '429' est une Retry-After ou backoff.
- 5xx/réseau - rétroactif.
Titres de l'expéditeur : 'User-Agent', 'X-Webhook-Producer', 'X-Attempt'.
6) Traitement côté destinataire
Pseudo-ligne :pseudo verify_signature()
if abs(now - X-Timestamp) > 300s: return 401
if seen(event_id):
return 200 // идемпотентный ответ
begin transaction if seen(event_id): commit; return 200 handle(data) // доменная логика mark_seen(event_id) // запись в KV/DB commit return 200
Transactionnalité : l'étiquette « seen » doit être placée atomiquement avec l'effet de l'opération (ou après fixation du résultat) pour éviter un double traitement en cas d'échec.
7) Garanties d'ordre et de snapshots
L'ordre n'est pas garanti. Utilisez 't'et les domaines' seq '/' version 'dans' data 'pour vérifier la pertinence.
Pour les longues tranches/pertes - ajoutez/replay à l'expéditeur et/resync au destinataire (obtenez le snapshot et le delta par la fenêtre temporelle/ID).
8) Statut, replay et DLQ
8. 1 Endpoints de l'expéditeur
'POST/webhooks/replay '- par la liste' event _ id 'ou par la fenêtre temporelle.
'GET/webhooks/events/: id '- afficher le paquet source et l'historique des tentatives.
DLQ : Événements « morts » (limite des retraits épuisée) → stockage séparé, alertes.
8. 2 Endpoints du destinataire
`GET /webhooks/status/:event_id` — `seen=true/false`, `processed_at`, `handler_version`.
'POST/webhooks/ack '- (en option) confirmation du traitement manuel à partir du DLQ.
9) Contrats d'erreur (réponse du destinataire)
http
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Retry-After: 120
X-Trace-Id: 4e3f...
{
"error": "invalid_state",
"error_description": "payout not found",
"trace_id": "4e3f..."
}
Recommandations : retournez toujours un code clair et, si possible, « Retry-After ». Ne retournez pas les détails de sécurité.
10) Surveillance et SLO
Métriques (expéditeur) :- livraison p50/p95, taux de réussite, retrai/événement, drop-rate DLQ, share 2xx/4xx/5xx, fenêtre de retard jusqu'à 2xx.
- verify fail rate (signature/heure), dup-rate, latency handler p95, 5xx.
- Livraison : ≥ 99. 9 % des événements reçoivent 2xx <3 c p95 (après la première tentative réussie).
- Cryptoprotection : validation de la signature ≤ 2-5 ms p95.
- Dedup : 0 effets répétés (exactly-once outcome au niveau du domaine).
11) Sécurité des données et vie privée
Ne pas transmettre le PAN/PII dans le corps du webhook ; utilisez les identifiants et les pulls suivants pour les détails de l'API autorisée.
Masquer les champs sensibles dans les logs ; ne stocker les corps d'événements qu'au minimum, avec TTL.
Crypter le stockage DLQ et le relais.
12) Versioning et compatibilité
Version dans 'version' (enveloppe) et en transit : '/webhooks/v1/payments '.
Nouveaux champs - optionnels ; suppression - seulement après la période 'Sunset'.
Documentez les modifications apportées à la machine-readable changelog (pour les rampes automatiques).
13) Cas de test (chèque UAT)
- Refaire le même 'event _ id' → un effet et '200' en double.
- Signature : clé correcte, clé incorrecte, ancienne clé (rotation), 'X-Timestamp' à l'extérieur de la fenêtre.
- Backoff : le destinataire donne '429' avec 'Retry-After' → une pause correcte.
- Ordre : les événements '... processed' arrivent avant '... created' → traitement/attente correct.
- L'OBD du destinataire a échoué entre l'effet et 'mark _ seen' → l'atomicité/répétition.
- DLQ et replay manuel → une livraison réussie.
- La « tempête » massive (fournisseur de shlets de paquet) → sans perte, les limites n'étouffent pas critique.
14) Mini-extraits
Signature de l'expéditeur (pseudo) :pseudo body = json(event)
canonical = ts + "\n" + "POST" + "\n" + path + "\n" + sha256(body)
sig = base64(hmac_sha256(secret, canonical))
headers = {"X-Timestamp": ts, "X-Event-Id": event.event_id, "X-Signature": "v1="+sig}
POST(url, body, headers)
Vérification et déduplication du destinataire (pseudo) :
pseudo assert abs(now - X-Timestamp) <= 300 assert timingSafeEqual(hmac(secret, canonical), sig)
if kv.exists("idemp:"+event_id): return 200
begin tx if kv.exists("idemp:"+event_id): commit; return 200 handle(event.data) // доменная логика kv.set("idemp:"+event_id, "ok", ttl=259200)
commit return 200
15) Erreurs fréquentes
Pas de dédupit → effets répétés (double refand/peyout).
Signature sans horloge/fenêtre → vulnérabilité à replay.
Gardez un secret HMAC pour tous les partenaires.
Les réponses '200' avant la fixation du résultat → la perte d'événements au crash.
« Laver » les détails de sécurité dans les réponses/logs.
Absence de DLQ/replay - Les incidents ne sont pas résolus.
16) La barre de mise en œuvre
La sécurité : HMAC v1 + ' X-Timestamp ' + ' X-Event-Id ', la fenêtre ≤ 5 mines; mTLS/IP allow-list par extension.
Конверт: `event_id`, `type`, `version`, `ts`, `attempt`, `data`.
Livraison : at-least-once, backoff avec gitter, 'Retry-After', API de replay DLQ +.
Idempotence : KV-cache 24-72 h, fixation atomique de l'effet + « mark _ seen ».
Observabilité : métriques de livraison, signatures, doublons ; trace 'trace _ id'.
Documentation : version, codes de réponse, exemples, chèque UAT.
Résumé
Les webhooks résistants sont construits sur trois baleines : une enveloppe signée, une livraison at-least-once et un traitement idempotent. Formalisez le contrat, activez le HMAC/mTLS et la fenêtre temporelle, implémentez les retraits + DLQ et la réplique, stockez les étiquettes idempotentes et enregistrez les effets atomiquement. Les événements restent alors fiables même en cas de défaillance du réseau, de pics de charge et de rares « doublons du destin ».