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

# Idempotency

> Make POST safe to retry — guaranteed at-most-once side effects.

Network requests fail. When that happens you want to retry, but retrying a mutating call risks doing the work twice (double-charging credits, creating two webhook subscriptions, queuing two renders).

The fix is **idempotency keys**. Send a unique `Idempotency-Key` header on every POST mutation; if the server already processed it, it returns the cached response instead of redoing the work.

## The contract

| Rule                              | Detail                                                             |
| --------------------------------- | ------------------------------------------------------------------ |
| **Header name**                   | `Idempotency-Key`                                                  |
| **Format**                        | UUID v4                                                            |
| **Scope**                         | Per workspace user. Same key from a different user is independent. |
| **Lifetime**                      | 24 hours                                                           |
| **What's cached**                 | Status code, response headers, response body                       |
| **What counts as "same request"** | Method + URL + body byte-hash                                      |

## Which endpoints support it

Every POST mutation, most importantly:

* `POST /api/v1/facade/generate` — the one that costs credits; a retried
  request with the same key is **never charged twice**
* `POST /api/v1/facade/brands` and `POST /api/v1/facade/brands/from-url`
* `POST /api/v1/facade/webhooks` (+ `/test`, `/rotate-secret`)
* `POST /api/v1/generate/sign`

GET endpoints are naturally idempotent — no key needed. (Signed image URLs
go further: the URL itself is the idempotency key — a unique URL renders
and bills exactly once, ever.)

## How to use it

<CodeGroup>
  ```bash curl theme={null}
  # request.json = a RenderForgeRequestV1 — see the quickstart
  curl -X POST https://api.kynva.ai/api/v1/facade/generate \
    -H "Authorization: Bearer $KYNVA_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: $(uuidgen)" \
    -d @request.json
  ```

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

  const key = randomUUID();

  async function renderOnce(request: object) {
    // request = a RenderForgeRequestV1 — see the quickstart
    return fetch("https://api.kynva.ai/api/v1/facade/generate", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.KYNVA_API_KEY!}`,
        "Content-Type": "application/json",
        "Idempotency-Key": key, // reuse on retry
      },
      body: JSON.stringify(request),
    });
  }

  // Retry-safe — second call returns the cached response, no second charge
  await renderOnce(request);
  await renderOnce(request);
  ```

  ```python Python theme={null}
  import os, uuid, requests

  key = str(uuid.uuid4())

  def render_once(request):
      # request = a RenderForgeRequestV1 dict — see the quickstart
      return requests.post(
          "https://api.kynva.ai/api/v1/facade/generate",
          headers={
              "Authorization": f"Bearer {os.environ['KYNVA_API_KEY']}",
              "Content-Type": "application/json",
              "Idempotency-Key": key,  # reuse on retry
          },
          json=request,
          timeout=60,
      )

  render_once(request)
  render_once(request)  # cached — not re-rendered, not re-charged
  ```
</CodeGroup>

## Response headers

| Header                         | Meaning                                                             |
| ------------------------------ | ------------------------------------------------------------------- |
| `X-Idempotency-Status: stored` | First time we've seen this key — response is now cached for 24h.    |
| `X-Idempotency-Status: cached` | We've seen this key before — this response was replayed from cache. |
| `X-Idempotency-Created`        | Timestamp of the first request that used this key.                  |

## Errors

### `INVALID_IDEMPOTENCY_KEY` (400)

The header value isn't a valid UUID v4.

```json theme={null}
{
  "error": {
    "code": "INVALID_IDEMPOTENCY_KEY",
    "message": "Idempotency-Key must be a valid UUID v4"
  }
}
```

### `IDEMPOTENCY_CONFLICT` (409)

You reused a key with a **different request body, URL, or method**. The server refuses to overwrite the cached response, and also refuses to silently replay a stale one.

```json theme={null}
{
  "error": {
    "code": "IDEMPOTENCY_CONFLICT",
    "message": "Idempotency key was already used for a different request",
    "details": [
      {
        "message": "Key was previously used for POST /api/v1/facade/generate",
        "severity": "error",
        "context": {
          "expected": "POST /api/v1/facade/generate",
          "actual": "POST /api/v1/facade/webhooks"
        }
      }
    ]
  }
}
```

**Fix**: generate a fresh UUID v4 for each *new* request. Only reuse a key when retrying *the same* request.

## When to generate a new key

```
User clicks "Render" once          →  one key
You retry that call 3x on network failure  →  same key
User clicks "Render" again         →  new key
A background job retries weekly    →  new key per run
```

A useful mental model: **one key per business operation**, not one per HTTP call.

## What if I don't send a key?

The endpoint still works — but you give up the safety net. A retry could legitimately do the work twice. We strongly recommend sending a key on every POST mutation.
