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

# Bind a child enforcement endpoint

> Mints a §9.3(b) bound-resolver label and returns the label, binding UUID, and connect_secret (all three returned exactly once). Requires an active consent_attestation for (audience_did, child_ref) as a §4.2.2 precondition. Caller must be an accredited resolver with a valid RFC 9421 signature.


Mints a §9.3(b) **bound-resolver label** for one child and returns the label, a
`binding_id`, and a `connect_secret` — **all three returned exactly once**. The caller must
be an accredited resolver with a valid [RFC 9421 signature](/concepts/signing-requests),
and there must be an active `consent_attestation` for `(audience_did, child_ref)` — the
§4.2.2 precondition. No consent → `403`.

<Warning>
  `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.
</Warning>

## Worked example

In the sandbox you satisfy the consent precondition with a single self-serve call
([mint test consent](/api-reference/data-plane/sandbox-consent-attestation)), then bind. This
runs end-to-end against the hosted sandbox:

<CodeGroup>
  ```ts Node 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"

  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())
  ```

  ```python Python theme={null}
  # sign_request(...) is the ~25-line signer from /concepts/signing-requests — paste it in.
  import json, requests

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

  def signed_post(path, body_obj):
      url  = BASE + path
      body = json.dumps(body_obj).encode()
      h    = sign_request("POST", url, KEYID, SEED, body)
      h["Content-Type"] = "application/json"
      return requests.post(url, data=body, headers=h)

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

  # 2. Bind the endpoint for that consented child.
  res = signed_post("/enforcement-endpoints",
                    {"audience_did": "did:ocss:loopline",
                     "child_ref": consent["target_ref"], "window_seconds": 3600})
  print(res.status_code, res.json())   # -> 201 { binding_id, connect_secret, endpoint_id_label }
  ```

  ```go Go theme={null}
  // SignRequest(...) is the stdlib-only signer from /concepts/signing-requests.
  package main

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

  const (
  	base  = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
  	seed  = "bG9vcGxpbmUBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE" // sandbox loopline test seed
  	keyID = "did:ocss:loopline#2026-06"
  )

  func signedPost(path string, bodyObj any) map[string]any {
  	body, _ := json.Marshal(bodyObj)
  	url := base + path
  	h, _ := SignRequest("POST", url, keyID, seed, body)
  	req, _ := http.NewRequest("POST", url, bytes.NewReader(body))
  	for k, v := range h {
  		req.Header.Set(k, v)
  	}
  	req.Header.Set("Content-Type", "application/json")
  	resp, _ := http.DefaultClient.Do(req)
  	defer resp.Body.Close()
  	b, _ := io.ReadAll(resp.Body)
  	fmt.Println(resp.StatusCode, string(b))
  	var out map[string]any
  	json.Unmarshal(b, &out)
  	return out
  }

  func main() {
  	// 1. Consent-first gate (sandbox self-serve). 2. Bind for that child.
  	consent := signedPost("/sandbox/consent-attestations",
  		map[string]string{"band": "13_15", "consent_scope": "collection_parental_authority"})
  	signedPost("/enforcement-endpoints", map[string]any{
  		"audience_did": "did:ocss:loopline", "child_ref": consent["target_ref"], "window_seconds": 3600,
  	}) // -> 201 { binding_id, connect_secret, endpoint_id_label }
  }
  ```

  ```bash curl + openssl theme={null}
  # The runnable, fully-signed openssl script lives in the signing guide:
  #   /concepts/signing-requests  (POST /enforcement-endpoints example, verified 201).
  # The wire shape of the four headers for this call:
  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 'Content-Digest: sha-256=:<std-base64 sha256(body)>:' \
    -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=:<std-base64 ed25519-sig>:' \
    -d '{"audience_did":"did:ocss:loopline","child_ref":"child:5ba0d00c-0000-4000-8000-0000000000c1","window_seconds":3600}'
  ```
</CodeGroup>

<Note>
  The signer (`sign_request` / `SignRequest`) is defined once in the
  [**request-signing guide**](/concepts/signing-requests) — 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`.
</Note>

**Real `201` response** (captured from the hosted sandbox):

```json theme={null}
{
  "binding_id": "c8ff82dd-4aa8-4a52-a235-ae13717eb4c8",
  "connect_secret": "pRrwmJa5gDmlCoK7pga9TGXAQ-kdZG6XBJkmkBjJ8lE",
  "endpoint_id_label": "iGrFqzp43O0S9YTNN2oAT6zMzcugEX_EwZbraWrE1AA"
}
```

Next: [poll the signed profile](/api-reference/data-plane/fetch-profile) with the
`endpoint_id_label`, and [rotate](/api-reference/data-plane/rotate-endpoint) using the
`binding_id`.


## OpenAPI

````yaml POST /enforcement-endpoints
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-endpoints:
    post:
      summary: Bind a child enforcement endpoint
      description: >
        Mints a §9.3(b) bound-resolver label and returns the label, binding
        UUID, and connect_secret (all three returned exactly once). Requires an
        active consent_attestation for (audience_did, child_ref) as a §4.2.2
        precondition. Caller must be an accredited resolver with a valid RFC
        9421 signature.
      operationId: bindAChildEnforcementEndpoint
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EndpointMintRequest'
      responses:
        '201':
          description: >-
            Endpoint bound. Store endpoint_id_label and connect_secret
            immediately.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EndpointMintResponse'
              example:
                endpoint_id_label: U49ctRQSAz5TEpBSuHZxWhVaW28J7qfEvI0grdOxaRE
                binding_id: e376e187-cecb-42ed-9d19-aa05de1043cd
                connect_secret: PyBm20VF2xm78D-HOgi0Jg3ldKSguAXy_H5DRIpNhnQ
        '400':
          description: >
            Malformed request — a required field is missing or an identifier is
            not well-formed. `class` is always `malformed`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Bad Request
                message: audience_did, child_ref, and window_seconds (>=1) are required
                code: 400
                class: malformed
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >
            No active consent_attestation backs this (audience_did, child_ref)
            binding (§4.2.2 precondition). `class` is `standing_failure` and
            `failed_step` names the §6.2 step that failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Forbidden
                message: >-
                  no active consent_attestation backs this binding (§4.2.2
                  precondition)
                code: 403
                class: standing_failure
                failed_step: authority_binding
        '404':
          description: >
            The child_ref is a well-formed `child:<uuid>` but no such child
            record exists. `class` is `not_found`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Not Found
                message: unknown child
                code: 404
                class: not_found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          description: >
            The census operation is not yet available on this deployment
            (feature-gated surface). Retry later.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Service Unavailable
                message: census operation not yet available
                code: 503
      security:
        - Rfc9421Signature: []
      x-codeSamples:
        - lang: Shell
          label: Signed curl (wire shape)
          source: >
            # 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}'
        - 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 body = { audience_did: "did:ocss:loopline", child_ref:
            "child:5ba0d00c-0000-4000-8000-0000000000c1", window_seconds: 3600 }

            const t = BASE + "/enforcement-endpoints", b = JSON.stringify(body)

            const h = signRequest({ method: "POST", targetURI: t, body: new
            TextEncoder().encode(b), keyID, seed, created: Math.floor(Date.now()
            / 1000) })

            h["Content-Type"] = "application/json"

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

            console.log(res.status, await res.json())
components:
  schemas:
    EndpointMintRequest:
      type: object
      description: >
        Binding-leg mint request. Source: endpointMintBody in
        internal/ocsshttp/handler_endpoints.go. Required: audience_did,
        child_ref, window_seconds >= 1.
      required:
        - audience_did
        - child_ref
        - window_seconds
      properties:
        audience_did:
          type: string
          description: DID of the resolving platform (e.g. "did:ocss:snaptr").
          example: did:ocss:snaptr
        child_ref:
          type: string
          description: |
            "child:<uuid>" — the child the resolver is binding to.
          example: child:11111111-1111-4111-8111-111111111111
        window_seconds:
          type: integer
          format: int64
          minimum: 1
          description: >
            Profile validity window in seconds. The census mints a window of
            this duration and increments rotation_epoch accordingly.
        statutory_fail_closed:
          type: array
          items:
            type: string
          description: >
            Optional list of category slugs the deployment declares fail-closed
            (§9.1 floor 3 / §9.2). fail_mode="closed" appears on those
            CategoryEntry rows.
      additionalProperties: true
    EndpointMintResponse:
      type: object
      description: >
        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.
      required:
        - endpoint_id_label
        - binding_id
        - connect_secret
      properties:
        endpoint_id_label:
          type: string
          description: >
            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:
          type: string
          format: uuid
          description: UUID of the persisted binding row (for rotation).
        connect_secret:
          type: string
          description: >
            HMAC-SHA256 secret for verifying X-Phosra-Signature on inbound
            connect-leg deliveries (feed into gk.config({ connectSecret })).
            Returned ONCE; store it immediately.
      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:
    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.

````