Un développeur reçoit un webhook, il veut que l’expéditeur (Stripe, un ERP, un transporteur) arrête de le harceler avec des tentatives répétées. La solution qui vient naturellement à l’esprit : répondre 200 OK tout de suite, puis traiter la charge utile tranquillement en tâche de fond. Le problème est réglé en apparence. En réalité, il vient de se déplacer, et il devient invisible.
Ce réflexe revient si souvent dans les intégrations WooCommerce que cela mérite un article à lui seul, tant les conséquences sont sournoises : elles n’apparaissent pas au moment du développement, ni même en recette, mais des semaines plus tard, quand une commande se retrouve bloquée sans que personne ne comprenne pourquoi.
Ce qu’on voit dans le code
Le point d’entrée du webhook, souvent une route enregistrée via register_rest_route ou un point d’entrée de plugin de paiement, commence par ces lignes, presque toujours écrites avec de bonnes intentions :
add_action( 'woocommerce_api_mon_webhook', function() {
status_header( 200 );
echo 'OK';
// Le traitement réel se fait plus tard, en arrière-plan
wp_schedule_single_event( time(), 'traiter_webhook_differe', array( $_POST ) );
} );
À première vue, rien ne choque : on répond vite, on planifie le vrai travail, tout le monde est content. Le problème apparaît dès que le traitement différé échoue — une commande introuvable, un verrou de base de données, un service tiers indisponible. L’expéditeur du webhook, lui, a déjà reçu son 200. Il considère l’événement comme livré et acquitté. Il ne le renverra jamais.
Pourquoi c’est un problème
La quasi-totalité des systèmes de webhooks sérieux (Stripe en tête, mais aussi la plupart des ERP et WMS) fonctionnent sur un principe simple : un code de retour 2xx signifie « traité avec succès, n’insiste pas » ; un code 4xx ou 5xx signifie « échec, réessaie plus tard selon une politique de retry exponentiel ». En renvoyant 200 avant même de savoir si le traitement va réussir, on désactive purement et simplement ce mécanisme de sécurité. On transforme un système avec garantie de nouvelle tentative en système à un seul essai, silencieux en cas d’échec.

Le second problème, moins évident, concerne l’ordre des événements. Si deux webhooks liés à la même commande arrivent à quelques secondes d’intervalle (par exemple une mise à jour de statut de paiement suivie d’une mise à jour d’adresse), et que les deux sont traités en arrière-plan via wp_schedule_single_event, rien ne garantit qu’ils s’exécuteront dans l’ordre de réception. WP-Cron n’est pas un ordonnanceur de file d’attente fiable : il dépend du trafic du site pour se déclencher, et deux tâches planifiées à la même seconde peuvent s’exécuter dans un ordre différent de leur arrivée.
Pourquoi le traitement asynchrone n’est pas le vrai coupable
Il faut être précis : traiter un webhook de façon asynchrone n’est pas en soi une erreur, c’est même souvent nécessaire pour ne pas bloquer la requête HTTP entrante pendant plusieurs secondes. L’erreur n’est pas d’être asynchrone, c’est de répondre 200 avant d’avoir la garantie que le travail est effectivement en file d’attente de façon durable, et de ne jamais faire remonter un échec de traitement différé vers l’expéditeur.
La bonne séquence
- Vérifier la signature de la charge utile (hors sujet ici, mais indispensable avant tout traitement).
- Insérer l’événement dans une file d’attente persistante — une table dédiée, ou un plugin de gestion de tâches comme Action Scheduler, déjà présent dans WooCommerce.
- Confirmer que cette insertion a réussi (vérifier la valeur de retour de l’insertion en base).
- Renvoyer 200 seulement à ce moment-là, une fois la persistance garantie — pas le traitement métier complet, juste sa mise en file sécurisée.
- Traiter la file de façon asynchrone, avec relance automatique en cas d’échec et alerte au-delà d’un certain nombre de tentatives.
Ce qu’Action Scheduler change concrètement
WooCommerce embarque Action Scheduler, une bibliothèque de tâches asynchrones bien plus robuste que WP-Cron nu : elle persiste les tâches en base de données, gère les tentatives, et surtout expose un statut consultable (menu WooCommerce > Statut > Planification des actions). Un webhook qui insère une action via as_enqueue_async_action() avant de répondre 200 peut être audité après coup : on voit exactement quelles actions ont échoué, combien de fois, et pourquoi.
- La réponse HTTP au webhook ne garantit que la mise en file, jamais le résultat métier final.
- Toute tâche en échec doit rester visible quelque part, jamais avalée silencieusement dans un
try/catchvide. - Un webhook qui échoue systématiquement doit remonter une alerte à une équipe humaine, pas seulement un log perdu parmi des milliers d’autres lignes.
Un webhook silencieux qui échoue est pire qu’un webhook qui échoue bruyamment : le second se répare en cinq minutes, le premier se découvre trois semaines plus tard dans une réclamation client.
Quoi faire
Ne répondez jamais 200 tant que vous n’avez pas la certitude que l’événement est capturé de façon durable, que ce soit dans une table dédiée ou via Action Scheduler. Gardez le traitement métier réellement asynchrone, mais donnez-lui un mécanisme de nouvelle tentative et une visibilité opérationnelle. Et surtout, ne considérez jamais un code 200 comme une preuve de succès métier : ce n’est qu’une preuve de réception.