Skip to main content
POST
The enforcing app POSTs its own signed enforcement_result receipt attesting that it applied a rule. The census verifies the caller’s RFC 9421 signature (the caller must equal the receipt’s signer — first-person binding, §8.3.8 cl.1) and records the receipt verbatim. It does not re-sign or append to the Receipt rail. The request body is the full outer receipt (id, type, spec, body, key_id, sig), not just the inner EnforcementResultBody. Three spec-pinned invariants:
  • §6.2 NOT-A-WRITE — this is the app’s attestation, not a policy write.
  • §10.6 NEVER-GATES-THE-RULE — a missing or rejected confirmation never blocks or revives the rule’s enforcement.
  • HONEST CEILING (§8.3.8) — “reported applied, never proven applied.”

Worked example

The @phosra/gatekeeper SDK is the supported path — verdict.confirm() constructs and signs the receipt, then POSTs it for you:
Hand-rolling? Use public @openchildsafety/ocss@0.1.5 and wrap the receipt with receiptToWire() before POSTing. The helper was introduced in 0.1.4 and remains present in public 0.1.5; the qualified 0.1.6 Railway candidate is not a public npm release. signReceipt returns body as a Uint8Array of canonical bytes; JSON.stringify on the raw receipt numeric-keys it ({"0":123,…}), which the census rejects as a non-canonical body — 401 {"class":"signature_invalid","message":"enforcement_result sender_signature failed verification … (§8.3.8 cl.4)"}. receiptToWire(receipt) decodes body to nested JSON so it rides as the verbatim canonical blob the census verifies (this is exactly what gk.confirm() does internally). The signed bytes are unchanged. verdict.confirm() remains the easiest path; the raw Node tab below uses receiptToWire. On @openchildsafety/ocss@0.1.3 and earlier the standalone raw path fails — upgrade to public 0.1.5.
The raw sign_receipt here is a faithful port of @openchildsafety/ocss’s signReceipt (JCS over {body, spec, type}, §8.3.8 D-9 boundary) and reproduces that SDK function’s bytes exactly — but those bytes are not what the live census currently accepts (see the warning above). The 201 capture below was produced by verdict.confirm(), the reliable path. §8.3.8 is a first-person attestation: the census verifies your RFC 9421 signature, the first-person binding (the signer must equal the receipt’s signer), a non-empty rule_ref, and dedup — it does not currently verify that the rule_ref corresponds to a profile you actually enforced, so a well-formed but fabricated rule_ref with a valid signature is recorded (201), not rejected. Attesting only to rules you truly applied is the enforcing app’s own integrity obligation (§8.3.8 cl.1), not a guarantee the census checks for you.

What comes back

The census records your receipt and echoes it back byte-for-byte — it never re-signs or wraps it (§8.3.8 cl.3: the app’s own receipt IS the artifact). So the 201 body is the exact receipt you POSTed. A byte-identical re-submit returns 200 with an OCSS-Replay: original header (the census deduplicates on rule_ref + caller DID); the same rule_ref with different content is a 409 replay.
The response bodies below are real wire captures — a live did:ocss:loopline-signed POST /enforcement-confirmations against the sandbox census (https://phosra-api-sandbox-production.up.railway.app), then the byte-identical re-POST. Not hand-written.
201 Created — your receipt, echoed verbatim
200 OK — idempotent re-submit (byte-identical body)
A 409 Conflict is returned only when the same rule_ref is re-submitted with different content (a D-13 replay), never for a faithful re-submit:
409 Conflict — same rule_ref, different content
rule_ref must be a live per-child reference from a profile the caller actually enforced (§8.3.8 cl.1 first-person binding); the receipt signer must equal the RFC 9421 caller. A fabricated rule_ref or a signer/caller mismatch is rejected 401/403 — which is why the SDK’s verdict.confirm() (it carries the exact rule_ref from the verdict) is the reliable path.

Body

application/json

Full §8.3.8 signed receipt (the app's first-person apply attestation). The confirmation POST body IS this outer receipt — not the inner body alone. Sig covers canon.Marshal({body, spec, type}); id and key_id ride outside the signed bytes (D-9 exposure rule). Source: receipt.Receipt in internal/ocss/receipt/types.go with body = EnforcementResultBody.

id
string
required

rcpt_ — rides outside signed bytes (D-9).

Example:

"rcpt_01JTEST00000000000000000"

type
enum<string>
required

Always "enforcement_result" for §8.3.8 confirmations.

Available options:
enforcement_result
spec
string
required

OCSS spec version — §11.4 domain separation.

Example:

"OCSS-v1.0-pre"

body
object
required

JSON-encoded EnforcementResultBody. Kept as raw bytes: re-marshaling would alter the canonical signed bytes and break verification.

key_id
string
required

DID key id of the submitting enforcing app (rides outside signed bytes). Verified indirectly: the signature only verifies if the resolved key matches.

Example:

"did:ocss:loopline#2026-06"

sig
string
required

"ed25519:" + base64url-raw signature over canon.Marshal({body,spec,type}).

Response

Idempotent re-submit of the same receipt (same (rule_ref, app_did), byte-identical content). The body is the original receipt echoed verbatim — identical to the 201 body — and the response carries an OCSS-Replay: original header so a client can tell the write already landed.

Full §8.3.8 signed receipt (the app's first-person apply attestation). The confirmation POST body IS this outer receipt — not the inner body alone. Sig covers canon.Marshal({body, spec, type}); id and key_id ride outside the signed bytes (D-9 exposure rule). Source: receipt.Receipt in internal/ocss/receipt/types.go with body = EnforcementResultBody.

id
string
required

rcpt_ — rides outside signed bytes (D-9).

Example:

"rcpt_01JTEST00000000000000000"

type
enum<string>
required

Always "enforcement_result" for §8.3.8 confirmations.

Available options:
enforcement_result
spec
string
required

OCSS spec version — §11.4 domain separation.

Example:

"OCSS-v1.0-pre"

body
object
required

JSON-encoded EnforcementResultBody. Kept as raw bytes: re-marshaling would alter the canonical signed bytes and break verification.

key_id
string
required

DID key id of the submitting enforcing app (rides outside signed bytes). Verified indirectly: the signature only verifies if the resolved key matches.

Example:

"did:ocss:loopline#2026-06"

sig
string
required

"ed25519:" + base64url-raw signature over canon.Marshal({body,spec,type}).