# Idempotency key

> Clé d'idempotence — garantie d'exécution unique d'une requête API

Mécanisme par lequel un client envoie un identifiant unique avec une requête API mutante (POST), garantissant qu'en cas de retry réseau, l'opération ne sera exécutée qu'une seule fois côté serveur. Best practice critique pour les paiements API.

_Catégorie: technique_

---

## Définition

L'**idempotency key** (clé d'idempotence) est un identifiant unique qu'un client envoie avec une requête mutante (typiquement un `POST`) pour garantir qu'en cas de retry réseau, l'opération ne sera exécutée qu'une seule fois.

Le serveur stocke la clé ; si la même requête revient, il renvoie la réponse précédente sans rejouer l'opération. C'est une best practice critique pour toute API de paiement, où une double exécution signifie double paiement, double facturation ou état corrompu. Stripe, Bridge, Adyen, Mollie, AWS et bien d'autres l'imposent ou la supportent.

## Pourquoi c'est critique

Sur le réseau, l'incertitude est la règle : une requête peut expirer sans qu'on sache si elle a été reçue, traitée, ou si un retry va doubler l'opération. Sans clé d'idempotence, impossible de retry en sécurité ; avec, le client peut retry sereinement, le serveur renvoyant la réponse stockée si l'opération a déjà eu lieu.

## Comment ça marche

```mermaid
sequenceDiagram
    autonumber
    participant Client
    participant Server
    participant DB

    Note over Client: 1. Génère idempotency_key UUID
    Client->>Server: 2. POST /payments<br/>Idempotency-Key: 4a7f3b...<br/>{amount: 100, recipient: ...}

    Server->>DB: 3. Lookup key 4a7f3b...
    alt Première fois (cache miss)
        DB-->>Server: 4. Pas trouvé
        Server->>Server: 5. Exécute paiement
        Server->>DB: 6. Stocke key + response
        Server-->>Client: 7. 200 OK + payment_id
    else Retry (cache hit)
        DB-->>Server: 4. Trouvée + response cachée
        Server-->>Client: 5. 200 OK + payment_id (même!)
    end

    Note over Client: Si timeout, retry avec MÊME clé<br/>→ pas de double paiement
```

## Format et bonnes pratiques

- **Génération** : UUID v4 (aléatoire), ULID (trié temporellement, debug-friendly), ou hash métier déterministe (`SHA256(user_id + transaction_ref)`).
- **Header** : `Idempotency-Key: 4a7f3b80-fe5c-4d12-9c7e-83b3a4ab9b1c`. L'IETF a publié un draft, mais la convention est adoptée bien avant son officialisation.
- **Durée de cache** : 24h chez Stripe et Bridge, 1h chez Mollie. Recommandé : 24 à 48h pour les paiements, 7 à 30 jours pour l'asynchrone. Au-delà, un retry rejouerait l'opération.
- **Scope** : par marchand / par compte (recommandé), plutôt que global (risque de collision).

## Payload différent, même clé

Si la même clé est envoyée avec un payload différent, le serveur devrait rejeter la requête (`409 Conflict`), comme le font Stripe et AWS. Cela évite qu'un `payment 200 €` soit silencieusement ignoré après un `payment 100 €` portant la même clé.

## Cas d'usage

- **Paiements** : `POST /payments`, `/transfers`, `/charges`, `/refunds` — toujours.
- **Création de ressources** : `POST /customers`, `/subscriptions` — pour éviter les doublons.
- **Opérations coûteuses** : `POST /kyc`, `/loans/disburse`.
- **Inutile sur** : `GET` (idempotent par nature), `PUT` avec ID explicite, `DELETE`.

## Idempotency-Key vs autres techniques

| Technique | Garantie | Complexité |
|-----------|----------|------------|
| Idempotency-Key (header) | Exactly-once par appel | Faible côté client |
| Token de transaction unique | Exactly-once par transaction métier | Moyenne |
| Verrou DB optimiste | Exactly-once par ressource | Moyenne |
| Saga + commands | Exactly-once par flow | Élevée |

L'Idempotency-Key agit au niveau API : elle ne protège pas d'un bug métier (un utilisateur qui paie deux fois via deux boutons différents génère deux clés distinctes).

## Implémentation côté serveur

Stockage typique : Redis (TTL natif, clé `idempotency:{merchant}:{key}`), DynamoDB pour la scalabilité, ou PostgreSQL à faible volume.

```python
def handle_payment(req):
    key = req.headers.get("Idempotency-Key")
    if not key:
        return create_payment(req)

    cached = redis.get(f"idempotency:{merchant}:{key}")
    if cached:
        return cached

    with redis.lock(f"idempotency-lock:{merchant}:{key}", ttl=30):
        cached = redis.get(f"idempotency:{merchant}:{key}")
        if cached:
            return cached

        response = create_payment(req)
        redis.setex(f"idempotency:{merchant}:{key}", 86400, response)
        return response
```

Le lock est nécessaire si deux requêtes portant la même clé arrivent en même temps (race condition de retry), pour éviter une double exécution en parallèle.

## Comportement en cas d'échec

- **Timeout client, succès serveur** : retry avec la même clé → réponse cachée (succès).
- **Erreur 5xx** : la plupart des fournisseurs rejouent (un 5xx n'est pas considéré comme définitif).
- **Erreur 4xx** : retry avec la même clé → la réponse 4xx cachée est renvoyée.

## Ce que l'Idempotency-Key n'est pas

- **Pas une transaction distribuée** : elle ne garantit pas la cohérence inter-services.
- **Pas une protection métier** : deux clics sur deux boutons génèrent deux clés.
- **Pas un remplacement de l'authentification** : c'est complémentaire.
- **Pas requise sur GET / PUT** : nativement idempotents.
- **Pas (encore) un standard formel** : draft IETF, mais convention quasi universelle.

## Dans l'écosystème PSD2 / Open Finance

- **Berlin Group** : recommande `X-Request-ID` (équivalent).
- **STET** : header `X-Request-ID`.
- **OBIE UK** : `x-idempotency-key`.
- **PSP acquéreurs** (Stripe, Adyen, Mollie) : universellement adopté.
- **Webhooks** : la clé est aussi cruciale en réception, pour ne pas traiter deux fois un même événement.

## Exemples concrets

- **Stripe** : header `Idempotency-Key`, cache 24h, rejet du payload différent — le standard du secteur.
- **Bridge** : `Idempotency-Key` + `X-Request-ID` selon l'endpoint.
- **Mollie** : `Idempotency-Key`, cache 1h.
- **AWS** : paramètre `client-token`.
- **Cas vécu** : un marchand sans clé d'idempotence retry après timeout → double prélèvement sur 0,5 % des paiements → dizaines de milliers d'euros de remboursements et perte de confiance.
- **Pattern UI** : générer la clé au moment du clic et la persister dans le state du formulaire — un reclic 5 secondes plus tard réutilise la même clé, sans double charge.

## Sources

- [Idempotent requests | Stripe API Reference](https://docs.stripe.com/api/idempotent_requests) — Stripe
- [API idempotency | Introduction to the Mollie API](https://docs.mollie.com/reference/api-idempotency) — Mollie
- [draft-ietf-httpapi-idempotency-key-header](https://datatracker.ietf.org/doc/draft-ietf-httpapi-idempotency-key-header/) — IETF

---

Source: https://openfinanceguide.com/fr/glossary/idempotency-key
