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

# Idempotency

# Idempotency

Network requests fail. Clients retry. Without a deduplication contract,
a retried `POST` quietly creates two records.

The Scripe API gives every write endpoint a single contract for safe
retries: pass an `Idempotency-Key` header. The server records the
response under that key for **24 hours** and replays it verbatim on
subsequent requests with the same body — including the original status
code.

***

## How to use it

Generate a unique key per logical operation (a UUID is the obvious
default — anything 1–64 chars matching `[A-Za-z0-9_-]` works) and send
it on every retry of that operation:

```bash theme={null}
KEY=$(uuidgen)

curl -i https://api.scripe.io/v1/notes \
  -X POST \
  -H "Authorization: Bearer scripe_sk_live_…" \
  -H "Scripe-Api-Version: 2026-08-01" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $KEY" \
  -d '{ "projectId": "proj_…", "content": "Hi" }'
```

The first request runs the handler. The response includes:

```http theme={null}
HTTP/1.1 200 OK
Idempotent-Replayed: false
```

Within the next 24h, the same key + same body returns the cached
response:

```http theme={null}
HTTP/1.1 200 OK
Idempotent-Replayed: true
```

Your application logic does NOT need to special-case the replay path —
the body is the original 200 envelope.

***

## Validation rules

* **Key shape**: 1–64 chars, regex `^[A-Za-z0-9_-]{1,64}$`. Anything
  longer or with disallowed characters is silently ignored (treated as
  if no header was sent).
* **Scope**: keys are scoped to `(workspaceId, route)`. The same value
  can be reused safely across different endpoints.
* **Body hashing**: we hash the canonicalised JSON body of the request
  (object keys sorted, no non-significant whitespace) with SHA-256. The
  hash, NOT the body itself, is what we compare on replay.
* **Window**: 24 hours from the first request. After expiry the key can
  be reused with any body.

***

## Conflict (409 `idempotency_key_conflict`)

If you send the same `Idempotency-Key` with a **different** body inside
the 24h window, the server rejects the request:

```http theme={null}
HTTP/1.1 409 Conflict
Content-Type: application/json

{
  "error": {
    "code": "idempotency_key_conflict",
    "message": "Idempotency-Key was already used for a different request body in the last 24h.",
    "request_id": "req_…",
    "docs_url": "https://docs.scripe.io/api/v1/errors/idempotency_key_conflict"
  }
}
```

Practical fix: generate a fresh key when you mean a fresh operation.
Reuse a key only when retrying a **literal** retry of an unchanged body.

***

## Body size cap

Buffering the request body for hashing has an upper bound (256 KB by
default; some endpoints set their own limit). Bodies larger than the
cap return `413 payload_too_large` even when an `Idempotency-Key` is
sent. The cap on each endpoint matches its content cap, so this only
fires on truly oversize inputs.

***

## What's NOT idempotent

* `GET` endpoints. They're read-only — `Idempotency-Key` on a `GET` is
  ignored.
* Side effects you trigger via webhooks fired by the API (the webhook
  delivery itself has its own idempotency layer: receivers should
  dedupe on `event.id`).
* Background jobs queued by `POST /v1/posts/generations` (coming in a
  later release) — those expose their own job-id-based idempotency.

***

## When to use it (always, but here's the threshold)

Whenever the cost of a duplicate write is non-zero. Notes and posts
both fall into that category — a duplicate post in the customer's queue
is annoying. We strongly recommend including an `Idempotency-Key` on
**every** write request as a default.
