Aller au contenu
Floweet
Accès pro

Idempotence des POST

L'en-tête Idempotency-Key rend chaque POST rejouable sans doublon pendant 24 heures : rejeu, requête en cours, clé réutilisée et nouvelles tentatives.

api.floweet.com/v1 · Contrat 1.0.0-beta.1 · Bêta

Sections · Idempotence

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:write
Requê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 exemple 6f1c2b0e-8d4a-4f7e-9a51-3c2d1e0f9b87). Une clé d'un autre format répond 400 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 : 429 et 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. Les GET n'en ont pas besoin ; PUT, PATCH et DELETE sont 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));
  }
}