--- title: Idempotency | FLORA API description: Safely retry FLORA mutations without duplicating runs or assets. --- Network errors happen. When your `POST` request fails mid-flight, you don’t know whether the server received it — retrying naively might create the same run twice. **Idempotency keys** make retries safe: the second request with the same key returns the original response instead of creating a duplicate. ## How it works 1. You generate a unique key for a logical operation: e.g. `q3-thumbnail-DE-2024-09-15`. 2. You send the request with `idempotency_key` set to that key. 3. FLORA stores `(key → response)` for **24 hours**. 4. If you retry within 24 hours with the same key + same body, FLORA returns the original response — no new side effect. Same key + **same body** = same response. Same key + **different body** = `422 idempotency_conflict`. Choose keys deterministically from your data, so the body is identical when you retry. ## Where to use it Use `idempotency_key` on any mutation whose accidental duplication would be expensive: - `POST /techniques/{slug}/runs` — duplicate runs charge credits twice. - `POST /runs/generation` and `POST /runs/technique` — same. - `POST /assets` — duplicate signed-URL reservations. - `POST /projects` — duplicate Projects. Read endpoints (`GET`) and safely-repeatable actions (polling, listing) don’t need it. ## In code ### TypeScript ``` const run = await client.techniques.runs.create('thumbnail-v3', { inputs: [...], mode: 'async', idempotency_key: `q3-thumb-${marketCode}`, }); ``` ### Go ``` run, err := client.Techniques.Runs.New(ctx, "thumbnail-v3", flora.TechniqueRunNewParams{ Inputs: inputs, Mode: flora.TechniqueRunNewParamsModeAsync, IdempotencyKey: param.Field("q3-thumb-" + marketCode), }) ``` ### CLI Terminal window ``` flora techniques:runs create \ --technique-id thumbnail-v3 \ --input '{...}' \ --mode async \ --idempotency-key "q3-thumb-DE" ``` ### curl Terminal window ``` curl -X POST https://app.flora.ai/api/v1/techniques/thumbnail-v3/runs \ -H "Authorization: Bearer $FLORA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "inputs": [...], "mode": "async", "idempotency_key": "q3-thumb-DE" }' ``` ## Choosing good keys Good keys are **deterministic from the operation**, so the same logical call produces the same key: - ✅ `q3-thumbnail-{market_code}` — derived from the market you’re processing. - ✅ `import-csv-row-{row_hash}` — derived from a hash of the input row. - ✅ `user-{user_id}-onboarding-thumbnail` — derived from the user. Bad keys are **random per attempt**, which defeats idempotency: - ❌ `uuidv4()` generated inside the retry loop — every retry gets a new key. - ❌ `Date.now()` — same problem. - ❌ `"my-run"` — collides across unrelated operations. If you must use a UUID, generate it **once** at the start of the logical operation and reuse it across retries. ``` // Generate once per logical operation const idempotencyKey = crypto.randomUUID(); async function createRunWithRetry() { for (let attempt = 0; attempt < 3; attempt++) { try { return await client.techniques.runs.create('thumbnail-v3', { inputs: [...], mode: 'async', idempotency_key: idempotencyKey, // same across retries }); } catch (err) { if (shouldRetry(err)) continue; throw err; } } } ``` ## What “same body” means FLORA compares the full request body byte-for-byte (after JSON normalization). If you retry with a different body — even a small change like reordering inputs — you get `422 idempotency_conflict`: ``` { "error": { "code": "idempotency_conflict", "message": "Idempotency key 'q3-thumb-DE' was used with a different request body." } } ``` Either reuse the original body or pick a new key. ## Key lifetime - Idempotency keys are remembered for **24 hours** from the first successful request. - After 24 hours, a request with the same key creates a new run (fresh side effect). - Keys are scoped per API key — using the same key string from a different API key creates a fresh entry. ## What gets cached The exact HTTP response — status code, headers (including the original `request-id`), and body. A retry that hits the cache returns identical data, which is exactly the point: your code can treat the retry as a “first” success. The cached response also includes the `idempotent-replayed: true` header so you can tell when a response came from the cache vs a fresh execution. ## What idempotency keys are **not** - **Not a deduplication system for output content.** Two distinct keys with the same input create two distinct runs, each charged separately. Keys protect against accidental retries, not intentional duplicates. - **Not a way to coalesce concurrent requests.** Two concurrent requests with the same key may both execute (one will hit the cache shortly after; the other returns a fresh run). For strict singleton behavior, serialize the requests in your code. - **Not authentication.** Anyone with your API key can use any idempotency key — they don’t authenticate. ## Recipe: idempotent batch import ``` import Flora from '@flora-ai/flora'; import crypto from 'node:crypto'; const client = new Flora({ apiKey: process.env.FLORA_API_KEY }); function keyFor(row: { market: string; headline: string }) { const hash = crypto .createHash('sha256') .update(`${row.market}|${row.headline}`) .digest('hex') .slice(0, 16); return `q3-thumb-${row.market}-${hash}`; } async function importRow(row: { market: string; headline: string }) { return client.techniques.runs.create('thumbnail-v3', { inputs: [ { id: 'headline', type: 'text', value: row.headline }, ], mode: 'async', idempotency_key: keyFor(row), }); } ``` Re-running this script with the same CSV is a no-op for already-processed rows. Edit a row’s headline and the key changes — that row gets re-run. ## Related - **[Errors](/platform/errors/index.md)** — `422 idempotency_conflict` and retry semantics. - **[Recipes](/recipes/batch-from-csv/index.md)** — production-grade batch patterns.