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

# Fetch the router-signed enforcement profile

> §9.3(b) bound-resolver authentication: the caller signs the request with its Trust-List-published key (RFC 9421); the {endpoint_id} path segment is the high-entropy bound-resolver label from the mint response. A wrong or unknown label returns 404 (indistinguishable from a wrong-resolver dereference — fail-closed, no existence leak, §9.3(a)). The response is a SignedDocument whose `document` string parses to an EnforcementProfile; verify the signature to the router key then to the root before trusting the profile.
Serving discipline (§8.3.6 cl.3): signatures are minted at compile time and cached per binding. Supports ETag / If-None-Match for 304 Not Modified.


The core read of the data-plane. The `{endpoint_id}` path segment is the high-entropy
bound-resolver label from [the bind response](/api-reference/data-plane/bind-endpoint), and
presenting it under your [RFC 9421 signature](/concepts/signing-requests)
**is** the §9.3(b) authentication. The response is a `SignedDocument` whose `document`
**string** parses to an `EnforcementProfile`.

<Warning>
  `document` is a JSON **string** on the wire, not an object. The signature covers exactly those
  UTF-8 bytes — verify the signature over the raw string, then `JSON.parse` it. Never
  re-stringify an object before verifying. The canonical profile field is **`categories[]`**;
  `profile.rules[]` does not exist.
</Warning>

A wrong or unknown label returns `404`, indistinguishable from a wrong-resolver dereference —
fail-closed, no existence leak (§9.3(a)). The endpoint supports `ETag` / `If-None-Match` for
`304 Not Modified`.

## Worked example

The [`@phosra/gatekeeper`](/sdks/mcp-server) SDK is the supported path: it polls, verifies to
the root, and exposes the decision. The raw signed GET is shown alongside it.

<CodeGroup>
  ```ts Node (raw signed GET) theme={null}
  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"

  const label = "iGrFqzp43O0S9YTNN2oAT6zMzcugEX_EwZbraWrE1AA" // your endpoint_id_label
  const targetURI = `${BASE}/enforcement-profiles/${label}`
  const headers = signRequest({ method: "GET", targetURI, keyID, seed, created: Math.floor(Date.now() / 1000) })

  const res = await fetch(targetURI, { method: "GET", headers })
  const signed = await res.json()               // { document: "<json string>", key_id, alg, sig }
  const profile = JSON.parse(signed.document)   // parse AFTER verifying the signature
  console.log(res.status, profile.categories)
  ```

  ```ts Node (@phosra/gatekeeper — verifies to root for you) theme={null}
  import { createGatekeeper } from "@phosra/gatekeeper"

  const gk = createGatekeeper({
    platformDid:          "did:ocss:loopline",
    platformKeyId:        "did:ocss:loopline#2026-06",
    gatekeeperSigningKey: { seed: /* 32-byte Ed25519 seed */ new Uint8Array(32), keyID: "did:ocss:loopline#2026-06" },
    censusBaseUrl:        "https://phosra-api-sandbox-production.up.railway.app",
    trustRootXB64Url:     process.env.PHOSRA_TRUST_ROOT_X!,   // pinned root pubkey X
    endpointId:           "iGrFqzp43O0S9YTNN2oAT6zMzcugEX_EwZbraWrE1AA",
  })

  const verdict = gk.check("infinite_scroll_block")
  if (verdict.decision === "block") { /* block the content */ }
  ```

  ```python Python (raw signed GET) theme={null}
  # sign_request(...) — the signer from /concepts/signing-requests. A GET has no body,
  # so no Content-Digest is covered (components @method, @target-uri, ocss-spec-version only).
  import json, requests

  BASE  = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
  SEED  = "bG9vcGxpbmUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE"
  KEYID = "did:ocss:loopline#2026-06"

  label = "iGrFqzp43O0S9YTNN2oAT6zMzcugEX_EwZbraWrE1AA"   # your endpoint_id_label
  url   = f"{BASE}/enforcement-profiles/{label}"
  res   = requests.get(url, headers=sign_request("GET", url, KEYID, SEED))  # no body arg
  signed  = res.json()                       # { document: "<json string>", key_id, alg, sig }
  profile = json.loads(signed["document"])   # parse AFTER verifying the signature
  print(res.status_code, profile["categories"])
  ```

  ```go Go (raw signed GET) theme={null}
  // SignRequest(...) — the stdlib signer from /concepts/signing-requests.
  package main

  import (
  	"encoding/json"; "fmt"; "io"; "net/http"
  )

  func main() {
  	const (
  		base  = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
  		seed  = "bG9vcGxpbmUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE"
  		keyID = "did:ocss:loopline#2026-06"
  		label = "iGrFqzp43O0S9YTNN2oAT6zMzcugEX_EwZbraWrE1AA" // your endpoint_id_label
  	)
  	url := base + "/enforcement-profiles/" + label
  	h, _ := SignRequest("GET", url, keyID, seed, nil) // nil body -> no Content-Digest
  	req, _ := http.NewRequest("GET", url, nil)
  	for k, v := range h {
  		req.Header.Set(k, v)
  	}
  	resp, _ := http.DefaultClient.Do(req)
  	defer resp.Body.Close()
  	b, _ := io.ReadAll(resp.Body)
  	var signed struct{ Document string `json:"document"` }
  	json.Unmarshal(b, &signed) // verify signed.sig before trusting; then parse signed.Document
  	fmt.Println(resp.StatusCode, signed.Document)
  }
  ```

  ```bash curl + openssl theme={null}
  # Runnable signed openssl GET: /concepts/signing-requests (drop the body + Content-Digest,
  # cover only the three components). Wire shape:
  curl https://phosra-api-sandbox-production.up.railway.app/api/v1/enforcement-profiles/iGrFqzp43O0S9YTNN2oAT6zMzcugEX_EwZbraWrE1AA \
    -H "OCSS-Spec-Version: OCSS-v1.0-pre" \
    -H 'Signature-Input: ocss=("@method" "@target-uri" "ocss-spec-version");created=1783315514;keyid="did:ocss:loopline#2026-06";alg="ed25519"' \
    -H 'Signature: ocss=:<std-base64 ed25519-sig>:'
  ```
</CodeGroup>

<Note>
  The signer is defined once in the [**request-signing guide**](/concepts/signing-requests). A
  `GET` covers only `@method`, `@target-uri`, and `ocss-spec-version` — **no** `content-digest`.
  Always verify `signed.sig` to the router key and root before you trust `categories[]`.
</Note>

**Real `200` response** (captured from the hosted sandbox — router-signed, `categories[]`
empty here because the sandbox self-serve child carries no compiled rules yet):

```json theme={null}
{
  "document": "{\"categories\":[],\"document_type\":\"enforcement_profile\",\"ocss_version\":\"OCSS-v1.0-pre\",\"profile_ref\":\"sha256:0979812325e659675635dee167c202b92744891f315f99f1761a628e4ac3a87e\",\"rotation_epoch\":495365,\"token_binding\":\"162d45da4cd20908d138c08ae100439199953ed7810c88a149d162283268c605\",\"window\":{\"not_after\":\"2026-07-06T06:00:00Z\",\"not_before\":\"2026-07-06T05:00:00Z\"}}",
  "key_id": "did:ocss:phosra-router#router-sandbox-2026-06",
  "alg": "ed25519",
  "sig": "_GkUlEiLYL0T…"
}
```

Once the child has active rules (see the [rule-write flow](/ocss/intent-api)), each
`categories[]` row carries a `category`, a `decision` (`allow` / `warn` / `block`), a
`fail_mode`, and a per-child `rule_ref` you echo when you
[confirm enforcement](/api-reference/data-plane/submit-confirmation).


## OpenAPI

````yaml GET /enforcement-profiles/{endpoint_id}
openapi: 3.1.0
info:
  title: Phosra OCSS Data Plane — enforcement endpoints, profiles & confirmations
  description: >
    The signed OCSS data-plane the @phosra/gatekeeper SDK drives. These are NOT
    plain REST: profile GETs are RFC 9421-signed by the resolver, binding POSTs
    require accredited caller signatures, and profile responses are
    root-verifiable signed documents consumed via @openchildsafety/ocss /
    @phosra/gatekeeper. This spec documents the wire shapes for client
    generation; it does not change census behavior. The control plane (provider
    org, keys, advisors) is in openapi.control-plane.yaml; the parental-control
    product API is in openapi.yaml.

    Canonical profile shape: `categories[]` (there is no `rules[]` field — it
    does not exist on the wire, in the Go model, or in @phosra/gatekeeper
    types).

    Wire shapes are authoritative from the Go census:
      - profile.Document / CategoryEntry: internal/ocss/profile/document.go
      - EndpointHandlers.Mint / Rotate: internal/ocsshttp/handler_endpoints.go
      - ProfileReads.ServeHTTP: internal/ocsshttp/handler_profiles.go
      - EnforcementHandlers.Confirm: internal/ocsshttp/handler_enforcement.go
      - receipt.Receipt / EnforcementResultBody: internal/ocss/receipt/types.go
  version: 1.0.0
  license:
    name: Proprietary
    url: https://phosra.com/terms
  contact:
    name: Phosra
servers:
  - url: https://phosra-api-sandbox-production.up.railway.app/api/v1
    description: >-
      Hosted sandbox — the one canonical, stable, partner-facing testing
      endpoint
  - url: http://localhost:8080/api/v1
    description: Local development
security: []
paths:
  /enforcement-profiles/{endpoint_id}:
    get:
      summary: Fetch the router-signed enforcement profile for a bound endpoint
      description: >
        §9.3(b) bound-resolver authentication: the caller signs the request with
        its Trust-List-published key (RFC 9421); the {endpoint_id} path segment
        is the high-entropy bound-resolver label from the mint response. A wrong
        or unknown label returns 404 (indistinguishable from a wrong-resolver
        dereference — fail-closed, no existence leak, §9.3(a)). The response is
        a SignedDocument whose `document` string parses to an
        EnforcementProfile; verify the signature to the router key then to the
        root before trusting the profile.

        Serving discipline (§8.3.6 cl.3): signatures are minted at compile time
        and cached per binding. Supports ETag / If-None-Match for 304 Not
        Modified.
      operationId: fetchTheRouterSignedEnforcementProfileForABoundEndpoint
      parameters:
        - name: endpoint_id
          in: path
          required: true
          description: >
            The high-entropy §9.3(b) bound-resolver label returned by the mint
            endpoint. Treat as a secret credential — never log.
          schema:
            type: string
      responses:
        '200':
          description: >
            Router-signed enforcement profile. Verify to root before trusting.
            categories[] is the canonical profile field.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SignedDocument'
              example:
                document: >-
                  {"categories":[],"document_type":"enforcement_profile","ocss_version":"OCSS-v1.0-pre","profile_ref":"sha256:898314e28cbd0e7bede49d8c48b6e4309ef019ff3189d03cae4215fe7b8d2b39","rotation_epoch":495369,"token_binding":"9f4187aa0931973599d0178b0b6a219604206f4a48dcc6166edfb66468a21220","window":{"not_after":"2026-07-06T10:00:00Z","not_before":"2026-07-06T09:00:00Z"}}
                key_id: did:ocss:phosra-router#router-sandbox-2026-06
                alg: ed25519
                sig: >-
                  stpRC97FNDiMd5JlkwTn_VxGJ3OngJ9opWiABfEZu4cof6ocL2aqIrjTDGH1vVUFmdI2m2TAhnMsT_Snz416Aw
        '304':
          description: Not Modified (ETag / If-None-Match cache hit). Empty body.
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: >
            Unknown label OR a wrong-resolver dereference — the two are answered
            IDENTICALLY (fail-closed, no existence leak per §9.3(a)). `class` is
            `not_found`. (An unaccredited/unsigned caller is rejected earlier by
            the RFC 9421 layer — see 401.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Not Found
                message: unknown enforcement endpoint
                code: 404
                class: not_found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          description: >
            The router signing identity is not provisioned on this deployment,
            so no signed profile can be served (honest vacancy — never a
            fabricated signature). Gatekeeper fail-closes.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Service Unavailable
                message: enforcement-profile signing identity not provisioned
                code: 503
      security:
        - Rfc9421Signature: []
      x-codeSamples:
        - lang: Shell
          label: Signed curl (wire shape)
          source: >
            # Signed GET — headers are per-request (compute with the SDK, Node
            tab). No body.

            curl
            https://phosra-api-sandbox-production.up.railway.app/api/v1/enforcement-profiles/iGrFqzp43O0S9YTNN2oAT6zMzcugEX_EwZbraWrE1AA
            \
              -H "OCSS-Spec-Version: OCSS-v1.0-pre" \
              -H 'Signature-Input: ocss=("@method" "@target-uri" "ocss-spec-version");created=1783315514;keyid="did:ocss:loopline#2026-06";alg="ed25519"' \
              -H 'Signature: ocss=:<base64-ed25519-sig>:'
        - lang: JavaScript
          label: Node (@openchildsafety/ocss)
          source: >
            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"

            const label = "iGrFqzp43O0S9YTNN2oAT6zMzcugEX_EwZbraWrE1AA"

            const t = `${BASE}/enforcement-profiles/${label}`

            const h = signRequest({ method: "GET", targetURI: t, keyID, seed,
            created: Math.floor(Date.now() / 1000) })

            const res = await fetch(t, { method: "GET", headers: h })

            const signed = await res.json()

            const profile = JSON.parse(signed.document) // parse AFTER verifying
            signed.sig

            console.log(res.status, profile.categories)
components:
  schemas:
    SignedDocument:
      type: object
      description: >
        Router-signed wrapper. `document` is a JSON STRING on the wire (not an
        object) whose UTF-8 contents are the canonical JCS bytes the router key
        signed. Verify with the resolved signing key over exactly those bytes —
        never by re-stringifying an object. Matches the Signed.MarshalJSON
        contract in internal/ocss/profile/document.go.
      required:
        - document
        - key_id
        - alg
        - sig
      properties:
        document:
          type: string
          description: >
            Canonical JSON string of the inner EnforcementProfile document. The
            signature covers exactly these UTF-8 bytes. Parse as JSON to obtain
            the EnforcementProfile; do not re-marshal before verifying.
        key_id:
          type: string
          description: D-15 key id in "did:ocss:<slug>#<kid>" form.
          example: did:ocss:phosra-router#router-sandbox-2026-06
        alg:
          type: string
          enum:
            - ed25519
          description: 'One of: ed25519.'
        sig:
          type: string
          description: >
            base64url-raw (-_ alphabet, no padding) Ed25519 detached signature
            over the UTF-8 bytes of `document`. Matches base64.RawURLEncoding in
            internal/ocss/profile/document.go Sign().
      additionalProperties: true
    Error:
      type: object
      description: Standard error envelope returned for every 4xx and 5xx response.
      properties:
        error:
          type: string
          description: >-
            Short, stable, machine-readable error class — matches the HTTP
            status text (e.g. `Not Found`).
        message:
          type: string
          description: >-
            Human-readable explanation of what went wrong and, where possible,
            how to fix it.
        code:
          type: integer
          description: Numeric HTTP status code, duplicated in the body for convenience.
        class:
          type: string
          enum:
            - signature_invalid
            - nonconformant
            - replay
            - standing_failure
            - scope_failure
            - malformed
            - not_found
          description: >
            Stable machine-readable error subclass for programmatic branching.
            Present on ALL seven published census (data-plane) error classes —
            400⇒`malformed`, 401⇒`signature_invalid`,
            403⇒`standing_failure`|`scope_failure`, 404⇒`not_found`,
            409⇒`replay`, 422⇒`nonconformant`. ABSENT on the plain house 500, on
            control-plane/management errors, and on the compact sandbox-lane
            `{error:"<code>"}` bodies (OAuth + sandbox consent).
        failed_step:
          type: string
          description: >
            Present only on a §8.3.1 standing failure (`class:standing_failure`)
            — names the §6.2 step that failed, e.g. `authority_binding`.
      example:
        error: Not Found
        message: unknown enforcement endpoint
        code: 404
        class: not_found
      additionalProperties: true
  responses:
    BadRequest:
      description: >
        The request was malformed — a required field is missing, a value failed
        validation, or an identifier is not a well-formed UUID. Census
        (data-plane) 400s carry the `malformed` error class.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Bad Request
            message: id must be a uuid
            code: 400
            class: malformed
    Unauthorized:
      description: >
        RFC 9421 message-signature authentication failed. Every data-plane call
        must carry exactly one `Signature-Input` and one `Signature` header
        computed over the request with your Trust-List-published Ed25519 key.
        `class` is `signature_invalid`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Unauthorized
            message: exactly one Signature-Input and one Signature header are required
            code: 401
            class: signature_invalid
    RateLimited:
      description: >-
        Too many requests. Back off and retry after the interval given in the
        `Retry-After` response header.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Too Many Requests
            message: rate limit exceeded
            code: 429
    ServerError:
      description: >-
        An unexpected error occurred inside Phosra. The request is safe to retry
        with exponential backoff; contact support if it persists.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Internal Server Error
            message: internal error
            code: 500
    BadGateway:
      description: >-
        A downstream provider returned an invalid response while Phosra was
        fulfilling the request. Retry with backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Bad Gateway
            message: downstream provider error
            code: 502
  securitySchemes:
    Rfc9421Signature:
      type: http
      scheme: signature
      description: >
        RFC 9421 message signature (Ed25519). The sender signs the request with
        its Trust-List-published key; the census verifies provenance and
        accreditation tier before processing. Used for POST
        enforcement-endpoints (binding mint), POST
        enforcement-endpoints/{id}/rotate, and POST enforcement-confirmations.
        Profile GETs (GET /enforcement-profiles/{endpoint_id}) also require the
        resolver's RFC 9421 signature — the §9.3(b) bound-resolver auth.

````