> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hooked.so/llms.txt
> Use this file to discover all available pages before exploring further.

# Idempotency

> Retry a create request without creating or paying for it twice

A create request that times out leaves you guessing: the project may or may not exist, and may or may not have been charged. Send an `Idempotency-Key` header and you can simply send the request again. The first request runs; for the next 24 hours the same key with the same body gets that first answer back, and nothing is created or charged a second time.

```bash theme={null}
curl -X POST https://api.hooked.so/v1/project/create/script-to-video \
  -H "x-api-key: $HOOKED_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-video" \
  -d '{ "script": "The ocean produces over half of the oxygen we breathe.", "voiceId": "1", "mediaType": "ai-images" }'
```

## Which endpoints accept it

Every `POST` that creates or charges something:

* all `POST /v1/project/create/*` endpoints, including `ugc-studio/{format}`, `clone-video`, `article-to-video`, `pdf-to-video`, `pdf-to-brainrot` and `cinematic`
* `POST /v1/render`
* `POST /v1/image/remove-background` and `POST /v1/image/expand`
* `POST /v1/media/import` and `POST /v1/media/upload`
* `POST /v1/schedule/create`
* `POST /v1/social/account/analyze`

The header is optional. Without it, these endpoints behave as before: every request runs.

## Choosing a key

* 1 to 255 printable ASCII characters. Anything else answers `400` with a message starting `Idempotency-Key:`.
* One key per operation you want to happen once, for example your own job or order id (`order-1042-video`) or a UUID you store before sending. Generate it once and reuse it on every retry of that operation; a new random key per attempt protects nothing.
* Keys belong to your team and to the endpoint: the same key on another endpoint, or from another team, is a different request.

## What you get back

| Situation | Answer |
| - | - |
| First request with this key | Runs normally |
| Same key, same body, within 24 hours | The first answer again (same status and body), with the header `Idempotent-Replayed: true`. Nothing runs, nothing is charged |
| Same key, different body | `422` `Idempotency-Key was used with a different request` |
| Same key while the first request is still running | `409` `A request with this Idempotency-Key is still in progress`. Wait a few seconds and retry |

The body is compared as JSON, so key order and whitespace do not matter.

Answers that do not settle the request are not kept, so a retry with the same key runs again:

* a `5xx`: the request failed on our side, retrying is safe;
* `401`, `402`, `403`, `409` and `429`: refused before anything ran (bad API key, no credits or missing provider keys, no access to the product, the key still in use, the [rate limit](/guides/error-handling#rate-limits-429)). Fix the cause (or wait `Retry-After` seconds) and retry with the same key.

Other `4xx` answers (a validation error, for example) are kept like a success: fix the request and send it with a new key.

After 24 hours a key is forgotten and the same key runs the request again.

## Retrying a create

```javascript theme={null}
import { randomUUID } from "node:crypto";

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

async function createWithRetry(path, body, maxAttempts = 4) {
  const idempotencyKey = randomUUID(); // one key for every attempt of this operation
  for (let attempt = 1; ; attempt++) {
    try {
      const res = await fetch(`https://api.hooked.so/v1${path}`, {
        method: "POST",
        headers: {
          "x-api-key": process.env.HOOKED_API_KEY,
          "Content-Type": "application/json",
          "Idempotency-Key": idempotencyKey,
        },
        body: JSON.stringify(body),
        signal: AbortSignal.timeout(120_000),
      });
      const retryable = res.status >= 500 || res.status === 409;
      if (!retryable || attempt === maxAttempts) return res;
    } catch (error) {
      if (attempt === maxAttempts) throw error; // timeout or network error: safe to retry
    }
    await sleep(1000 * 2 ** (attempt - 1)); // 1s, 2s, 4s
  }
}
```

<Note>
  The [MCP server](/guides/mcp-server) tools do not send an `Idempotency-Key`. When an MCP create call times out, check `list_projects` before calling it again.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.