Submit a signed §8.3.8 enforcement-result receipt

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/[email protected] 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/[email protected] and earlier the standalone raw path fails — upgrade to public 0.1.5.

ts
import { createGatekeeper } from "@phosra/gatekeeper"
 
const seed = new Uint8Array(Buffer.from(process.env.OCSS_SANDBOX_TEST_SEED_B64URL!, "base64url"))
const keyID = process.env.OCSS_SANDBOX_KEY_ID!
const gk = createGatekeeper({
  platformDid:          process.env.OCSS_SANDBOX_DID!,
  platformKeyId:        keyID,
  gatekeeperSigningKey: { seed, keyID },
  censusBaseUrl:        "https://phosra-api-sandbox-production.up.railway.app",
  trustRootXB64Url:     process.env.PHOSRA_TRUST_ROOT_X!,
  endpointId:           process.env.OCSS_SANDBOX_ENDPOINT_ID_LABEL!,
})
 
const verdict = gk.isAllowed({ category: "infinite_scroll_block" })
// ...apply the decision at the edge, then attest the outcome:
await verdict.confirm("applied")   // signs the §8.3.8 receipt + POSTs /enforcement-confirmations

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 the sandbox profile whose test decision you applied, 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.

json
{
  "id": "rcpt_01KWVD0ABX041061050R3GG28A",
  "type": "enforcement_result",
  "spec": "OCSS-v1.0-pre",
  "body": {
    "applied_at": "2026-07-06T09:43:59.612Z",
    "enforcement_state": "applied",
    "envelope_ref": "env_9f2c1a7e",
    "method_class": "platform_gate",
    "rule_ref": "rr_loopline_infinite_scroll_block"
  },
  "key_id": "did:ocss:loopline#2026-06",
  "sig": "ed25519:mGm30Y7TKrhTnTXH8fq2H8BaaVwFGOJCmJpI_F1hqefhCf9u9I5TRZawk2t12KSiq0pyuCbgzxjeQF11djnUAA"
}
http
HTTP/2 200
content-type: application/json
ocss-replay: original

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:

json
{
  "error": "Conflict",
  "message": "(rule_ref, app_did) replayed with a different enforcement_result (D-13)",
  "code": 409,
  "class": "replay"
}

rule_ref must be a per-child sandbox reference from the profile used for the test decision (§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.