# Retry a request without duplicates

> The Idempotency-Key header of the Praeko API: retry a POST, PATCH or DELETE after a network failure without creating two posts, two media items or two webhooks. Rules, errors, curl, SDK and CLI.

Source: https://praeko.app/docs/en/guides/idempotency

When a response gets lost (the network dropped, the request timed out), you cannot tell whether Praeko received it. Send the `Idempotency-Key` header and the server runs the request only once: a retry gets the same response as the first attempt.

> **Without the header, nothing changes** Existing integrations keep working exactly as before. The header is optional on every route that accepts it.

## Rules

| Rule | Value |
|---|---|
| Where it applies | Authenticated `POST`, `PATCH` and `DELETE` routes under `/v1` (API key or panel session). `GET` is never stored; `PUT` is already safe to repeat. |
| The key | 1 to 255 visible ASCII characters, no spaces. Use a UUID. Anything else: `422 invalid_field`. |
| Scope | Workspace, route and key. The same key in another workspace, or on another route, is a different key. No response ever crosses workspaces. |
| Lifetime | 24 hours after the first request. |
| Same request | Same method, path, query and body (and the same `X-Praeko-Contract`): the stored response is replayed with `Idempotent-Replayed: true`. |
| Different request | Same key with another method, path, query or body: `422 idempotency_key_reuse`. Nothing runs. |
| First request still running | `409 idempotency_in_progress`, with `Retry-After: 1`. Retry in a moment. |
| Server error | A `5xx` response is not stored: the key is released and the next attempt really runs. |
| Client error | A `4xx` response (validation, 404, 409…) is stored and replayed. Fix the request and use a new key. |
| Large response | Above 256 KB only the status is stored; a retry gets `409 idempotency_response_too_large`. Read the resource with `GET`. |
| Orphaned request | If the server dies mid-request, the reservation can be taken over after 2 minutes. |
| Not covered | `PUT /v1/media/:id/parts/:part` and `PUT /v1/media/:id/content` (raw bytes, already safe to repeat: sending the same part again is fine). Creating (`POST /v1/media`) and completing (`POST /v1/media/:id/complete`) accept the key. |

Authentication errors and the rate limit (`401`, `403`, `429`) are answered before the key is checked and are never stored.

## The three errors

| Status | `code` | What to do |
|---|---|---|
| 422 | `idempotency_key_reuse` | The key was already used with another request. Generate a new key for the new request. |
| 409 | `idempotency_in_progress` | The first request is still running. Wait for `Retry-After` and retry with the same key. |
| 409 | `idempotency_response_too_large` | The original response was over 256 KB and was not stored. Read the resource with `GET`. |

## curl

**First attempt, then the identical retry**

```bash
KEY=$(uuidgen)
curl -X POST https://praeko.app/v1/ideas \
  -H "Authorization: Bearer $PRAEKO_KEY" -H "X-Praeko-Contract: en" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $KEY" \
  -d '{"title":"The bug that only showed up on Mondays"}'

# the network dropped and you do not know if it went through: repeat it EXACTLY, with the same key
curl -i -X POST https://praeko.app/v1/ideas \
  -H "Authorization: Bearer $PRAEKO_KEY" -H "X-Praeko-Contract: en" \
  -H "Content-Type: application/json" -H "Idempotency-Key: $KEY" \
  -d '{"title":"The bug that only showed up on Mondays"}'
# HTTP/2 200 … idempotent-replayed: true   (same body, only one idea on the board)
```

## JavaScript with fetch

**Retry with the same key**

```js
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function createPost(post) {
  const key = crypto.randomUUID(); // one key per request, the same on every attempt
  for (let i = 0; i < 3; i++) {
    try {
      const r = await fetch("https://praeko.app/v1/posts", {
        method: "POST",
        headers: { authorization: `Bearer ${process.env.PRAEKO_KEY}`, "x-praeko-contract": "en", "content-type": "application/json", "idempotency-key": key },
        body: JSON.stringify(post),
      });
      if (r.status === 409 && (await r.clone().json()).error.code === "idempotency_in_progress") { await sleep(1000); continue; }
      return r;
    } catch { /* network failure: retry with the same key */ }
  }
  throw new Error("no response after 3 attempts");
}
```

## SDK (`@praeko/sdk`)

Every call that writes accepts the `idempotencyKey` option. With `true`, the SDK generates a UUID and resends the request with the **same** key on a network failure, a `429` or a `5xx`. With a string, you pick the key (for example, one of your job ids, so it survives a process restart).

**JavaScript**

```js
import { Praeko } from "@praeko/sdk";
const praeko = new Praeko({ url: "https://praeko.app", key: process.env.PRAEKO_KEY });

// 1) the SDK generates the UUID and retries the POST with the same key if the network fails
const post = await praeko.posts.create({ title, media: { video: video.id }, texts, items }, { idempotencyKey: true });

// 2) or pick the key yourself
await praeko.webhooks.create({ url: "https://example.com/hooks/praeko", idempotencyKey: `hook-${jobId}` });
```

Without `idempotencyKey`, the SDK never retries a `POST`, `PATCH` or `DELETE` (only `GET` and upload parts). These accept the option: `media.create` and `media.complete`, `posts.create`, `adopt`, `update`, `cancel`, `publishNow` and `scheduleYoutube`, `webhooks.create`, `rotateSecret` and `redeliver`, `ideas.create` and `import`, `apiKeys.create`, `youtube.reply` and `instagram.reply`.

## CLI

**Terminal**

```bash
praeko publish-now <id> --idempotency-key K
praeko webhook create --url https://example.com/hooks/praeko --idempotency-key K
```

`publish-now` and `webhook create` send the header. The CLI does not retry on its own: the option is for when you run the command again, for example from a script with retries.

## What was already safe to resend

| Situation | What to do |
|---|---|
| `POST /v1/posts` with your own `id` | Send your own `id` in the body: a repeat returns `409 id_exists` and nothing is duplicated. Works even without `Idempotency-Key`. |
| Repeated media part | Safe: `PUT /v1/media/:id/parts/:part` overwrites the same part. |
| Repeated webhook delivery | `Praeko-Delivery` is the same on every attempt: keep the ids you already handled and ignore repeats. |
| `POST /v1/insights/import` | Idempotent: it upserts by network and id. |
| `POST /v1/ideas/import` | Idempotent by each idea's old id (only ideas that do not exist yet are added). |
