> ## Documentation Index
> Fetch the complete documentation index at: https://docs.phosra.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Data-plane API reference

> The signed OCSS data-plane the @phosra/gatekeeper SDK drives — bind an endpoint, poll a router-signed enforcement profile, and confirm enforcement.

This is the **signed OCSS data-plane** — the wire the [`@phosra/gatekeeper`](/sdks/mcp-server)
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.

<Note>
  Building a **parental-controls product** (families, children, policies)? That surface is
  the [Phosra product API](/api-reference/overview) — a different plane. This data-plane is
  for **enforcement partners** running a gatekeeper. Start with the
  [Platform quickstart](/quickstart) if you are new.
</Note>

## The nine operations

<CardGroup cols={2}>
  <Card title="Bind an endpoint" icon="link" href="/api-reference/data-plane/bind-endpoint">
    `POST /enforcement-endpoints` — mint a §9.3(b) bound-resolver label for a child.
  </Card>

  <Card title="Rotate a label" icon="arrows-rotate" href="/api-reference/data-plane/rotate-endpoint">
    `POST /enforcement-endpoints/{id}/rotate` — issue a fresh label, invalidate the old.
  </Card>

  <Card title="Fetch the signed profile" icon="file-signature" href="/api-reference/data-plane/fetch-profile">
    `GET /enforcement-profiles/{endpoint_id}` — poll the router-signed enforcement profile.
  </Card>

  <Card title="Confirm enforcement" icon="circle-check" href="/api-reference/data-plane/submit-confirmation">
    `POST /enforcement-confirmations` — submit a signed §8.3.8 result receipt.
  </Card>

  <Card title="Sandbox: run the connect ceremony" icon="handshake" href="/api-reference/data-plane/sandbox-test-connect">
    `POST /sandbox/test-connect` — the provider side of connect, to your own DID.
  </Card>

  <Card title="Sandbox: mint test consent" icon="user-check" href="/api-reference/data-plane/sandbox-consent-attestation">
    `POST /sandbox/consent-attestations` — satisfy the consent-first gate with no roster edit.
  </Card>

  <Card title="Sandbox OAuth: authorize" icon="lock-open" href="/api-reference/data-plane/oauth-authorize">
    `GET /oauth/authorize` — reference parent-consent authorize leg.
  </Card>

  <Card title="Sandbox OAuth: token" icon="key" href="/api-reference/data-plane/oauth-token">
    `POST /oauth/token` — exchange an authorization code for an access token.
  </Card>

  <Card title="Sandbox OAuth: profiles" icon="children" href="/api-reference/data-plane/oauth-profiles">
    `GET /oauth/profiles` — bearer-scoped seeded child list.
  </Card>
</CardGroup>

## 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`.

<Note>
  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.
</Note>

## 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**](/concepts/signing-requests); the TypeScript `signRequest` from
`@openchildsafety/ocss` is shown below.

<Frame caption="The request-signing guide — copy-paste Ed25519 signers in Python, Go, TypeScript, and curl+openssl, each verified against the hosted sandbox.">
  <img src="https://mintcdn.com/phosra/o0ansqUg4hqAQihW/images/signing-requests-tabs.jpg?fit=max&auto=format&n=o0ansqUg4hqAQihW&q=85&s=59b0ffb6f5840ddd297f43cf1afb505d" alt="The Phosra signing-requests guide showing four language tabs: Python, Go, TypeScript, and curl+openssl" width="1225" height="1400" data-path="images/signing-requests-tabs.jpg" />
</Frame>

```ts signer.ts theme={null}
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:

```http theme={null}
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…:
```

<Warning>
  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](/concepts/signing-requests) each mint fresh
  headers from a monotonic clock; the shell wire-shape examples on the reference pages are for
  illustration only.
</Warning>

## The end-to-end loop

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

<Steps>
  <Step title="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.
  </Step>

  <Step title="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[]`.
  </Step>

  <Step title="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).
  </Step>

  <Step title="Rotate">
    Periodically `POST /enforcement-endpoints/{binding_id}/rotate` to get a fresh label;
    the prior label 404s immediately.
  </Step>
</Steps>
