Skip to main content
POST
/
enforcement-endpoints
Signed curl (wire shape)
# Signature-Input/Signature/Content-Digest are per-request — compute with the SDK
# (see the Node tab); a static signature cannot be reused. Body shown for reference.
curl -X POST https://phosra-api-sandbox-production.up.railway.app/api/v1/enforcement-endpoints \
  -H "Content-Type: application/json" \
  -H "OCSS-Spec-Version: OCSS-v1.0-pre" \
  -H 'Signature-Input: ocss=("@method" "@target-uri" "ocss-spec-version" "content-digest");created=1783315514;keyid="did:ocss:loopline#2026-06";alg="ed25519"' \
  -H 'Signature: ocss=:<base64-ed25519-sig>:' \
  -H 'Content-Digest: sha-256=:<base64-sha256-of-body>:' \
  -d '{"audience_did":"did:ocss:loopline","child_ref":"child:5ba0d00c-0000-4000-8000-0000000000c1","window_seconds":3600}'
{
  "endpoint_id_label": "U49ctRQSAz5TEpBSuHZxWhVaW28J7qfEvI0grdOxaRE",
  "binding_id": "e376e187-cecb-42ed-9d19-aa05de1043cd",
  "connect_secret": "PyBm20VF2xm78D-HOgi0Jg3ldKSguAXy_H5DRIpNhnQ"
}
Mints a §9.3(b) bound-resolver label for one child and returns the label, a binding_id, and a connect_secretall three returned exactly once. The caller must be an accredited resolver with a valid RFC 9421 signature, and there must be an active consent_attestation for (audience_did, child_ref) — the §4.2.2 precondition. No consent → 403.
endpoint_id_label and connect_secret are secret credentials returned once. Presenting the label as the {endpoint_id} path segment on a profile GET is the authentication — store both immediately and never log them.

Worked example

In the sandbox you satisfy the consent precondition with a single self-serve call (mint test consent), then bind. This runs end-to-end against the hosted sandbox:
import { signRequest } from "@openchildsafety/ocss"

const BASE  = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
const seed  = new Uint8Array(Buffer.from("bG9vcGxpbmUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE", "base64url"))
const keyID = "did:ocss:loopline#2026-06"

async function signedPost(path, body) {
  const targetURI = BASE + path, 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 })
}

// 1. Satisfy the consent-first gate (sandbox self-serve) — returns target_ref.
const consent = await (await signedPost("/sandbox/consent-attestations", { band: "13_15", consent_scope: "collection_parental_authority" })).json()

// 2. Bind the endpoint for that consented child.
const res = await signedPost("/enforcement-endpoints", {
  audience_did:   "did:ocss:loopline",
  child_ref:      consent.target_ref,
  window_seconds: 3600,
})
console.log(res.status, await res.json())
The signer (sign_request / SignRequest) is defined once in the request-signing guide — copy it in, or use the @openchildsafety/ocss SDK’s signRequest. That guide’s Python, Go, and curl+openssl recipes were each run end-to-end against this sandbox and returned a real 201.
Real 201 response (captured from the hosted sandbox):
{
  "binding_id": "c8ff82dd-4aa8-4a52-a235-ae13717eb4c8",
  "connect_secret": "pRrwmJa5gDmlCoK7pga9TGXAQ-kdZG6XBJkmkBjJ8lE",
  "endpoint_id_label": "iGrFqzp43O0S9YTNN2oAT6zMzcugEX_EwZbraWrE1AA"
}
Next: poll the signed profile with the endpoint_id_label, and rotate using the binding_id.

Body

application/json

Binding-leg mint request. Source: endpointMintBody in internal/ocsshttp/handler_endpoints.go. Required: audience_did, child_ref, window_seconds >= 1.

audience_did
string
required

DID of the resolving platform (e.g. "did:ocss:snaptr").

Example:

"did:ocss:snaptr"

child_ref
string
required

"child:" — the child the resolver is binding to.

Example:

"child:11111111-1111-4111-8111-111111111111"

window_seconds
integer<int64>
required

Profile validity window in seconds. The census mints a window of this duration and increments rotation_epoch accordingly.

Required range: x >= 1
statutory_fail_closed
string[]

Optional list of category slugs the deployment declares fail-closed (§9.1 floor 3 / §9.2). fail_mode="closed" appears on those CategoryEntry rows.

Response

Endpoint bound. Store endpoint_id_label and connect_secret immediately.

Binding mint result. Source: EndpointHandlers.Mint response in internal/ocsshttp/handler_endpoints.go. All three fields are returned exactly once; endpoint_id_label and connect_secret are never logged.

endpoint_id_label
string
required

High-entropy §9.3(b) bound-resolver label. Presenting it as the {endpoint_id} path segment in GET /enforcement-profiles/{endpoint_id} IS the authentication credential — treat as a secret, never log.

binding_id
string<uuid>
required

UUID of the persisted binding row (for rotation).

connect_secret
string
required

HMAC-SHA256 secret for verifying X-Phosra-Signature on inbound connect-leg deliveries (feed into gk.config({ connectSecret })). Returned ONCE; store it immediately.