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:
| 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 |
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:
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.| 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" |
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-derivedidempotency_key — proof that the operation is deduplicated on a deterministic
identity rather than on wall-clock time.
TypeScript
Response · both calls
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
200 with OCSS-Replay: original and the exact
receipt bytes from the first call.
The one pitfall: a changed payload
A409 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.
Response · 409 conflicting 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, 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
Errors
The
409 replay class in the full error table.Rate limits
How to retry safely without tripping the throttle.