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

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:
KindThe key is…Used by
Client-supplieda field in the request body, idempotency_keyrule writes (POST …/rules), consent attestations, source syncs, alert subscriptions
Server-derivedcomputed deterministically from stable request fields (e.g. (rule_ref, app_did)), so two honest retries collide on purposeenforcement 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.
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 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:
1

First application → 201 Created

The write happens. You get 201 and the freshly signed receipt.
2

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.
3

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.
You sentServer stateHTTPMarker
New (scope, key)applied once201 Created
Same key + same payloadunchanged200 OKOCSS-Replay: original
Same key + changed payloadunchanged (rejected)409 Conflictclass: "replay"
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.

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
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
Response · both calls
{
  "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 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:
curl
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.
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.
Response · 409 conflicting replay
{
  "error": "Conflict",
  "message": "idempotency key reused with a different payload",
  "code": 409,
  "class": "replay"
}

Retry rules

SituationRetry?How
Network error / timeout / 5xxYesResend the same key + same payload; a duplicate is absorbed as a faithful replay
429YesWait for reset, then resend the same key + payload
200 + OCSS-Replay: originalNoAlready applied — you have the original receipt
409 replayNoYou changed the payload; reuse the original bytes, or mint a fresh key for a genuinely new write

Next steps

Errors

The 409 replay class in the full error table.

Rate limits

How to retry safely without tripping the throttle.