Skip to main content
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

RuleDetail
Header nameIdempotency-Key
FormatUUID v4
ScopePer workspace user. Same key from a different user is independent.
Lifetime24 hours
What’s cachedStatus 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

# 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
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);
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

Response headers

HeaderMeaning
X-Idempotency-Status: storedFirst time we’ve seen this key — response is now cached for 24h.
X-Idempotency-Status: cachedWe’ve seen this key before — this response was replayed from cache.
X-Idempotency-CreatedTimestamp of the first request that used this key.

Errors

INVALID_IDEMPOTENCY_KEY (400)

The header value isn’t a valid UUID v4.
{
  "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.
{
  "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.