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

# Idempotency

> How Phosra guarantees a duplicate write never has a duplicate effect — where the key lives, what a faithful replay returns, and the one status that means you changed the payload.

Networks retry. Phosra makes that safe: a state-changing call carries an
**idempotency key**, and a redelivery of that key never produces a second effect.
"Duplicate delivery MUST NOT produce a duplicate consequence" is the §8.1 clause-3
contract, and it holds by construction — the state change, its signed receipt, the
audit rail row, and the key are all written in **one database transaction**, so a
duplicate rolls all four back together.

<Info>
  **Runnable.** The signed worked example below was executed live against
  `https://phosra-api-sandbox-production.up.railway.app` on **2026-07-06** using the
  published `@openchildsafety/ocss` signer (`v0.1.3`). The response bytes are pasted
  verbatim.
</Info>

## Where the key lives — there is no `Idempotency-Key` header

Unlike some APIs, Phosra does **not** read an `Idempotency-Key` HTTP header. A write
gets its key one of two ways:

| Kind                | The key is…                                                                                                                  | Used by                                                                               |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| **Client-supplied** | a field in the **request body**, `idempotency_key`                                                                           | rule writes (`POST …/rules`), consent attestations, source syncs, alert subscriptions |
| **Server-derived**  | computed deterministically from stable request fields (e.g. `(rule_ref, app_did)`), so two honest retries collide on purpose | enforcement confirmations, routing manifests, compliance bundles, alert emits         |

Either way the key is scoped to its operation (a frozen operation → namespace
table), so one operation's replay space can never bleed into another's.

<Warning>
  Do **not** send an `Idempotency-Key` header expecting dedup — it is ignored. Put
  `idempotency_key` in the JSON body where the endpoint accepts one, and freeze the
  whole payload before you attach it (see [pitfalls](#the-one-pitfall-a-changed-payload)).
</Warning>

## The two outcomes

Once a `(scope, key)` has been applied, redelivering it has exactly two possible
results — decided by whether the **payload** is byte-identical to the first one:

<Steps>
  <Step title="First application → 201 Created">
    The write happens. You get `201` and the freshly signed receipt.
  </Step>

  <Step title="Faithful replay → 200 + OCSS-Replay: original">
    Same key, **byte-identical** payload. Nothing runs a second time. You get `200`,
    the header **`OCSS-Replay: original`**, and the **byte-identical original
    receipt**. This is a success, not an error.
  </Step>

  <Step title="Conflicting replay → 409 replay">
    Same key, **different** payload. The key is bound to its first payload and cannot
    be rebound: `409 Conflict`, `class: "replay"`, and **nothing is applied**. The
    stored receipt is withheld.
  </Step>
</Steps>

| You sent                   | Server state         | HTTP           | Marker                  |
| -------------------------- | -------------------- | -------------- | ----------------------- |
| New `(scope, key)`         | applied once         | `201 Created`  | —                       |
| Same key + same payload    | unchanged            | `200 OK`       | `OCSS-Replay: original` |
| Same key + changed payload | unchanged (rejected) | `409 Conflict` | `class: "replay"`       |

<Note>
  **A faithful replay is success-shaped.** It returns the original bytes and never
  emits a failed-call receipt. The `200` vs `201` distinction is how you tell a
  fresh apply from a replay without diffing bodies — read the status and the
  `OCSS-Replay` header.
</Note>

## Worked example — a server-derived key, live

The sandbox consent-attestation route is a signed (RFC 9421) write. Send the same
signed request twice and the census returns a **stable, server-derived
`idempotency_key`** — proof that the operation is deduplicated on a deterministic
identity rather than on wall-clock time.

```typescript TypeScript theme={null}
import { signRequest } from "@openchildsafety/ocss"; // published: v0.1.3

const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
const seed = new Uint8Array(
  Buffer.from("bG9vcGxpbmUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE", "base64url"),
);
const keyID = "did:ocss:loopline#2026-06";
const target = `${BASE}/sandbox/consent-attestations`;
const bodyText = JSON.stringify({ band: "13_15", consent_scope: "collection_parental_authority" });

async function mint() {
  const headers = signRequest({
    method: "POST", targetURI: target,
    body: new TextEncoder().encode(bodyText),
    keyID, seed, created: Math.floor(Date.now() / 1000),
  });
  headers["Content-Type"] = "application/json";
  const res = await fetch(target, { method: "POST", headers, body: bodyText });
  return { status: res.status, replay: res.headers.get("ocss-replay"), body: await res.json() };
}

console.log(await mint());
console.log(await mint()); // same derived key both times
```

```json Response · both calls theme={null}
{
  "ok": true,
  "app_ref": "did:ocss:loopline",
  "target_ref": "child:5ba0d00c-0000-4000-8000-0000000000c1",
  "standing_ref": "consent:attestation:sbx-consent:did:ocss:loopline:child:5ba0d00c-0000-4000-8000-0000000000c1",
  "idempotency_key": "sbx-consent:did:ocss:loopline:child:5ba0d00c-0000-4000-8000-0000000000c1",
  "band": "13_15",
  "consent_scope": "collection_parental_authority",
  "expiry": "2027-07-06T07:02:30Z"
}
```

The `idempotency_key` is derived from `(caller DID, target child)` — no timestamp —
so it is identical on every honest retry. Pass the returned `target_ref` straight
into [`POST /enforcement-endpoints`](/api-reference/data-plane/bind-endpoint) to
finish the consent-first mint.

## Client-supplied keys

Where an endpoint accepts a client key (rule writes, source syncs), put it in the
body and reuse it — with the **same payload** — on every retry:

```bash curl theme={null}
BASE=https://phosra-api-sandbox-production.up.railway.app
# Reuse ONE key + the SAME body across retries. First call applies; retries replay.
KEY="rules-2026-07-06-batch-01"
curl -s -X POST "$BASE/api/v1/policies/$POLICY_ID/rules" \
  -H "Authorization: Bearer $PHOSRA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"idempotency_key":"'"$KEY"'","rule_category":"content_rating","age_band":"13_15","...":"..."}'
```

On a faithful retry this returns `200` with `OCSS-Replay: original` and the exact
receipt bytes from the first call.

## The one pitfall: a changed payload

A `409 replay` almost always means a retried request **mutated its body** between
attempts — a regenerated timestamp, a fresh nonce, or a reordered array. The key is
frozen to its first payload; a different payload under the same key is rejected.

<Warning>
  **Freeze the payload before you attach the key.** Serialize the body once, keep
  both the key and those exact bytes, and resend them together on every retry. Do
  not rebuild the JSON (and never regenerate an inner timestamp) between attempts.
</Warning>

```json Response · 409 conflicting replay theme={null}
{
  "error": "Conflict",
  "message": "idempotency key reused with a different payload",
  "code": 409,
  "class": "replay"
}
```

## Retry rules

| Situation                       | Retry?  | How                                                                                                        |
| ------------------------------- | ------- | ---------------------------------------------------------------------------------------------------------- |
| Network error / timeout / `5xx` | **Yes** | Resend the **same key + same payload**; a duplicate is absorbed as a faithful replay                       |
| `429`                           | **Yes** | Wait for [reset](/rate-limits#handle-it-wait-for-the-reset-then-retry), then resend the same key + payload |
| `200` + `OCSS-Replay: original` | **No**  | Already applied — you have the original receipt                                                            |
| `409 replay`                    | **No**  | You changed the payload; reuse the original bytes, or mint a fresh key for a genuinely new write           |

## Next steps

<CardGroup cols={2}>
  <Card title="Errors" icon="triangle-exclamation" href="/errors#idempotency-and-replay">
    The `409 replay` class in the full error table.
  </Card>

  <Card title="Rate limits" icon="gauge-high" href="/rate-limits">
    How to retry safely without tripping the throttle.
  </Card>
</CardGroup>
