Idempotency
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
Section titled “How it works”- You generate a unique key for a logical operation: e.g.
q3-thumbnail-DE-2024-09-15. - You send the request with
idempotency_keyset to that key. - FLORA stores
(key → response)for 24 hours. - If you retry within 24 hours with the same key + same body, FLORA returns the original response — no new side effect.
Where to use it
Section titled “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/generationandPOST /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
Section titled “In code”TypeScript
Section titled “TypeScript”const run = await client.techniques.runs.create('thumbnail-v3', { inputs: [...], mode: 'async', idempotency_key: `q3-thumb-${marketCode}`,});run, err := client.Techniques.Runs.New(ctx, "thumbnail-v3", flora.TechniqueRunNewParams{ Inputs: inputs, Mode: flora.TechniqueRunNewParamsModeAsync, IdempotencyKey: param.Field("q3-thumb-" + marketCode),})flora techniques:runs create \ --technique-id thumbnail-v3 \ --input '{...}' \ --mode async \ --idempotency-key "q3-thumb-DE"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
Section titled “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 operationconst 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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.