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 :
- À 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.
- 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. - Si la clé existe et que la première tentative est toujours en cours, retourner
409afin que l'appelant recule plutôt que de tenter une course. - 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 :
- Le corps de la deuxième réponse correspond à celui de la première, y compris l'ID de ressource. Un nouvel ID signifie qu'une nouvelle ressource a été créée.
- Un
GETde suivi sur la collection renvoie un seul enregistrement, pas deux. - Tout compteur ou solde n'a bougé qu'une seule fois.
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 :
- Même clé, corps différent. Attendez-vous à un
422, pas à un succès silencieux. - Doublons concurrents. Envoyez les deux requêtes en même temps et confirmez qu'une seule gagne. Cela détecte la contrainte d'unicité manquante qu'un test séquentiel ne révélera jamais.
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.
- Rendez l'opération naturellement idempotente. Un
PUTvers un chemin de ressource choisi par le client est idempotent par construction :PUT /orders/{client_order_id}. Si vous contrôlez la conception de l'API, préférez cela à unPOSTavec un en-tête. Cela ne nécessite aucune table supplémentaire. - Vérifiez avant d'écrire. Demandez à l'agent d'interroger un enregistrement existant avec la même clé naturelle avant d'en créer un. C'est plus faible, car une course entre la vérification et l'écriture peut toujours produire deux enregistrements, mais cela élimine le cas courant du délai d'attente.
- Dédupliquez en aval. Si l'écriture est un message ou un événement, placez la déduplication chez le consommateur. Attachez un ID de message stable et demandez au consommateur d'ignorer les doublons. C'est une pratique courante dans les systèmes événementiels et cela s'accorde avec les conseils de notre guide sur les webhooks fiables.
- Verrouillez l'action. Pour les opérations véritablement irréversibles et qui ne peuvent pas être rendues idempotentes, mettez un humain en face. C'est le modèle de porte d'approbation de notre article sur les garde-fous des agents IA, et c'est la bonne réponse lorsque le coût d'un doublon est suffisamment élevé.
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
- Chaque outil non-idempotent que l'agent peut appeler nécessite une clé d'idempotence, et l'enveloppe de l'outil refuse d'envoyer sans.
- Les clés dérivent de la tâche et de l'étape, et non de la tentative.
- Le serveur revendique la clé avant d'effectuer le travail, pas après.
- La même clé avec une charge utile différente renvoie une erreur au lieu de la réponse en cache.
- Les doublons concurrents sont gérés par une contrainte de base de données, et non par la temporisation de l'application.
- Un test enregistré prouve que le deuxième appel ne change rien, et il s'exécute en CI.
- Les clés expirent selon un calendrier et la table est nettoyée.
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
- Ai-je besoin de clés d'idempotence pour les outils en lecture seule ? Non. Les requêtes
GETsont déjà idempotentes et sûres, donc en réessayer une vous coûte un peu de latence et rien d'autre. Réservez les clés pour les appels qui créent, facturent, envoient ou modifient l'état. - Où la clé doit-elle être générée, dans l'agent ou dans l'enveloppe de l'outil ? Dans l'enveloppe de l'outil, basée sur les identifiants de tâche et d'étape de l'agent. Laisser le modèle générer la clé est une erreur : les modèles régénèrent des valeurs lors des réessais et peuvent produire des collisions entre les tâches.
- Quel code de statut doit renvoyer une requête répétée ? Renvoyez le statut stocké de l'appel original, de sorte qu'un deuxième
POSTqui a d'abord renvoyé201renvoie201à nouveau avec le même corps. Certaines API ajoutent un en-tête tel queIdempotent-Replay: truepour marquer la répétition, ce qui est utile pour le débogage et inoffensif pour les clients qui l'ignorent. - Combien de temps les clés doivent-elles être conservées ? Vingt-quatre heures couvrent presque toutes les fenêtres de réessai. Une conservation plus longue aide rarement et fait croître la table sans limite. Si un client réessaie après la fenêtre, traitez-le comme une nouvelle opération.
- Est-ce que cela remplace les transactions ? Non. Les clés d'idempotence empêchent les requêtes dupliquées de produire des effets dupliqués. Les transactions maintiennent une seule requête atomique. Vous avez besoin des deux, et la revendication de clé doit être écrite dans la même transaction que le travail chaque fois que votre base de données le permet.
- Comment tester cela sans un vrai fournisseur de paiement ? Dirigez l'agent vers une simulation qui implémente la sémantique de la clé, y compris le
422en cas de non-concordance de la charge utile. Notre guide sur le test des agents IA contre des API simulées couvre la configuration, et Téléchargez Apidog si vous voulez que la simulation et le test de réessai vivent dans le même projet.
