Articles

29 septembre 2026

Sécuriser une API de paiement : bonnes pratiques concrètes (idempotence, webhooks, réconciliation)

Introduction

Construire une API de paiement, c'est accepter que tout ce qui peut mal tourner dans un système distribué (timeout, double envoi, panne réseau, désynchronisation) finira par arriver. La différence entre une API fragile et une API fiable ne se joue pas sur les cas nominaux, mais sur la gestion de ces cas limites.

Voici les pratiques concrètes que j'applique dans mes projets fintech.

1. L'idempotence : la règle numéro un

Un client (ou un réseau instable) peut envoyer deux fois la même requête de paiement. Sans protection, cela peut entraîner un double débit.

Solution : exiger une clé d'idempotence (Idempotency-Key) dans l'en-tête de chaque requête de création de transaction. Côté serveur, cette clé est stockée avec le résultat de la première exécution ; toute requête identique renvoie la réponse déjà calculée sans ré-exécuter l'opération.

public function handle(Request $request)
{
    $idempotencyKey = $request->header('Idempotency-Key');

    $existing = IdempotencyRecord::where('key', $idempotencyKey)->first();
    if ($existing) {
        return response()->json($existing->response, $existing->status_code);
    }

    // ... traitement de la transaction ...

    IdempotencyRecord::create([
        'key' => $idempotencyKey,
        'response' => $result,
        'status_code' => 201,
    ]);

    return response()->json($result, 201);
}

2. Sécuriser les webhooks entrants

Un webhook mal sécurisé est une porte ouverte : n'importe qui pourrait simuler une confirmation de paiement.

Bonnes pratiques :

  • Vérifier la signature de chaque webhook (HMAC avec un secret partagé) avant tout traitement.
  • Rejeter les événements trop anciens (timestamp au-delà d'une fenêtre de tolérance) pour limiter les attaques par rejeu.
  • Traiter chaque événement de façon idempotente côté réception aussi, en stockant l'identifiant unique de l'événement fourni par le provider.
  • Répondre rapidement (2xx) puis traiter en asynchrone via une queue, pour éviter que le provider ne considère l'envoi comme un échec et ne relance inutilement.

3. Authentification et autorisation des clients API

  • Clés API scoping par marchand, avec possibilité de révocation immédiate.
  • Signature de chaque requête sortante (HMAC) en plus du TLS, pour garantir l'intégrité du payload.
  • Rate limiting par client, pour limiter l'impact d'une clé compromise ou d'un bug côté intégrateur.

4. Réconciliation systématique

Même avec toutes les protections précédentes, un écart peut apparaître entre l'état interne et l'état réel chez le provider (panne, bug, incident réseau).

Mettre en place :

  • un job planifié qui confronte quotidiennement les transactions internes aux relevés du provider ;
  • des alertes automatiques dès qu'un écart est détecté (transaction "réussie" en interne mais absente côté provider, ou inversement) ;
  • un statut explicite UNKNOWN / TO_INVESTIGATE plutôt que de forcer un état binaire incertain.

5. Traçabilité et audit

Chaque changement d'état d'une transaction doit être journalisé de façon immuable : qui/quoi a déclenché le changement, à quel moment, avec quelle donnée source. En cas de litige avec un client ou un provider, cet historique est la seule source de vérité fiable.

6. Gestion des remboursements

Le remboursement mérite les mêmes garanties que le paiement initial : idempotence, traçabilité, et vérification que le montant remboursé ne dépasse jamais le montant initial (y compris en cas de remboursements partiels multiples).

Ce que je retiens

Sécuriser une API de paiement, ce n'est pas ajouter de la sécurité "en plus" — c'est concevoir dès le départ pour l'échec : idempotence par défaut, vérification systématique des webhooks, et réconciliation continue plutôt que confiance aveugle dans les flux en temps réel.