---
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 are billed twice.
- `POST /generate` — same, for direct model generations.
- `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}`,
});
```

### 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.
