Skip to main content
Before a provider can complete the connect ceremony and start delivering endpoint_id_labels to your app, your platform registers one endpoint with the census. Registration:
  1. declares your webhook URL — the base URL your POST /api/ocss/connect receiver lives under, and
  2. returns two secrets exactly once — the endpoint_id_label (your §9.3(a) registration name) and the connect_secret (the HMAC key that authenticates inbound connect-leg deliveries).
This is the EXT-04 §3.5 out-of-band establishment step made a real operation. Reference implementation: internal/ocsshttp/handler_platform_endpoints.go (Mint) over internal/ocss/profile/platform.go (MintPlatformEndpoint).
One endpoint, self-scoped. You register your own platform DID’s endpoint — the signed request’s caller DID must equal the {did} in the path. A mismatch answers the same 404 an unknown platform answers (no existence leak). Re-minting the same (did, connect_url) pair rotates both credentials — that is the EXT-04 §3.5 re-issue path.

Prerequisites

ItemSource
did:ocss:<your-slug> on the Trust ListSandbox: self-serve — POST /api/v1/advisors/self-register or phosra register. Production: apply for accreditation.
Ed25519 signing key pair@openchildsafety/ocss — a SenderKey is { seed, keyID }; the same key your DID publishes on its Trust-List entry.
Canonical sandbox census URLhttps://phosra-api-sandbox-production.up.railway.app — the one canonical partner sandbox host (see Conformance status).

Step 1 — Mint your endpoint — POST /api/v1/platforms/{did}/endpoints

The request is RFC-9421 signed with your DID’s Ed25519 key (the census verifies the signature against your live Trust-List entry — provisional-tier self-registrations included). There is no phosra_ API key on this path; the census identifies you by signature, not by a bearer token. Request body:
FieldTypeRequiredMeaning
connect_urlstringyesAbsolute base URL your POST /api/ocss/connect receiver lives under. HTTPS required outside sandbox (sandbox admits plain http for private-network sim receivers). No query, fragment, or embedded credentials; a trailing / is trimmed.
capabilitiesstring[]noDeclared capability slugs (informative; the signed gatekeeper capabilities document remains the enforced artifact). Lowercase slugs ^[a-z][a-z0-9_.:-]{0,63}$, max 128, de-duplicated.
Because the request must be signed, mint through the SDK rather than raw curl. Using signRequest from @openchildsafety/ocss:
import { signRequest } from "@openchildsafety/ocss"

const CENSUS = "https://phosra-api-sandbox-production.up.railway.app"
const did    = "did:ocss:snaptr"
const key    = mySenderKey                       // { seed: Uint8Array(32), keyID: "did:ocss:snaptr#snaptr-2026-06" }

const bodyText  = JSON.stringify({
  connect_url:  "https://snaptr.example.com/webhooks/ocss-connect",  // your receiver base
  capabilities: ["content_rating", "addictive_pattern_block"],
})
const targetURI = `${CENSUS}/api/v1/platforms/${encodeURIComponent(did)}/endpoints`

// RFC-9421: signRequest signs the request line + Content-Digest with STANDARD base64 (padded, +/).
const headers = signRequest({
  method:  "POST",
  targetURI,
  body:    new TextEncoder().encode(bodyText),
  keyID:   key.keyID,
  seed:    key.seed,
  created: Math.floor(Date.now() / 1000),
})
headers["Content-Type"] = "application/json"

const res  = await fetch(targetURI, { method: "POST", headers, body: bodyText })
const mint = await res.json()
201 response — the two secrets are here and nowhere else:
{
  "endpoint_id":       "3f0e…-uuid",
  "endpoint_id_label": "eplbl_9c2f…",
  "connect_secret":    "cs_7b41…",
  "connect_url":       "https://snaptr.example.com/webhooks/ocss-connect",
  "capabilities":      ["content_rating", "addictive_pattern_block"],
  "rotated_at":        "2026-07-03T18:22:04Z"
}
endpoint_id_label and connect_secret are returned only once. Per §9.3, the census stores only their SHA-256 digests (migration 191_ocss_platform_endpoints) — the cleartexts live in this 201 body and nowhere else. They are never logged, receipted, or exported. Persist both immediately (a secret manager, not source). Lose them and you re-mint, which rotates both and invalidates the old pair.

What each returned value is for

ValueUsed for
endpoint_idThe stable UUID of your registration (safe to log).
endpoint_id_labelYour §9.3(a) registration name — an opaque credential a provider references when it completes the connect ceremony to you. Treat like a session credential; never log it. Feeds PHOSRA_ENDPOINT_ID / createGatekeeper({ endpointId }).
connect_secretThe HMAC key that authenticates inbound connect-leg deliveries (Step 3). Feeds PHOSRA_CONNECT_SECRET / createGatekeeper({ connectSecret }).
connect_urlEcho of your declared webhook base (normalized).

Step 2 — Declare your webhook receiver

The connect_url you registered is the base your POST /api/ocss/connect receiver lives under. When a provider completes the connect ceremony, Phosra delivers the endpoint_id_label server-to-server to that URL. Stand up the receiver and hand it to @phosra/gatekeeper’s handleConnect — see Platform Quickstart → Step 1:
// POST /webhooks/ocss-connect  (must match the connect_url you minted, sans the appended path)
app.post("/webhooks/ocss-connect", express.raw({ type: "application/json" }), async (req, res) => {
  const webReq = new Request("https://snaptr.example.com/webhooks/ocss-connect", {
    method:  "POST",
    headers: { ...(req.headers as Record<string, string>) },
    body:    req.body,   // raw Buffer — do NOT pre-parse; the HMAC is over exact bytes
  })
  const response = await gk.handleConnect(webReq)
  res.status(response.status).json(await response.json())
})
The sender appends /api/ocss/connect to your registered base, so register the base (e.g. https://snaptr.example.com/webhooks/ocss-connect) — the SDK path convention and the Go sender (internal/ocss/link/sender.go) agree on this.

Step 3 — Verify inbound deliveries with the connect-secret

Phosra signs every connect-leg delivery with an X-Phosra-Signature header so your receiver can prove the request came from Phosra and was not tampered with. Algorithm: X-Phosra-Signature = lowercase_hex( HMAC-SHA256( connect_secret, rawRequestBody ) ) (reference: internal/ocss/link/sender.go Signature / VerifySignature). Pass the connect_secret from Step 1 into createGatekeeper and the SDK verifies it for you, fail-closed:
const gk = createGatekeeper({
  // ...bases + signing key...
  connectSecret: process.env.PHOSRA_CONNECT_SECRET,  // from the endpoint-mint 201
})
ConditionhandleConnect result
connectSecret set, signature matchesAccept — persist label, attempt refreshProfile
connectSecret set, signature absent or wrongHTTP 401 (fail-closed)
Same endpoint_id_label seen again (replay)HTTP 200 immediately, no second refreshProfile (idempotent)
No connectSecret configuredNo signature check — trusted-network only
Always configure connectSecret in production. Without it, handleConnect accepts any POST to your webhook. The connect_secret is the only thing that binds an inbound delivery to Phosra.

The @phosra/gatekeeper env contract — the six PHOSRA_* vars

A gatekeeper deployment is fully parametrized by six PHOSRA_* environment variables plus one secret signing seed. Everything createGatekeeper needs maps 1:1 onto them:
Env varcreateGatekeeper fieldValue / source
PHOSRA_PLATFORM_DIDplatformDiddid:ocss:<your-slug> — your Trust-List DID
PHOSRA_PLATFORM_KEY_IDplatformKeyIdYour signing key id, e.g. did:ocss:snaptr#snaptr-2026-06 (the keyID your DID publishes)
PHOSRA_CENSUS_URLcensusBaseUrlhttps://phosra-api-sandbox-production.up.railway.app (canonical sandbox host)
PHOSRA_TRUST_ROOT_XtrustRootXB64UrlCMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU — root pubkey X (base64url, public, pinned out-of-band)
PHOSRA_ENDPOINT_IDendpointIdThe endpoint_id_label from your endpoint-mint 201 (populated after the connect ceremony binds you)
PHOSRA_CONNECT_SECRETconnectSecretThe connect_secret from your endpoint-mint 201
Plus the signing seed — the one genuinely sensitive value, kept out of the six because it is a private key, not configuration:
SecretBuildsValue / source
PHOSRA_SIGNING_SEEDgatekeeperSigningKey.seedbase64url of your Ed25519 32-byte seed. Load from a secret manager; never commit it.
# .env — the six PHOSRA_* config vars (safe to template; the SEED is a secret)
PHOSRA_PLATFORM_DID=did:ocss:snaptr
PHOSRA_PLATFORM_KEY_ID=did:ocss:snaptr#snaptr-2026-06
PHOSRA_CENSUS_URL=https://phosra-api-sandbox-production.up.railway.app
PHOSRA_TRUST_ROOT_X=CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU
PHOSRA_ENDPOINT_ID=eplbl_9c2f…            # endpoint_id_label from the mint 201
PHOSRA_CONNECT_SECRET=cs_7b41…            # connect_secret from the mint 201
# secret — from a vault, not this file:
# PHOSRA_SIGNING_SEED=<base64url 32-byte Ed25519 seed>
import { createGatekeeper } from "@phosra/gatekeeper"
import { b64urlRawDecode } from "@openchildsafety/ocss"

const gk = createGatekeeper({
  platformDid:          process.env.PHOSRA_PLATFORM_DID!,
  platformKeyId:        process.env.PHOSRA_PLATFORM_KEY_ID!,
  censusBaseUrl:        process.env.PHOSRA_CENSUS_URL!,
  trustRootXB64Url:     process.env.PHOSRA_TRUST_ROOT_X!,
  endpointId:           process.env.PHOSRA_ENDPOINT_ID!,
  connectSecret:        process.env.PHOSRA_CONNECT_SECRET!,
  gatekeeperSigningKey: {
    seed:  b64urlRawDecode(process.env.PHOSRA_SIGNING_SEED!),   // Uint8Array(32)
    keyID: process.env.PHOSRA_PLATFORM_KEY_ID!,
  },
  ratingMappings: [{ ocssCategory: "content_rating", myField: "mpaaRating", vocabulary: "mpaa" }],
})
PHOSRA_TRUST_ROOT_X is a public key pinned out-of-band on purpose — that is what root verification means: you do not fetch the root from the census you are about to verify. The staging census is a Phosra-internal pre-release instance with a different root and is not a partner surface — always use the canonical production sandbox host above.

Conformance

The signed endpoint-mint (Step 1) needs your DID key, so it is exercised by the SDK, not raw curl. What a docs-only stranger — or the nightly docs-conformance CI — can run with no credentials is the reachability + trust-anchor check the whole registration depends on: the canonical census serves a root-signed Trust List whose root matches the PHOSRA_TRUST_ROOT_X pinned above.
# 1. The canonical sandbox census is reachable and healthy.
curl -fsS https://phosra-api-sandbox-production.up.railway.app/health
# → 200 {"status":"ok"}

# 2. It serves a root-signed Trust List whose root key_id is the sandbox root.
curl -fsS https://phosra-api-sandbox-production.up.railway.app/.well-known/ocss/trust-list \
  | grep -o '"key_id":"root-sandbox-2026-06"'
# → "key_id":"root-sandbox-2026-06"   (non-empty ⇒ PASS)
A signed endpoint-mint round-trip (mint → receive endpoint_id_label at your webhook → handleConnect 200 → refreshProfileisAllowed) is exercised end-to-end by scripts/sandbox/verify-web-engagement-rule.mjs, which exits 0 only if the full chain survives against the live sandbox census.

Next

  • Platform Quickstart — receive the connection, fetch/verify the profile, enforce locally, confirm
  • Onboarding — get your sandbox did:ocss:<slug> (self-serve, no email)
  • Conformance status — the one canonical sandbox host and every live/preview surface