Data-plane API reference
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 and binding writes use RFC 9421 signatures, and profile responses are
root-verifiable signed documents. The public environment is a synthetic sandbox; its tier fields
exercise the proposed standing checks and are not independent accreditation.
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
| Environment | Base URL |
|---|---|
| Hosted sandbox (partner-facing, seeded, stable) | https://phosra-api-sandbox-production.up.railway.app/api/v1 |
| Local development | http://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.
The examples target the hosted sandbox and use synthetic data. Secret-bearing values are
redacted. The reference OAuth and sandbox/* legs are sandbox-only preview surfaces.
How a signed request is built
Every write and every profile read on this plane carries an RFC 9421 Ed25519 signature
under your sandbox-registry key. The sandbox reconstructs the same signature base and exercises
provenance plus proposed standing checks before processing. Signers in curl+openssl, TypeScript,
Python, and Go live in the
request-signing guide; the TypeScript signRequest from
@openchildsafety/ocss is shown below.

import { signRequest } from "@openchildsafety/ocss"
const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
// Self-register a synthetic sandbox identity, then keep its private seed outside source control.
const seed = new Uint8Array(Buffer.from(process.env.OCSS_SANDBOX_TEST_SEED_B64URL!, "base64url"))
const keyID = process.env.OCSS_SANDBOX_KEY_ID!
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=<unix-seconds>;keyid="<your-sandbox-key-id>";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:
- 1Bind
POST /enforcement-endpointsreturns yourendpoint_id_label(the credential),binding_id(for rotation), andconnect_secret(for inbound connect verification) — all three exactly once. - 2Poll
GET /enforcement-profiles/{endpoint_id_label}returns a router-signed profile. Verify the signature to the router key, then to the root, before trustingcategories[]. - 3Enforce + confirm
Apply the
decisionlocally, thenPOST /enforcement-confirmationswith a signed §8.3.8 receipt (or callverdict.confirm("applied")in the SDK). - 4Rotate
Periodically
POST /enforcement-endpoints/{binding_id}/rotateto get a fresh label; the prior label 404s immediately.