# Idempotency keys

> Retry a create request without creating a second request or paying twice: send an Idempotency-Key header and sference deduplicates it for 24 hours.

Source: https://sference.com/docs/guides/idempotency

A network timeout does not tell you whether the server created the request. Retrying blindly can create a second inference request and bill it twice. Send an `Idempotency-Key` header and sference will deduplicate the create for 24 hours.

```bash
curl https://api.sference.com/v1/chat/completions \
  -H "Authorization: Bearer $SFERENCE_API_KEY" \
  -H "Idempotency-Key: 4f1c2a7e-8b3d-4a1f-9c2e-7d6b5a4f3e21" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "zai-org/GLM-5.3",
    "messages": [{"role": "user", "content": "Say hello in one sentence."}]
  }'
```

The header is optional. Without it, every request creates a new resource exactly as before.

## What the key guarantees

A key is scoped to **your organization, the creator, the API key, and the endpoint**. It is 1–256 characters and is retained for **24 hours**. Generate one key per logical request — a UUID is ideal — and reuse it only for retries of that request.

| Situation | What you get |
| --- | --- |
| Same key, same JSON parameters, original completed | The original response: same id, body, and usage. Nothing is re-run or re-billed. |
| Same key, same parameters, original still running | `409 idempotency_key_in_progress`, with `Retry-After`. |
| Same key, different JSON parameters | `409 idempotency_key_reused`. |
| Same key, original cancelled before completing | The request is re-executed under the same key. The first attempt was never billed. |
| Same key, original failed | The original error is replayed. Use a new key to retry. |
| Key older than 24 hours | Treated as a new key. |
| Key empty or longer than 256 characters | `400 idempotency_key_invalid`. |

The fingerprint is a SHA-256 of the canonical request body plus the endpoint. Headers are ignored, so changing a trace or auth header does not change the request's identity. `stream` is part of the body, so a streaming retry of a non-streaming request is a different request.

> **Cancelled requests re-execute by design**
>
> A request that timed out or was cancelled before producing a result was never billed. Replaying that cancellation would leave a client retrying a `504` forever, so sference re-executes it under the same key. A request that **completed** replays its stored result; a request that **failed** replays its error.

## Where it is accepted

| Surface | Header |
| --- | --- |
| `POST /v1/chat/completions` | `Idempotency-Key` |
| `POST /v1/responses` | `Idempotency-Key` |
| `POST /v1/messages` | `Idempotency-Key` |
| `POST /v1/batches` | `Idempotency-Key` |

## Use it with the OpenAI SDK

Pass the key as an extra header. The SDK reuses the same header across its automatic retries, which is exactly the case this is for.

```python
import os
import uuid
from openai import OpenAI

client = OpenAI(
    base_url="https://api.sference.com/v1",
    api_key=os.environ["SFERENCE_API_KEY"],
)

key = str(uuid.uuid4())  # one key per logical request

response = client.chat.completions.create(
    model="zai-org/GLM-5.3",
    messages=[{"role": "user", "content": "Summarize this document."}],
    extra_headers={"Idempotency-Key": key},
)
```

If you write your own retry loop, keep the key across attempts:

```python
def complete_once(prompt: str, key: str):
    return client.chat.completions.create(
        model="zai-org/GLM-5.3",
        messages=[{"role": "user", "content": prompt}],
        extra_headers={"Idempotency-Key": key},
    )

key = str(uuid.uuid4())
for attempt in range(3):
    try:
        return complete_once(prompt, key)
    except Exception:
        if attempt == 2:
            raise
```

Do **not** generate a new key for each retry — that defeats the deduplication.

## Streaming

Only the terminal result is retained. A replay of a streamed request is delivered as a valid SSE stream with the full content in one delta, the terminal finish reason, optional usage, and `[DONE]`. It is correct, but not token-by-token.

## Batches

A batch is a resource from the moment it is created, so a replay returns the same batch object in its current state (`pending`, `running`, `completed`, …) rather than a `409`. The batch window still sets the scheduling tier.

## Zero data retention

Replays render from the original request row, so zero-data-retention and archiving behave exactly as they do for the original request. If a ZDR result has already been scrubbed, a replay returns `409 idempotency_result_unavailable`; use a new key to retry.

- [Requests at scale](https://sference.com/docs/guides/requests-at-scale): Fan-out patterns that pair well with idempotent retries.
- [Max queue wait](https://sference.com/docs/guides/max-queue-wait): Fail fast on congestion and retry with the same key.
- [Batches](https://sference.com/docs/guides/batches): Submit bulk work with a key that survives retries.
