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

# Run the connect ceremony to your own DID (sandbox)

> SANDBOX-ONLY reference provider. Mints a §9.3 binding for a seeded test child scoped to your platform_did and delivers the signed §3.6 callback to your webhook, so a cold self-registered platform dev can watch a real signed enforcement profile land with no live counterparty and no OCSS_CONSENT_ATTESTATION_APPS roster edit (it skips §6.2 standing for the seeded fixture child). RFC 9421 signed as the caller. Gated on the Restricted band (PHOSRA_ENV==sandbox) — returns 404 on dev/staging/production.


<Note>
  **Sandbox only.** Gated on `PHOSRA_ENV==sandbox` — returns `404` on dev, staging, and
  production.
</Note>

Runs the **provider side** of the EXT-04 §3.2 connect ceremony against your own platform DID +
webhook, so a cold self-registered platform dev receives a **real signed binding** with no live
counterparty and no roster edit. It mints a §9.3 binding for a seeded test child scoped to your
`platform_did` and delivers the signed §3.6 callback to your `webhook_url`. The call is
[RFC 9421-signed](/api-reference/data-plane/overview#how-a-signed-request-is-built) as you.

The binding is minted and `profile_url` is observable **even if the webhook delivery leg
fails** — `delivered=false` with a `note` explaining why, but you can still poll the profile.

## 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/test-connect"
  const body = {
    platform_did:   "did:ocss:loopline",
    webhook_url:    "https://your-gatekeeper.example.com",   // POST /api/ocss/connect receiver base
    connect_secret: "cs_your_hmac_secret",                    // the secret you feed gk.config({ connectSecret })
  }
  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 })
  console.log(res.status, await res.json())
  ```

  ```bash Shell (wire shape) theme={null}
  curl -X POST https://phosra-api-sandbox-production.up.railway.app/api/v1/sandbox/test-connect \
    -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 '{"platform_did":"did:ocss:loopline","webhook_url":"https://your-gatekeeper.example.com","connect_secret":"cs_your_hmac_secret"}'
  ```
</CodeGroup>

**Real `200` response** (captured — `example.com` returns `405` to the callback, so
`delivered=false`; the binding is minted regardless):

```json theme={null}
{
  "binding_id": "145c2779-1759-4378-abf3-c49a49408ede",
  "resolver_did": "did:ocss:loopline",
  "endpoint_id_label": "ngb14RDrOQ57c9xIMPpKdQtqdLDdent9jiBo375He90",
  "profile_url": "/api/v1/enforcement-profiles/ngb14RDrOQ57c9xIMPpKdQtqdLDdent9jiBo375He90",
  "connect_receiver": "https://example.com/api/ocss/connect",
  "delivered": false,
  "http_status": 405,
  "state": "sandbox-test-connect:145c2779-1759-4378-abf3-c49a49408ede",
  "note": "callback delivery did not receive a 2xx — check webhook_url reachability and that connect_secret matches your gk.config; the binding is minted and profile_url is observable regardless"
}
```

Poll `profile_url` with your platform key ([fetch profile](/api-reference/data-plane/fetch-profile))
to see the router-signed profile land, and [rotate](/api-reference/data-plane/rotate-endpoint)
with the returned `binding_id`.


## OpenAPI

````yaml POST /sandbox/test-connect
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/test-connect:
    post:
      summary: >-
        Run the provider side of the connect ceremony to your own DID (sandbox
        only)
      description: >
        SANDBOX-ONLY reference provider. Mints a §9.3 binding for a seeded test
        child scoped to your platform_did and delivers the signed §3.6 callback
        to your webhook, so a cold self-registered platform dev can watch a real
        signed enforcement profile land with no live counterparty and no
        OCSS_CONSENT_ATTESTATION_APPS roster edit (it skips §6.2 standing for
        the seeded fixture child). RFC 9421 signed as the caller. Gated on the
        Restricted band (PHOSRA_ENV==sandbox) — returns 404 on
        dev/staging/production.
      operationId: runTheProviderSideOfTheConnectCeremonyToYourOwnDidSandboxOnly
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SandboxTestConnectRequest'
      responses:
        '200':
          description: >
            Ceremony ran. The binding is minted and profile_url is observable
            even if the webhook leg failed (delivered=false + note).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxTestConnectResponse'
              example:
                binding_id: 22b577ea-4a53-4505-9eb5-541bf237cb42
                resolver_did: did:ocss:loopline
                endpoint_id_label: iWdlltz5F4sKbZKYgIvGkIJg7sylz2LN37cZUK8wiWY
                profile_url: >-
                  /api/v1/enforcement-profiles/iWdlltz5F4sKbZKYgIvGkIJg7sylz2LN37cZUK8wiWY
                connect_receiver: https://loopline.example.com/api/ocss/connect
                delivered: false
                http_status: 0
                state: sandbox-test-connect:22b577ea-4a53-4505-9eb5-541bf237cb42
                note: >-
                  callback delivery did not receive a 2xx — check webhook_url
                  reachability and that connect_secret matches your gk.config;
                  the binding is minted and profile_url is observable regardless
        '400':
          description: >
            A required field is missing (`platform_did`, `connect_secret`) or
            the webhook_url is malformed. `class` is `malformed`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Bad Request
                message: platform_did is required
                code: 400
                class: malformed
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: >
            The Restricted-band sandbox surface is not active on this deployment
            (dev / staging-internal / production), so the route is not mounted —
            the router answers with a plain-text chi 404, NOT the JSON error
            envelope. Live only on the partner sandbox census.
          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 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: >
            # 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/test-connect
            \
              -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 '{"platform_did":"did:ocss:loopline","webhook_url":"https://your-gatekeeper.example.com","connect_secret":"cs_your_hmac_secret"}'
        - 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 = { platform_did: "did:ocss:loopline", webhook_url:
            "https://your-gatekeeper.example.com", connect_secret:
            "cs_your_hmac_secret" }

            const t = BASE + "/sandbox/test-connect", 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:
    SandboxTestConnectRequest:
      type: object
      description: >
        SANDBOX-ONLY (POST /api/v1/sandbox/test-connect). Runs the PROVIDER side
        of the EXT-04 §3.2 connect ceremony against the caller's own platform
        DID + webhook so a cold self-registered platform dev receives a real
        signed binding with no live counterparty. RFC 9421 signed as the caller.
        Source: sandboxTestConnectBody in
        internal/ocsshttp/handler_sandbox_test_connect.go.
      required:
        - platform_did
        - webhook_url
        - connect_secret
      properties:
        platform_did:
          type: string
          description: >-
            The consuming platform's Trust-List DID (the DID you self-registered
            and sign with).
          example: did:ocss:my-gatekeeper
        webhook_url:
          type: string
          description: >
            Your POST /api/ocss/connect receiver BASE (the well-known path is
            appended). Absolute http/https, no query/fragment/credentials. Plain
            http admitted (sandbox).
          example: https://my-gatekeeper.example.com
        connect_secret:
          type: string
          description: >
            The HMAC secret you fed into gk.config({ connectSecret }); the §3.6
            callback is signed with it so your receiver verifies
            X-Phosra-Signature. Supplied because the census stores only its
            digest.
        child_ref:
          type: string
          description: >-
            Optional seeded test child ("child:<uuid>"); defaults to the seeded
            Mia child.
          example: child:a11ce0fa-0000-4000-8000-0000000000a1
        window_seconds:
          type: integer
          format: int64
          description: Optional §6.3 rotation window in seconds; defaults to 3600.
      additionalProperties: true
    SandboxTestConnectResponse:
      type: object
      description: >
        Result of the sandbox provider ceremony. The binding is minted and
        profile_url is observable even when the webhook delivery leg fails
        (delivered=false + note). Source: sandboxTestConnectResp in
        handler_sandbox_test_connect.go.
      required:
        - binding_id
        - resolver_did
        - endpoint_id_label
        - profile_url
        - connect_receiver
        - delivered
        - http_status
        - state
      properties:
        binding_id:
          type: string
          format: uuid
          description: >-
            The binding UUID returned once by the mint ceremony; pass it to the
            rotate endpoint.
        resolver_did:
          type: string
          description: >-
            Echo of platform_did — the DID the binding resolves to (sign the
            profile poll with it).
        endpoint_id_label:
          type: string
          description: >
            The connect-ceremony binding label (unprefixed 43-char base64url).
            Poll it at profile_url with your platform key. Never log.
        profile_url:
          type: string
          example: /api/v1/enforcement-profiles/…
          description: Path to poll the router-signed enforcement profile for this binding.
        connect_receiver:
          type: string
          description: >-
            The full receiver URL the signed §3.6 callback was POSTed to
            (webhook_url + /api/ocss/connect).
        delivered:
          type: boolean
          description: >
            True when your receiver returned 2xx. False (with note) when the
            webhook leg failed — the binding is minted regardless.
        http_status:
          type: integer
          description: The HTTP status your receiver returned (0 if unreachable).
        state:
          type: string
          description: >-
            Opaque sandbox principal ref delivered alongside the label
            ("sandbox-test-connect:<binding_id>").
        note:
          type: string
          description: >-
            Present only when delivered=false — explains the webhook-leg
            failure.
      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
  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.

````