Sur cette page
Une coupure réseau ou un délai dépassé ne vous dit pas si votre requête a abouti. Pour réessayer sans créer deux fois le même prospect ni la même simulation, ajoutez l'en-tête Idempotency-Key à chaque POST.
Exemple
POST
/v1/prospectsScope prospects:writeRequête
curl -X POST "https://api.floweet.com/v1/prospects" \
-H "Authorization: Bearer $FLOWEET_API_KEY" \
-H "Idempotency-Key: 6f1c2b0e-8d4a-4f7e-9a51-3c2d1e0f9b87" \
-H "Content-Type: application/json" \
-d '{
"name": "Transports Martin",
"fleet": {
"fuel": "gazole",
"currentBrand": "Esso",
"reference": "cheapest",
"kmPerVehicle": 30000,
"lines": [
{
"type": "lcv",
"vehicles": 12,
"consumptionL100": 8.5
},
{
"type": "hgv",
"vehicles": 3,
"consumptionL100": 30
}
]
}
}'Réponse 201
{
"id": "33e3daba-2998-438d-a05e-1c513009107a",
"name": "Transports Martin",
"updatedAt": "2026-10-26T11:46:19.310433+00:00",
"input": {
"fuel": "gazole",
"currentBrand": "Esso",
"reference": "cheapest",
"kmPerVehicle": 30000,
"lines": [
{
"type": "lcv",
"vehicles": 12,
"consumptionL100": 8.5
},
{
"type": "hgv",
"vehicles": 3,
"consumptionL100": 30
}
]
},
"cost": {
"fuel": "gazole",
"pricesFrom": "2026-10-19T11:35:43.000Z",
"pricesTo": "2026-10-26T11:00:00.000Z",
"vehicles": 15,
"currentBrand": "Esso",
"currentPrice": 2428,
"referenceBrand": "Total Access",
"referencePrice": 2252,
"lines": [
{
"type": "lcv",
"vehicles": 12,
"consumptionL100": 8.5,
"litersPerYear": 30600,
"overspendPerYear": 538600,
"overspendPerVehicle": 44900,
"co2Tonnes": 75.9,
"ets2PerYear": 341500
},
{
"type": "hgv",
"vehicles": 3,
"consumptionL100": 30,
"litersPerYear": 27000,
"overspendPerYear": 475200,
"overspendPerVehicle": 158400,
"co2Tonnes": 67,
"ets2PerYear": 301300
}
],
"total": {
"litersPerYear": 57600,
"overspendPerYear": 1013800,
"co2Tonnes": 142.8,
"ets2PerYear": 642800
},
"replaceFirst": "hgv",
"ets2": {
"startYear": 2028,
"pricePerTonne": 4500,
"costPerLiter": 112
}
}
}Rejouée avec la même clé et le même corps, la requête rend la même réponse 201, avec l'en-tête Idempotent-Replayed: true : aucun second prospect n'est créé.
Règles
- Une clé par opération : 1 à 255 caractères parmi lettres, chiffres,
._:-, un UUID v4 de préférence (par exemple6f1c2b0e-8d4a-4f7e-9a51-3c2d1e0f9b87). Une clé d'un autre format répond400 validation_error. Générez-la avant le premier envoi et gardez-la avec l'opération (en base, dans votre file de tâches) pour toutes ses nouvelles tentatives. - Pendant 24 heures, la même clé avec la même requête (même méthode, même chemin, mêmes paramètres, même corps) rejoue la première réponse telle quelle, avec
Idempotent-Replayed: true. - La même clé avec une autre requête :
422 idempotency_key_mismatch. La même requête encore en cours :409 idempotency_in_progress, réessayez dans une seconde. - Réponses mémorisées et rejouées : succès (2xx) et erreurs 4xx, sauf
429. Ne le sont pas, pour qu'une nouvelle tentative s'exécute vraiment :429et les erreurs 5xx — la clé est libérée. - Les clés sont propres à votre organisation et à l'environnement : toutes vos clés d'API d'un même environnement les partagent, et une clé employée en test ne rejoue jamais une réponse live.
- L'en-tête est facultatif, mais fortement recommandé sur chaque
POST. LesGETn'en ont pas besoin ;PUT,PATCHetDELETEsont déjà sans effet en double (rejoués, ils laissent la ressource dans le même état) et l'ignorent.
Clé réutilisée
Réponse 422
{
"error": {
"code": "idempotency_key_mismatch",
"message": "Cette clé d'idempotence a déjà servi pour une requête différente (méthode, chemin ou corps). Utilisez une nouvelle clé."
}
}Réessayer avec la même clé
import { randomUUID } from "node:crypto";
// Une clé par opération, créée AVANT le premier envoi et gardée pour toutes ses tentatives.
const idempotencyKey = randomUUID();
const delays = [1_000, 5_000, 30_000];
async function createProspect(body) {
for (let attempt = 0; ; attempt++) {
let res;
try {
res = await fetch("https://api.floweet.com/v1/prospects", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.FLOWEET_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
} catch (networkError) {
res = null; // Coupure ou délai dépassé : on ne sait pas si la requête a abouti.
}
if (res?.ok) return res.json(); // 201, ou le rejeu de la première réponse (Idempotent-Replayed: true)
const retryable = !res || res.status >= 500 || res.status === 429 || res.status === 409;
if (!retryable || attempt >= delays.length) throw new Error(res ? (await res.json()).error.code : "network_error");
const wait = Number(res?.headers.get("Retry-After") ?? 0) * 1000 || delays[attempt];
await new Promise((r) => setTimeout(r, wait));
}
}