Idempotence des agents IA : Éviter la double facturation des relances

Les tentatives de reprise d'un agent créent des prélèvements en double et des commandes en double. Découvrez comment fonctionnent les clés d'idempotence, comment les générer à chaque étape de la tâche, et comment tester que le second appel ne change rien.

Ashley Innocent

Ashley Innocent

26 August 2026

Idempotence des agents IA : Éviter la double facturation des relances

Apidog pour les entreprises

Déploiement sur site

SSO & RBAC

Conforme SOC 2

Découvrir Apidog Enterprise

Votre agent a appelé le point de terminaison de paiement. La requête a abouti, le paiement a été effectué, puis la réponse a expiré en chemin. L'agent n'a jamais vu de 200, il a donc fait ce que vous lui avez demandé en cas d'échec : il a réessayé. Maintenant, le client a été facturé deux fois, et rien dans vos journaux ne ressemble à une erreur.

C'est le mode de défaillance qui distingue les agents des clients API ordinaires. Un humain qui clique une fois sur « Payer » voit une icône de chargement et attend. Un agent dans une boucle de réessai voit le silence et essaie à nouveau, parfois trois ou quatre fois de suite, plus vite que n'importe quelle personne. Chaque politique de réessai que vous ajoutez pour rendre l'agent plus fiable rend également les écritures en double plus probables. La solution est l'idempotence : faire en sorte qu'une requête répétée produise le même résultat qu'une seule requête.

Ce guide explique ce que l'idempotence signifie au niveau HTTP, comment générer des clés qu'un agent peut réellement réutiliser, ce que le serveur doit stocker pour les honorer, et comment tester le tout avant qu'un client réel ne soit facturé deux fois. Si vous n'avez pas lu notre article fondamental sur les raisons pour lesquelles les agents IA échouent en production, les écritures en double sont le mode de défaillance qui se cache derrière la plupart des rapports « l'agent l'a fait deux fois ».

Apidog apparaît dans la partie test de ceci. L'idempotence est quelque chose que vous intégrez à votre API et à la couche d'outils de votre agent. Ce dont vous avez besoin ensuite est un moyen d'envoyer la même requête deux fois et de prouver que la seconde n'a rien changé, ce qui est un test que vous pouvez sauvegarder et exécuter en CI.

Pourquoi les agents brisent l'idempotence plus souvent que les humains

Trois éléments concernant le trafic des agents rendent les doublons fréquents.

Le premier est le volume de réessais. Les frameworks d'agents réessayent agressivement par défaut car les défaillances réseau transitoires sont la cause la plus fréquente d'un échec d'exécution. Notre guide sur la récupération d'erreurs d'agent explique les stratégies de temporisation et les disjoncteurs, et chaque technique qu'il contient augmente le nombre de fois qu'une requête donnée atteint votre serveur.

Le second est l'ambiguïté d'un délai d'attente. Lorsqu'une requête expire, le client n'apprend rien sur le fait que le serveur l'ait traitée ou non. Un 504 provenant d'un proxy pourrait signifier que l'écriture n'a jamais eu lieu ou qu'elle a eu lieu et que la réponse a été perdue. Les humains vérifient généralement avant de réessayer. Les agents ne le font généralement pas, car « vérifier d'abord » est un appel d'outil supplémentaire que le modèle doit décider de faire.

Le troisième est la boucle. Un agent qui échoue à une tâche peut redémarrer l'intégralité de la tâche, et pas seulement l'étape échouée. Si l'étape un crée une commande et que l'étape quatre échoue, un redémarrage naïf crée une deuxième commande. C'est là que les agents à plusieurs étapes diffèrent nettement d'un script : la limite de réessai est floue, et c'est le modèle, et non votre code, qui décide où elle commence.

Mettez tout cela ensemble et vous obtenez la forme du problème. Ce n'est pas que les agents envoient de mauvaises requêtes. Ils envoient des requêtes correctes plus d'une fois.

Ce que l'idempotence garantit réellement

Une opération est idempotente lorsque l'exécuter plusieurs fois a le même effet que de l'exécuter une seule fois. GET, PUT et DELETE sont définis comme idempotents dans la RFC 9110, la spécification des sémantiques HTTP. POST ne l'est pas, ce qui est précisément la raison pour laquelle les opérations dangereuses tendent à être des appels POST : créer une commande, envoyer un message, démarrer un transfert.

Deux clarifications évitent beaucoup de confusion.

Idempotent n'est pas la même chose que sûr. Une méthode sûre ne change rien. DELETE est idempotent mais destructeur : l'appeler cinq fois laisse la ressource supprimée, comme l'appeler une seule fois, mais la ressource est toujours partie. Les agents ont besoin que ces deux propriétés soient gérées séparément, ce qui est l'argument avancé par notre article sur les clés API à privilège minimum pour les agents du côté des identifiants.

Idempotent n'est pas non plus synonyme de réponse identique. Le deuxième appel peut renvoyer le résultat stocké du premier, et il peut renvoyer un code de statut différent. Ce qui ne doit pas changer, c'est l'état sur le serveur. Un paiement. Une commande. Un e-mail.

Clés d'idempotence : le modèle qui rend POST sûr

La solution standard est une clé générée par le client et envoyée avec la requête. Le serveur enregistre la clé avec le résultat, et toute requête ultérieure portant la même clé renvoie le résultat enregistré au lieu de refaire le travail.

Stripe a popularisé l'en-tête, et la documentation d'idempotence de Stripe reste la description la plus claire de la sémantique. Il existe également un effort de l'IETF pour la standardiser comme le champ d'en-tête Idempotency-Key, qu'il est bon de lire avant d'inventer votre propre nom d'en-tête.

La requête ressemble à ceci :

POST /v1/payments HTTP/1.1
Host: api.yourservice.com
Authorization: Bearer sk_live_...
Idempotency-Key: 9f2b7c14-6d3a-4b18-9d55-1e2a7c0b4f31
Content-Type: application/json

{
  "amount": 4900,
  "currency": "usd",
  "customer_id": "cus_8812",
  "description": "Pro plan, August"
}

La clé est un UUID. Elle n'a aucune signification pour le serveur au-delà de « c'est la même opération logique ». Le serveur la stocke, ainsi qu'une empreinte numérique du corps de la requête et de la réponse qu'elle a produite.

Générer une clé que l'agent peut réutiliser

C'est là que la plupart des implémentations d'agents échouent. Si l'enveloppe de l'outil génère un nouveau UUID à chaque appel, la clé change à chaque réessai, et l'idempotence ne sert à rien. La clé doit être liée à l'opération logique, et non à la tentative HTTP.

La règle : générer la clé lorsque l'agent décide d'effectuer une action, et la conserver pour chaque réessai de cette décision.

import uuid

class PaymentTool:
    def __init__(self, client):
        self.client = client
        self._keys = {}

    def charge(self, task_id, step_id, amount, customer_id):
        # One key per (task, step). Retries of the same step reuse it.
        op = f"{task_id}:{step_id}"
        if op not in self._keys:
            self._keys[op] = str(uuid.uuid4())

        return self.client.post(
            "/v1/payments",
            headers={"Idempotency-Key": self._keys[op]},
            json={"amount": amount, "customer_id": customer_id},
        )

Une clé déterministe fonctionne aussi, et elle survit aux redémarrages de processus, ce qu'un dictionnaire en mémoire ne fait pas :

import hashlib

def idempotency_key(task_id: str, step_id: str, payload: dict) -> str:
    raw = f"{task_id}|{step_id}|{sorted(payload.items())}"
    return hashlib.sha256(raw.encode()).hexdigest()[:32]

Dérivez la clé de l'exécution de la tâche et de l'étape, jamais d'un horodatage ou d'une valeur aléatoire régénérée à chaque tentative. Si l'agent redémarre toute la tâche et a réellement l'intention d'effectuer un nouveau paiement, l'ID de la tâche change, et la clé aussi. C'est le comportement souhaité.

Ce que le serveur doit faire

Traiter correctement l'en-tête demande plus qu'une simple recherche. Une implémentation fonctionnelle fait quatre choses :

  1. À l'arrivée, essayer de revendiquer la clé. L'insérer dans une table avec une contrainte d'unicité avant de faire tout travail. Si l'insertion échoue, une autre tentative la possède.
  2. Si la clé existe et que l'empreinte de la requête stockée diffère, rejeter avec 422. La même clé avec un corps différent signifie un bug client, et renvoyer silencieusement l'ancien résultat le masquerait.
  3. Si la clé existe et que la première tentative est toujours en cours, retourner 409 afin que l'appelant recule plutôt que de tenter une course.
  4. Lorsque le travail est terminé, stocker le code de statut et le corps par rapport à la clé, puis le renvoyer pour chaque appel ultérieur.
CREATE TABLE idempotency_records (
  key             TEXT PRIMARY KEY,
  request_hash    TEXT NOT NULL,
  state           TEXT NOT NULL,      -- in_progress | completed
  response_status INT,
  response_body   JSONB,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  expires_at      TIMESTAMPTZ NOT NULL
);

Définissez une expiration. Vingt-quatre heures couvrent toute fenêtre de réessai réaliste, et conserver les clés indéfiniment transforme la table en un fardeau. Stripe expire les clés après 24 heures, ce qui est une valeur par défaut raisonnable à copier.

Tester que le deuxième appel ne change rien

Construire l'idempotence est la moitié du travail. Prouver qu'elle fonctionne est l'autre moitié, et c'est la moitié qui est souvent ignorée, car le chemin nominal semble identique que la fonctionnalité fonctionne ou non.

Le test est simple à décrire : envoyez la requête, capturez le résultat, envoyez exactement la même requête à nouveau, et affirmez que le serveur n'a pas effectué le travail deux fois. La partie difficile est la dernière affirmation, car la réponse seule ne vous le dira pas. Deux paiements réussis retournent tous deux 200.

Alors, affirmez sur l'état, et non sur la réponse :

Dans Apidog, vous pouvez configurer cela comme un scénario de test : l'étape un envoie le POST avec une Idempotency-Key fixe, l'étape deux le répète, et l'étape trois liste la ressource et affirme le compte. Enregistrez l'ID de réponse de l'étape un dans une variable et affirmez que l'étape deux renvoie la même valeur. Parce que l'intégralité du scénario est stockée, il s'exécute en CI à chaque modification du chemin de paiement, c'est là que les régressions apparaissent réellement. La même technique s'applique aux modèles plus larges de notre guide de test de contrat API.

Deux autres cas méritent d'être couverts, car ils détectent de vrais bugs :

La simulation aide aussi ici. Si vous êtes encore en train de construire l'agent et que l'API de paiement n'existe pas encore, simulez-la avec une réponse consciente de l'idempotence afin que la logique de réessai de l'agent soit exercée tôt. Notre article sur les raisons pour lesquelles les agents devraient utiliser des simulations au lieu de la production justifie plus largement cette habitude.

Quand vous ne pouvez pas ajouter de clé

Parfois, l'API ne vous appartient pas et ne prend pas en charge l'idempotence. Vous avez quand même des options, par ordre de préférence approximatif.

Savoir quelle exécution a fait quoi

L'idempotence arrête le doublon. Elle ne vous dit pas quelle tentative a créé l'enregistrement, et c'est la question qui vous est posée après un incident.

Gardez l'identité de l'exécution attachée au travail. Lorsque l'agent est votre propre service, cela signifie l'ID de tâche et l'ID d'étape de la dérivation de clé ci-dessus, enregistrés à chaque tentative. Lorsque l'agent est un environnement d'exécution de code effectuant un travail assigné, la plateforme le conserve généralement pour vous : dans Sharkly, chaque exécution est attachée à la Tâche dont elle provient, avec son état d'exécution et son résultat stockés à côté du fil de commentaires, de sorte qu'une écriture répétée remonte à une exécution spécifique plutôt qu'à une tentative anonyme.

Une liste de vérification avant de livrer

Parcourez cette liste et l'histoire de la double facturation cesse d'être possible, ce qui signifie que votre politique de réessai peut devenir plus agressive plutôt que moins. C'est le véritable avantage : l'idempotence est ce qui vous permet de rendre un agent résilient sans le rendre dangereux.

Questions fréquemment posées

Pratiquez le Design-first d'API dans Apidog

Découvrez une manière plus simple de créer et utiliser des API