---
title: "Idempotence"
description: "Rendre une écriture rejouable sans risque : envoyez un Idempotency-Key et la seconde tentative rejoue la première réponse au lieu de créer un second enregistrement."
canonical: https://developer.billabex.com/fr/guides/idempotence/
lang: fr
alternate: https://developer.billabex.com/en/guides/idempotency/
last-updated: 2026-09-18
---

# Idempotence

> Rendre une écriture rejouable sans risque : envoyez un Idempotency-Key et la seconde tentative rejoue la première réponse au lieu de créer un second enregistrement.

Source: https://developer.billabex.com/fr/guides/idempotence/
Langue: Français (fr)
Version anglaise: https://developer.billabex.com/en/guides/idempotency/

Une nouvelle tentative n'est pas une exception, c'est la fin normale d'une requête qui a expiré. La
connexion tombe, le processus redémarre, le proxy abandonne : l'appel a peut-être atteint Billabex,
peut-être pas, et votre client ne peut pas le savoir. Sans un moyen de marquer la seconde tentative
comme étant la même, les deux réponses sont mauvaises. Réessayer peut créer une seconde facture ; ne
pas réessayer peut perdre la première.

`Idempotency-Key` supprime ce choix. Envoyez-en un sur une écriture, et une seconde requête portant
la même clé renvoie la réponse stockée de la première au lieu de s'exécuter à nouveau.

## Comment l'utiliser

Envoyez l'en-tête sur n'importe quel `POST`, `PUT`, `PATCH` ou `DELETE` sous `/api/public/v1/`, avec
une valeur propre à cette requête. Un UUID généré une fois, avant la première tentative, et réutilisé
à chaque nouvelle tentative de celle-ci. Exemple avec la création de facture du
[guide de démarrage](/fr/guides/demarrage/) : `POST /api/public/v1/invoices`, en
`multipart/form-data`, avec le fichier, les composantes des dates et les montants :

```bash
# Générée une fois, avant la première tentative, et conservée avec la facture synchronisée.
KEY=$(uuidgen)

curl -i -X POST https://next.billabex.com/api/public/v1/invoices \
  -H "Authorization: Bearer $BILLABEX_ACCESS_TOKEN" \
  -H "Idempotency-Key: $KEY" \
  -F "accountId=$ACCOUNT_ID" \
  -F "number=INV-2026-114" \
  -F "issuedDate.year=2026" -F "issuedDate.month=9" -F "issuedDate.day=15" \
  -F "dueDate.year=2026" -F "dueDate.month=10" -F "dueDate.day=15" \
  -F "totalAmount=1200.00" \
  -F "taxAmount=200.00" \
  -F "billingAddress.street=1 rue Exemple" \
  -F "billingAddress.city=Paris" \
  -F "billingAddress.postalCode=75001" \
  -F "billingAddress.country=FR" \
  -F "file=@facture.pdf;type=application/pdf"
```

Ne fixez pas vous-même l'en-tête `Content-Type` : `curl -F`, comme `fetch` avec un `FormData`, écrit
`multipart/form-data` avec la frontière (`boundary`) qui sépare les champs. Une date d'échéance
s'envoie en trois champs `dueDate.year`, `dueDate.month` et `dueDate.day`, jamais en une seule
chaîne `2026-10-15`. Après un timeout ou une connexion coupée, renvoyez la même commande avec la
même valeur de `$KEY`. `curl` choisit une nouvelle frontière à chaque exécution, et ce n'est pas un
problème : Billabex compare les champs et les octets du fichier, pas le corps brut, donc la nouvelle
tentative est reconnue comme la même requête.

La clé appartient à la requête, pas à la session. Générez-la là où vous construisez la requête,
stockez-la à côté de ce que vous synchronisez, et réutilisez-la à chaque nouvelle tentative de cette
écriture. Une clé générée à l'intérieur de la boucle de retry est une clé différente à chaque tour,
ce qui revient à n'en envoyer aucune.

L'en-tête est facultatif. Une requête qui n'en porte pas se comporte exactement comme avant : elle
s'exécute à chaque envoi.

## Ce que reçoit une seconde tentative

Une réponse rejouée porte le statut et le corps de la première tentative, plus un en-tête :

```http
HTTP/1.1 201 Created
Idempotent-Replay: true
```

Lisez `Idempotent-Replay` quand vous devez distinguer une écriture réelle d'une réponse rejouée, par
exemple pour décider de journaliser une création. N'y branchez pas votre logique métier : le corps
est le même dans les deux cas, et c'est précisément l'objectif.

Une clé est mémorisée **24 heures**, et elle est liée à votre client OAuth, à la méthode HTTP et au
chemin. Deux clients envoyant la même clé ne voient jamais la réponse de l'autre, et la même clé sur
`POST /invoices` et sur `POST /credit-notes` désigne deux opérations différentes.

## Erreurs

| Statut | Quand                                                                | Que faire                                                                                                      |
| ------ | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `400`  | La clé est vide ou dépasse 255 caractères.                           | Envoyez un UUID ou une valeur courte et unique équivalente.                                                    |
| `409`  | Une requête portant cette clé est encore en cours.                   | Attendez le délai `Retry-After`, puis réessayez avec la même clé pour lire son résultat.                       |
| `422`  | Cette clé a déjà servi avec un corps de requête différent.           | Utilisez une nouvelle clé pour une nouvelle requête. Renvoyez le corps d'origine pour lire la réponse stockée. |
| `503`  | Le magasin d'idempotence est injoignable, donc rien n'a été exécuté. | Réessayez à l'identique, avec la même clé.                                                                     |

Le `409` compte dès que votre client tourne sur plusieurs workers. Deux d'entre eux qui réessaient la
même clé au même instant ne doivent pas écrire tous les deux : le second est invité à attendre plutôt
qu'autorisé à s'exécuter à côté du premier.

Le `422` est un signalement de bug, pas un refus d'aider : répondre avec la réponse précédente vous
remettrait le résultat d'une requête que vous n'avez pas envoyée. Il signifie que la clé a été
réutilisée pour autre chose, en général parce qu'elle dérive de quelque chose d'insuffisamment
unique, comme un identifiant client plutôt que l'opération elle-même.

Un `503` signifie que la requête n'a **pas** été exécutée. C'est le seul cas où refuser est plus sûr
qu'essayer : exécuter l'écriture sans pouvoir enregistrer qu'elle a eu lieu, c'est exactement ainsi
que naît le doublon que cet en-tête prévient.

## Ce qui n'est pas couvert

- **Les lectures.** `GET` et `HEAD` ne changent rien, il n'y a donc rien à sécuriser. L'en-tête y est
  ignoré.
- **Les échecs.** Une écriture qui a répondu `4xx` ou `5xx` libère sa clé : réessayer avec la même
  clé la réexécute, ce qui est le comportement souhaité puisque rien de rejouable ne s'est produit.
- **Le serveur MCP.** Les outils MCP portent une annotation `idempotentHint` qui indique si appeler
  un outil deux fois produit le même effet qu'une seule fois. Lisez-la dans
  [le catalogue d'outils](https://next.billabex.com/mcp/tools) avant de réessayer un appel d'outil.

## Les écritures déjà sûres

Certaines écritures n'ont pas besoin de clé, car les répéter ne peut pas produire un second
enregistrement :

- **Les upserts par `sourceId`.** `sourceId` est unique par organisation : envoyer deux fois le même
  document le met à jour au lieu de le dupliquer. Voir le guide [upsert](../upsert/).
- **`DELETE`.** Supprimer ce qui est déjà supprimé répond de la même façon.
- **Poser une valeur.** Un `PATCH` d'un montant payé ou d'une date d'échéance pose une valeur, il ne
  s'ajoute pas à une valeur existante.

Utilisez tout de même une clé quand c'est la _réponse_ de la première tentative qui vous intéresse,
et pas seulement son effet : un upsert réessayé après un timeout ne vous dit pas laquelle des deux
tentatives a créé l'enregistrement.

---

Billabex developer portal. OpenAPI specification: https://developer.billabex.com/openapi.json.
Agent instructions: https://developer.billabex.com/llms.txt. Complete documentation: https://developer.billabex.com/llms-full.txt.
