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

# Mint a test consent attestation (sandbox)

> SANDBOX-ONLY self-serve consent. Mints a §8.3.2-shaped TEST consent row with app_ref = the RFC 9421 caller DID, so a cold self-registered platform can satisfy the consent-first gate on POST /api/v1/enforcement-endpoints with no roster edit and no live counterparty. With no child_ref, the census auto-provisions the sandbox self-serve test child and targets it; a supplied child_ref must already exist (the door never fabricates a caller-named child). Production §8.3.2 semantics unchanged. Gated on the Restricted band (PHOSRA_ENV==sandbox) — returns 404 on dev/staging/production. Source: internal/ocsshttp/handler_sandbox_consent.go.


<Note>
  **Sandbox only.** Gated on `PHOSRA_ENV==sandbox` — returns `404` on dev, staging, and
  production. Production §8.3.2 consent semantics are unchanged.
</Note>

Mints a §8.3.2-shaped **test consent row** with `app_ref` = your
[RFC 9421 caller DID](/api-reference/data-plane/overview#how-a-signed-request-is-built), so a
cold self-registered platform can satisfy the **consent-first gate** on
[`POST /enforcement-endpoints`](/api-reference/data-plane/bind-endpoint) with no
`OCSS_CONSENT_ATTESTATION_APPS` roster edit and no live counterparty.

With no `child_ref`, the census auto-provisions the sandbox self-serve test child and targets
it. A supplied `child_ref` must already exist — the door never fabricates a caller-named child.
The response gives you the `target_ref` to pass straight into the bind call.

## Worked example

<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"

  const targetURI = BASE + "/sandbox/consent-attestations"
  const body = { band: "13_15", consent_scope: "collection_parental_authority" }   // both optional; these are the defaults
  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"

  const res = await fetch(targetURI, { method: "POST", headers, body: bodyText })
  const consent = await res.json()
  console.log(res.status, consent.target_ref)   // pass target_ref to POST /enforcement-endpoints
  ```

  ```bash Shell (wire shape) theme={null}
  curl -X POST https://phosra-api-sandbox-production.up.railway.app/api/v1/sandbox/consent-attestations \
    -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 '{"band":"13_15","consent_scope":"collection_parental_authority"}'
  ```
</CodeGroup>

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

```json theme={null}
{
  "ok": true,
  "app_ref": "did:ocss:loopline",
  "target_ref": "child:5ba0d00c-0000-4000-8000-0000000000c1",
  "standing_ref": "consent:attestation:sbx-consent:did:ocss:loopline:child:5ba0d00c-0000-4000-8000-0000000000c1",
  "idempotency_key": "sbx-consent:did:ocss:loopline:child:5ba0d00c-0000-4000-8000-0000000000c1",
  "band": "13_15",
  "consent_scope": "collection_parental_authority",
  "expiry": "2027-07-06T05:26:40Z",
  "note": "sandbox-only test consent; pass audience_did=<this DID> and child_ref=<target_ref> to POST /api/v1/enforcement-endpoints to complete the consent-first mint"
}
```

The call is idempotent on `(app_ref, target_ref)` — re-minting returns the same
`standing_ref`. Next: [bind the endpoint](/api-reference/data-plane/bind-endpoint).


## OpenAPI

````yaml POST /sandbox/consent-attestations
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:
  /sandbox/consent-attestations:
    post:
      summary: Mint a test consent attestation for your own DID (sandbox only)
      description: >
        SANDBOX-ONLY self-serve consent. Mints a §8.3.2-shaped TEST consent row
        with app_ref = the RFC 9421 caller DID, so a cold self-registered
        platform can satisfy the consent-first gate on POST
        /api/v1/enforcement-endpoints with no roster edit and no live
        counterparty. With no child_ref, the census auto-provisions the sandbox
        self-serve test child and targets it; a supplied child_ref must already
        exist (the door never fabricates a caller-named child). Production
        §8.3.2 semantics unchanged. Gated on the Restricted band
        (PHOSRA_ENV==sandbox) — returns 404 on dev/staging/production. Source:
        internal/ocsshttp/handler_sandbox_consent.go.
      operationId: mintATestConsentAttestationForYourOwnDidSandboxOnly
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                child_ref:
                  type: string
                  description: >
                    Optional child:<uuid> target. Empty ⇒ the auto-provisioned
                    sandbox self-serve test child.
                  example: child:5ba0d00c-0000-4000-8000-0000000000c1
                band:
                  type: string
                  enum:
                    - under_13
                    - '13_15'
                    - '16_17'
                    - adult
                  default: '13_15'
                consent_scope:
                  type: string
                  enum:
                    - collection_parental_authority
                    - third_party_disclosure
                  default: collection_parental_authority
      responses:
        '201':
          description: >
            Test consent minted. Pass audience_did=<your DID> and
            child_ref=<target_ref> to POST /api/v1/enforcement-endpoints to
            complete the consent-first mint.
          content:
            application/json:
              schema:
                type: object
                required:
                  - ok
                  - app_ref
                  - target_ref
                  - standing_ref
                  - band
                  - consent_scope
                  - expiry
                properties:
                  ok:
                    type: boolean
                    example: true
                  app_ref:
                    type: string
                    example: did:ocss:playnest
                  target_ref:
                    type: string
                    example: child:5ba0d00c-0000-4000-8000-0000000000c1
                  standing_ref:
                    type: string
                    example: consent:attestation:sbx-consent-playnest-5ba0d00c
                  idempotency_key:
                    type: string
                  band:
                    type: string
                    example: '13_15'
                  consent_scope:
                    type: string
                    example: collection_parental_authority
                  expiry:
                    type: string
                    format: date-time
                  note:
                    type: string
              example:
                ok: true
                app_ref: did:ocss:dx24c6fcc23
                target_ref: child:c22fd864-a783-5354-bfbb-d7d066b17592
                standing_ref: >-
                  consent:attestation:sbx-consent:did:ocss:dx24c6fcc23:child:c22fd864-a783-5354-bfbb-d7d066b17592
                idempotency_key: >-
                  sbx-consent:did:ocss:dx24c6fcc23:child:c22fd864-a783-5354-bfbb-d7d066b17592
                band: '13_15'
                consent_scope: collection_parental_authority
                expiry: '2027-07-06T09:01:54Z'
                note: >-
                  sandbox-only test consent; pass audience_did=<this DID> and
                  child_ref=<target_ref> to POST /api/v1/enforcement-endpoints
                  to complete the consent-first mint
        '400':
          description: >
            The body failed to parse (`invalid_json`) or carried an
            out-of-vocabulary `band` / `consent_scope`. NOTE: the sandbox
            consent lane emits a COMPACT `{error: "<code>"}` body (no
            `message`/`code`/`class`), unlike the census error envelope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: invalid_band
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: >
            The Restricted-band sandbox surface is not active on this
            deployment, so the route is not mounted — the router answers with a
            plain-text chi 404, NOT the JSON error envelope. (A
            supplied-but-malformed child_ref is a 400 `{"error":"child_ref must
            be child:<uuid>"}`, not a 404.)
          content:
            text/plain:
              schema:
                type: string
              example: |
                404 page not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
        '503':
          description: >
            The census store is vacant / not yet provisioned. Compact `{error:
            "<code>"}` sandbox-lane body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: census_store_vacant
      security:
        - Rfc9421Signature: []
      x-codeSamples:
        - lang: Shell
          label: Signed curl (wire shape)
          source: >
            # Signed headers are per-request — compute with the SDK (Node tab).

            curl -X POST
            https://phosra-api-sandbox-production.up.railway.app/api/v1/sandbox/consent-attestations
            \
              -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 '{"band":"13_15","consent_scope":"collection_parental_authority"}'
        - 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 = { band: "13_15", consent_scope:
            "collection_parental_authority" }

            const t = BASE + "/sandbox/consent-attestations", 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 })

            const consent = await res.json()

            console.log(res.status, consent.target_ref) // pass to POST
            /enforcement-endpoints
components:
  schemas:
    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
  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.

````