Skip to main content
This is the signed OCSS data-plane — the wire the @phosra/gatekeeper SDK drives when a platform enforces child-safety rules at the edge. These are not plain REST: profile reads are RFC 9421-signed by the resolver, binding writes require an accredited caller signature, and profile responses are root-verifiable signed documents.
Building a parental-controls product (families, children, policies)? That surface is the Phosra product API — a different plane. This data-plane is for enforcement partners running a gatekeeper. Start with the Platform quickstart if you are new.

The nine operations

Bind an endpoint

POST /enforcement-endpoints — mint a §9.3(b) bound-resolver label for a child.

Rotate a label

POST /enforcement-endpoints/{id}/rotate — issue a fresh label, invalidate the old.

Fetch the signed profile

GET /enforcement-profiles/{endpoint_id} — poll the router-signed enforcement profile.

Confirm enforcement

POST /enforcement-confirmations — submit a signed §8.3.8 result receipt.

Sandbox: run the connect ceremony

POST /sandbox/test-connect — the provider side of connect, to your own DID.

Sandbox: mint test consent

POST /sandbox/consent-attestations — satisfy the consent-first gate with no roster edit.

Sandbox OAuth: authorize

GET /oauth/authorize — reference parent-consent authorize leg.

Sandbox OAuth: token

POST /oauth/token — exchange an authorization code for an access token.

Sandbox OAuth: profiles

GET /oauth/profiles — bearer-scoped seeded child list.

Base URL

EnvironmentBase URL
Hosted sandbox (partner-facing, seeded, stable)https://phosra-api-sandbox-production.up.railway.app/api/v1
Local developmenthttp://localhost:8080/api/v1
The /oauth/* reference legs live on the census host root, not under /api/v1 — use https://phosra-api-sandbox-production.up.railway.app/oauth/authorize.
Every example on these pages was executed against the hosted sandbox above and the real responses are pasted in verbatim. The sandbox reference OAuth and sandbox/* legs are gated on PHOSRA_ENV==sandbox — they return 404 on production, by design.

How a signed request is built

Every write and every profile read on this plane carries an RFC 9421 Ed25519 signature under your Trust-List-published key. The census reconstructs the same signature base and verifies provenance + accreditation before processing. Copy-paste signers in curl+openssl, TypeScript, Python, and Go — each verified end-to-end against this sandbox — live in the request-signing guide; the TypeScript signRequest from @openchildsafety/ocss is shown below.
The Phosra signing-requests guide showing four language tabs: Python, Go, TypeScript, and curl+openssl
signer.ts
import { signRequest } from "@openchildsafety/ocss"

const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"

// Your Trust-List key. In the sandbox, the seeded emitter "loopline" signs with a
// deterministic test seed (slug bytes right-padded 0x01 to 32 B); in production this
// is your real Ed25519 seed, kept server-side only.
const seed  = new Uint8Array(Buffer.from("bG9vcGxpbmUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE", "base64url"))
const keyID = "did:ocss:loopline#2026-06"

export async function signedPost(path: string, body: unknown) {
  const targetURI = BASE + path
  const bodyText  = JSON.stringify(body)
  const headers   = signRequest({
    method: "POST",
    targetURI,
    body: new TextEncoder().encode(bodyText),
    keyID, seed,
    created: Math.floor(Date.now() / 1000),
  })
  headers["Content-Type"] = "application/json"
  return fetch(targetURI, { method: "POST", headers, body: bodyText })
}

export async function signedGet(path: string) {
  const targetURI = BASE + path
  const headers   = signRequest({ method: "GET", targetURI, keyID, seed, created: Math.floor(Date.now() / 1000) })
  return fetch(targetURI, { method: "GET", headers })
}
signRequest covers @method, @target-uri, ocss-spec-version, and — when a body is present — content-digest, in that literal order. It emits:
OCSS-Spec-Version: OCSS-v1.0-pre
Content-Digest: sha-256=:NIK7mwcaIj22qhAF1stpRE6wKwKO4nZMK0PqGsjdT5A=:
Signature-Input: ocss=("@method" "@target-uri" "ocss-spec-version" "content-digest");created=1783315514;keyid="did:ocss:loopline#2026-06";alg="ed25519"
Signature: ocss=:qAR6e0phugzYAz4APa7xY309kOeKeBQm00…:
A signature is bound to the method, URL, body, and created timestamp — it cannot be copy-pasted into a static curl and reused (it will fail signature verification or expire). Compute headers per-request with a signer. The runnable curl+openssl / TypeScript / Python / Go recipes each mint fresh headers from a monotonic clock; the shell wire-shape examples on the reference pages are for illustration only.

The end-to-end loop

A gatekeeper binds once, then polls and confirms on a rotation cadence:
1

Bind

POST /enforcement-endpoints returns your endpoint_id_label (the credential), binding_id (for rotation), and connect_secret (for inbound connect verification) — all three exactly once.
2

Poll

GET /enforcement-profiles/{endpoint_id_label} returns a router-signed profile. Verify the signature to the router key, then to the root, before trusting categories[].
3

Enforce + confirm

Apply the decision locally, then POST /enforcement-confirmations with a signed §8.3.8 receipt (or call verdict.confirm("applied") in the SDK).
4

Rotate

Periodically POST /enforcement-endpoints/{binding_id}/rotate to get a fresh label; the prior label 404s immediately.