Un webhook paiement arrive à 02:14 pendant un deploy. L'app répond 503. Le provider réessaie trois fois. Le matin, le client a trois abonnements actifs et une seule facture. Personne n'avait implémenté l'idempotence — seulement un INSERT naïf.
Les webhooks ne sont pas des « callbacks HTTP sympas ». Ce sont des livraisons at-least-once avec retries, ordre non garanti, et parfois des minutes de décalage.
Modèle de livraison
Assumez at-least-once : doublons normaux. Parfois ordering non-FIFO entre types d'événements. Votre handler doit être idempotent et tolérer le réordonnancement logique (ex. cancelled avant paid rare mais possible).
Signature et horodatage
Vérifiez HMAC-SHA256 avec secret rotatable. Rejetez si timestamp > 5 min (replay). Constant-time compare pour la signature.
Logguez les échecs de signature séparément des 500 applicatifs — ne confondez pas attaque et bug.
Idempotence en pratique
| Approche | Avantage | Limite |
|---|---|---|
| event_id en base | Simple | TTL cleanup |
| Idempotency-Key header | Standard API | Support provider variable |
| État métier (paid_at) | Robuste métier | Race conditions |
Combinez clé provider + contrainte unique SQL.
Réponses HTTP et async
Traitement < 2 s : 200 sync. Sinon 202 + queue (Redis, SQS, Rabbit). Le worker reprend event_id. Jamais de travail lourd avant d'acquitter sans idempotence.
Dead-letter queue pour échecs métier après N tentatives.
Observabilité
Métriques : latence handler, taux 2xx/4xx/5xx, lag queue, doublons ignorés. Trace correlée event_id → job → facture. Runbook : « replay manuel sans double effet » documenté.
Checklist endpoint production
- [ ] Signature HMAC vérifiée constant-time
- [ ] Timestamp anti-replay (<5 min)
- [ ] Table event_id contrainte unique
- [ ] Réponse 2xx/202 < timeout provider
- [ ] Queue async pour traitement >2 s
- [ ] Dead-letter après N échecs métier
- [ ] Logs structurés event_id + latency
- [ ] Idempotence testée (3 POST identiques)
- [ ] Firewall IP provider documenté
- [ ] Runbook replay manuel sans double effet
- [ ] Rotation secret webhook planifiée
- [ ] RGPD : rétention payload limitée
Cocher cette liste avant d'exposer l'URL publique — un webhook « presque bon » coûte plus qu'un retard release de deux jours.
Scénarios de replay et incident
Imaginez un deploy qui redémarre PHP-FPM pendant trente secondes : le provider paiement envoie cinq retries espacés. Sans table d'idempotence, vous créditez cinq fois le même paiement. Le support client découvre le bug avant vos logs applicatifs parce que le dashboard compta agrège les doublons plus vite que votre monitoring technique.
Un replay légitime diffère d'un replay malveillant : le premier vient du provider après timeout, le second d'un attaquant qui capture une URL webhook non signée (si vous avez oublié HMAC). La signature lie le corps au secret ; sans elle, rejouer un POST capturé suffit.
Pour les tests, utilisez l'outil de replay du provider (Stripe CLI, GitHub redelivery) en staging avec base jetable. Vérifiez que le troisième retry identique ne change rien en base. Automatisez ce test en CI si l'endpoint est critique.
Côté hébergement mutualisé, vérifiez max_execution_time PHP et timeout nginx proxy — un 504 gateway génère retries provider même si votre handler aurait fini deux secondes plus tard. Alignez timeouts : nginx > PHP > provider webhook timeout, avec marge.
Les providers diffèrent sur signature header (Stripe-Signature, X-Hub-Signature-256, etc.) — encapsulez la vérification par adapter, pas copy-paste doc une fois.
Versionnez le secret webhook comme password : rotation avec double secret actif pendant 24 h si provider le supporte.
Charge test : simuler 10× burst webhooks — queue depth et worker count dimensionnés avant Black Friday.
Tenez runbook daté, métriques avant/après, revue post-incident — la discipline cumulative évite la panique du vendredi soir.
Tenez runbook daté, métriques avant/après, revue post-incident — la discipline cumulative évite la panique du vendredi soir.
Timeouts mutualisé
Alignez nginx > PHP > timeout provider — 504 déclenche retries.
Décider et avancer sans angle mort
- Vérifier la signature HMAC en constant-time avec horodatage anti-replay inférieur à cinq minutes.
- Dédupliquer par event_id — contrainte unique en base, idempotence testée avec trois POST identiques.
- Répondre 2xx/202 rapidement — queue async si le traitement dépasse deux secondes ; dead-letter après N échecs métier.
- Documenter les IP provider — firewall, rotation secret webhook, runbook replay manuel sans double effet.
- Limiter la rétention payload — conformité RGPD sur les corps stockés.
Pour choisir un hébergement API fiable (latence, SLA), utilisez le comparateur, l'annuaire et nos guides observabilité.
Questions fréquentes
Pourquoi signer les webhooks ?
La signature HMAC prouve que le corps vient du provider et n'a pas été altéré en transit. Sans elle, n'importe qui peut POST sur votre endpoint.
Quel code HTTP pour les retries ?
2xx acquitte. 4xx non retryable si erreur permanente (signature invalide). 5xx ou timeout déclenchent retries — d'où l'idempotence obligatoire.
Comment dédupliquer ?
Stockez event_id (ou hash stable) avec contrainte unique ; ignorez les doublons déjà traités même si le corps diffère légèrement.
Timeout côté émetteur ?
Souvent 5–30 s. Répondez vite (202 + job async) si le traitement est long, mais garantissez alors l'idempotence côté worker.
Avant d'ouvrir l'endpoint en prod, simulez trois POST identiques — si la base change trois fois, vous n'êtes pas prêts.
