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

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.

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.

The Phosra signing-requests guide showing four language tabs: Python, Go, TypeScript, and curl+openssl
The request-signing guide — copy-paste Ed25519 signers in Python, Go, TypeScript, and curl+openssl, each verified against the hosted sandbox.
ts
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:

http
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:

  1. 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. 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. 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. 4
    Rotate

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