Quand un client paie avec Wave ou Orange Money, votre application ne reçoit pas la confirmation directement : c’est l’opérateur, ou un agrégateur comme PayTech, qui vous prévient après coup en appelant une adresse de votre serveur. C’est ce qu’on appelle un webhook.

Sur le papier, c’est simple. En production, c’est l’une des premières sources d’incidents financiers que nous rencontrons en audit.

Ce qui peut mal tourner

Un webhook n’est pas un message unique et fiable. C’est une tentative de notification, que le fournisseur répète tant qu’il n’a pas reçu de réponse satisfaisante. Concrètement, votre serveur peut recevoir :

  • la même notification plusieurs fois, parce que votre première réponse a pris trop de temps ;
  • deux notifications en même temps, traitées en parallèle par deux processus ;
  • une notification en retard, après qu’un client a déjà relancé un second paiement ;
  • une fausse notification, envoyée par quelqu’un qui a deviné l’adresse de votre webhook.

Sans précaution, chacun de ces cas se traduit par une commande validée deux fois, un solde crédité deux fois, ou une commande livrée sans avoir été payée.

Les quatre règles que nous appliquons

1. Vérifier la signature avant tout

Chaque notification doit être authentifiée. La plupart des fournisseurs signent le contenu avec une clé secrète partagée (souvent en HMAC-SHA256). On recalcule la signature côté serveur et on rejette tout ce qui ne correspond pas — avant même de lire le montant.

import { createHmac, timingSafeEqual } from 'node:crypto';

function isSignatureValid(rawBody: string, received: string, secret: string) {
  const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(received);
  return a.length === b.length && timingSafeEqual(a, b);
}

Deux détails comptent : signer le corps brut de la requête (pas l’objet déjà transformé en JSON), et comparer en temps constant pour ne rien laisser deviner.

2. Enregistrer chaque événement avec une clé unique

Chaque notification porte un identifiant de transaction. On l’enregistre dans une table dédiée, avec une contrainte d’unicité en base de données. Si le même identifiant arrive une deuxième fois, l’insertion échoue : on sait immédiatement que c’est un doublon, et on répond « OK » sans rien refaire.

C’est la base de données qui garantit l’unicité, pas le code applicatif — c’est la seule protection qui résiste aux traitements simultanés.

3. Verrouiller la commande pendant la mise à jour

Pour passer une commande de « en attente » à « payée », on la lit en la verrouillant dans une transaction. Un second processus qui voudrait la modifier au même moment attend que le premier ait terminé, puis constate qu’elle est déjà payée.

await db.transaction(async (tx) => {
  const order = await tx.orders.findForUpdate(orderId); // verrou
  if (order.status === 'paid') return;                  // déjà traité
  await tx.orders.markPaid(orderId, transactionId);
  await tx.ledger.record(order, transactionId);         // écriture comptable
});

4. Répondre vite, traiter ensuite

Le fournisseur attend une réponse en quelques secondes. On répond donc dès que l’événement est vérifié et enregistré, et on confie le reste (e-mails, versements, notifications) à une file de tâches. Moins de délais, moins de renvois — et donc moins de doublons.

Et quand un doute subsiste ?

Même avec ces règles, un webhook peut ne jamais arriver. C’est pourquoi nous ajoutons toujours une réconciliation : une tâche régulière qui compare les transactions enregistrées chez nous avec le relevé de l’opérateur, et signale tout écart à l’équipe.

RisqueProtection
Fausse notificationVérification de signature
Notification rejouéeClé unique en base de données
Traitements simultanésVerrou dans une transaction
Notification perdueRéconciliation avec le relevé

À retenir

Un paiement fiable ne repose pas sur la confiance dans le réseau, mais sur des garanties posées dans la base de données. Ces quatre règles s’appliquent quel que soit le moyen de paiement : Wave, Orange Money, carte bancaire ou agrégateur.

Vous avez une intégration de paiement qui produit des écarts ? C’est exactement le type de problème que nous traitons en audit technique.