openapi: 3.1.0
info:
  title: Phosra Developer API — provider account, keys & federation
  description: >
    The Phosra developer/provider control plane: manage your developer
    organization, API keys, and usage, and register as an advisor in the OCSS
    federation. Phosra is an accredited provider that operates OCSS-conformant
    infrastructure.


    The signed OCSS protocol verbs (rule writes, alert ingest, enforcement,
    Trust List) are NOT REST methods here — they are RFC 9421-signed and consumed
    via the `@openchildsafety/ocss` library. This spec covers only the plain-REST management
    surface. The parental-control product API is documented separately in
    `openapi.yaml`.
  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 canonical, stable, partner-facing testing endpoint (seeded demo family + phosra_test_ keys). Same base the CLI uses.
- url: https://prodapi.phosra.com/api/v1
  description: Production
- url: http://localhost:8080/api/v1
  description: Local development
security:
- SessionAuth: []
components:
  securitySchemes:
    DeveloperApiKeyAuth:
      type: http
      scheme: bearer
      description: >
        A developer API key (prefix `phosra_`) issued under a developer org.
        Send as `Authorization: Bearer phosra_…` or the `X-Api-Key` header.
        NOT used by these management operations — it authenticates the
        data-plane `/developer/*` routes. Management operations (orgs, keys,
        usage, advisors, MCP tokens) use SessionAuth, which is why a fresh
        signup can mint its first key with no pre-existing key.
    SessionAuth:
      type: http
      scheme: bearer
      description: >
        A logged-in user session bearer token (WorkOS AuthKit access token
        from signup/login).
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          description: 'HTTP status text (e.g. "Bad Request").'
        message:
          type: string
          description: 'Human-readable detail (the actual reason, e.g. "unknown_capability: foo").'
        code:
          type: integer
          description: HTTP status code.
        class:
          type: string
          description: >
            Stable error subclass for programmatic branching. This management/control
            plane returns the PLAIN `{error, message, code}` envelope and does NOT set
            `class` — the `class` field is emitted only by the signed data-plane
            (census) surface documented in `openapi.data-plane.yaml`.
      description: Standard error envelope returned for every 4xx and 5xx management response.
      example:
        error: Not Found
        message: developer org not found
        code: 404
      additionalProperties: true
    DeveloperOrg:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for this resource.
        name:
          type: string
          description: Human-readable display name.
        slug:
          type: string
          description: URL-safe stable identifier.
        description:
          type: string
          description: Human-readable description.
        website_url:
          type: string
          description: URL for website.
        logo_url:
          type: string
          description: URL for logo.
        owner_user_id:
          type: string
          format: uuid
          description: UUID identifier.
        tier:
          type: string
          description: >
            Accreditation / integration tier of the organization. New orgs start on `free`;
            higher tiers (e.g. `accredited`) are granted after the OCSS accreditation review.
          example: free
        rate_limit_rpm:
          type: integer
          description: Per-minute request-rate limit applied to this organization's keys.
        created_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp of when the resource was created.
        updated_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp of the resource's most recent update.
      additionalProperties: true
    DeveloperOrgMember:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for this resource.
        org_id:
          type: string
          format: uuid
          description: UUID identifier.
        user_id:
          type: string
          format: uuid
          description: Identifier of the user.
        role:
          type: string
          description: Member's role within the organization (e.g. `owner`, `admin`, `member`).
        created_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp of when the resource was created.
      additionalProperties: true
    DeveloperApiKey:
      type: object
      description: An API key. The secret is returned only once at create/regenerate (see DeveloperApiKeyWithSecret).
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for this resource.
        org_id:
          type: string
          format: uuid
          description: UUID identifier.
        name:
          type: string
          description: Human-readable display name.
        key_prefix:
          type: string
          description: Key prefix.
        environment:
          type: string
          enum: [test, live]
          description: 'One of: test, live.'
        scopes:
          type: array
          items:
            type: string
          description: Permission scopes granted to this key; empty means unscoped (full access within the account).
        last_used_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp.
        last_used_ip:
          type: string
          description: IP address of the most recent request made with this key; null if never used.
        expires_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp of when this value expires.
        revoked_at:
          type: [string, 'null']
          format: date-time
          description: RFC 3339 timestamp of when the key was revoked; null while the key is active.
        created_by:
          type: string
          format: uuid
          description: UUID identifier.
        created_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp of when the resource was created.
      additionalProperties: true
    DeveloperApiKeyWithSecret:
      allOf:
      - $ref: '#/components/schemas/DeveloperApiKey'
      - type: object
        properties:
          key:
            type: string
            description: The raw secret key — shown only once.
        additionalProperties: true
    DeveloperUsage:
      type: object
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for this resource.
        key_id:
          type: string
          format: uuid
          description: Identifier of the API key these requests were made with.
        org_id:
          type: string
          format: uuid
          description: Identifier of the developer organization that owns the key.
        hour:
          type: string
          format: date-time
          description: Start of the UTC hour this rollup covers (RFC 3339).
        endpoint:
          type: string
          description: Route pattern the requests hit (e.g. `/api/v1/children`).
        status_2xx:
          type: integer
          description: Count of 2xx (success) responses in this hour.
        status_4xx:
          type: integer
          description: Count of 4xx (client-error) responses in this hour.
        status_5xx:
          type: integer
          description: Count of 5xx (server-error) responses in this hour.
        total_requests:
          type: integer
          description: Total requests in this hour across all status classes.
      additionalProperties: true
    AdvisorRegistration:
      type: object
      required: [slug, display_name, public_key_pem, key_id, scope]
      properties:
        slug:
          type: string
          description: Unique identifier slug for the advisor agent.
        display_name:
          type: string
          description: Human-readable name for the advisor.
        publisher_org:
          type: string
          description: Name of the organization publishing this advisor.
        callback_url:
          type: string
          description: HTTPS callback endpoint for advisory verdicts.
        public_key_pem:
          type: string
          description: Ed25519 public key in PEM format, used to verify advisory signatures.
        key_id:
          type: string
          description: Identifier for the signing key (forms the did#kid).
        capability_domains:
          type: array
          items:
            type: string
          description: >
            Advisory capability domains this agent supports.
            An unrecognized capability name causes the request to be rejected
            with a 400 error (`unknown_capability: <name>`).
            A recognized capability that is above the provisional band is
            silently dropped (skipped) from the accepted set.
        scope:
          type: string
          enum: [family, global]
          description: "`family` = per-family advisory (scope_family_id required); `global` = coalition-wide advisory (manifest_url required)."
        scope_family_id:
          type: [string, "null"]
          format: uuid
          description: Family UUID to scope this advisor to. Required when scope=family.
        manifest_url:
          type: [string, "null"]
          description: URL of the advisor manifest. Required when scope=global.
      additionalProperties: true
    McpToken:
      type: object
      description: An MCP (Model Context Protocol) authentication token bound to a user and family.
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier for this resource.
        user_id:
          type: string
          format: uuid
          description: Identifier of the user.
        family_id:
          type: string
          format: uuid
          description: Identifier of the family this resource belongs to.
        label:
          type: string
          description: Human-readable label the user gave this MCP token.
        token_prefix:
          type: string
          description: Non-secret prefix of the token (safe to display in a list).
        last_used_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp.
        created_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp of when the resource was created.
        revoked_at:
          type: [string, 'null']
          format: date-time
          description: RFC 3339 timestamp of when the key was revoked; null while the key is active.
      additionalProperties: true
    CreatedMcpToken:
      type: object
      description: >
        Returned only at MCP token creation time. The `plain` field is the raw
        plaintext token — show it to the user once and never persist it.
      properties:
        token:
          $ref: '#/components/schemas/McpToken'
        plain:
          type: string
          description: Raw plaintext token (prefix `phomcp_`). Shown once; never stored server-side.
      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. Management-plane errors carry no `class`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          # Live wire body captured from the sandbox census (GET org with a non-uuid id).
          example:
            error: Bad Request
            message: invalid org ID
            code: 400
    Unauthorized:
      description: >
        No valid session bearer token. Send the WorkOS AuthKit access token as
        `Authorization: Bearer <token>`.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          # Real middleware wire body (internal/handler/middleware/auth.go:67).
          example:
            error: Unauthorized
            message: missing authorization header
            code: 401
    NotFound:
      description: No resource matches the identifier supplied in the path.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          # Real service error surfaced verbatim (service.ErrOrgNotFound).
          example:
            error: Not Found
            message: developer org not found
            code: 404
    Conflict:
      description: The request conflicts with the current state of the resource — for example, a slug with the same natural key already exists.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          # Real handler wire body (developer_handler.go:51).
          example:
            error: Conflict
            message: organization slug already exists, please try again
            code: 409
    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
    ServiceUnavailable:
      description: A downstream provider or dependency is temporarily unavailable. Retry with backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Service Unavailable
            message: downstream provider unavailable
            code: 503
paths:
  /developers/orgs:
    post:
      operationId: createOrg
      summary: Create a developer organization
      description: "Creates a developer organization and returns the created resource. The creator becomes the first member with the owner role."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X POST "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN" \
            -H "Content-Type: application/json" \
            -d '{
            "name": "Acme Safety Labs",
            "description": "Parental-controls integration for Acme.",
            "website_url": "https://acme.example.com"
          }'
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs`, {
            method: "POST",
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              "name": "Acme Safety Labs",
              "description": "Parental-controls integration for Acme.",
              "website_url": "https://acme.example.com"
            }),
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.post(
              f"{BASE}/developers/orgs",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
              json={
                  "name": "Acme Safety Labs",
                  "description": "Parental-controls integration for Acme.",
                  "website_url": "https://acme.example.com"
              },
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"bytes"
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	body := bytes.NewBufferString(`{
            "name": "Acme Safety Labs",
            "description": "Parental-controls integration for Acme.",
            "website_url": "https://acme.example.com"
          }`)
          	req, _ := http.NewRequest("POST", base+"/developers/orgs", body)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	req.Header.Set("Content-Type", "application/json")
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      tags: [Organizations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Display name for the organization.
                description:
                  type: string
                website_url:
                  type: string
      responses:
        '201':
          description: >
            Organization created. A fresh org always starts on the `free` tier with a
            `rate_limit_rpm` of 100, and its `slug` is derived from the name with a short
            random suffix appended for uniqueness. `logo_url` is empty until you set one, and
            `created_at` equals `updated_at`. (Body captured verbatim from the live sandbox.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeveloperOrg'
              # Live wire body captured from the sandbox census (POST name "Docs DX24 Sample").
              example:
                id: a9a85c68-90be-4829-96d3-2038f835db7c
                name: Docs DX24 Sample
                slug: docs-dx24-sample-807b
                description: ''
                website_url: https://example.com
                logo_url: ''
                owner_user_id: 06c5090f-ea5b-4841-a611-e8c9d67df0c5
                tier: free
                rate_limit_rpm: 100
                created_at: '2026-07-06T09:26:48.812564861Z'
                updated_at: '2026-07-06T09:26:48.812564861Z'
        '400':
          description: A required field is missing (management errors carry no `class`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Live wire body captured from the sandbox census (empty body).
              example:
                error: Bad Request
                message: name is required
                code: 400
        '409':
          description: The derived slug collided; retry (a fresh random suffix is appended).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Real handler wire body (developer_handler.go:51).
              example:
                error: Conflict
                message: organization slug already exists, please try again
                code: 409
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
    get:
      operationId: listOrgs
      summary: List developer organizations for the authenticated user
      description: "Returns the developer organizations the authenticated user is a member of. Organizations own API keys, members, and usage."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN"
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs`, {
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
            },
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.get(
              f"{BASE}/developers/orgs",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	req, _ := http.NewRequest("GET", base+"/developers/orgs", nil)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      tags: [Organizations]
      responses:
        '200':
          description: Array of organizations the user belongs to.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DeveloperOrg'
              # Live wire body captured from the sandbox census.
              example:
              - id: ef35953a-44f5-473a-a86f-1341163fa40e
                name: Propagate F1 Test
                slug: propagate-f1-test-a085
                description: ''
                website_url: ''
                logo_url: ''
                owner_user_id: 06c5090f-ea5b-4841-a611-e8c9d67df0c5
                tier: free
                rate_limit_rpm: 100
                created_at: '2026-07-03T10:47:17.133403Z'
                updated_at: '2026-07-03T10:47:17.133403Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
  /developers/orgs/{orgId}:
    parameters:
    - name: orgId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID of the developer organization.
    get:
      operationId: getOrg
      summary: Get a developer organization by ID
      description: "Returns a single developer organization by ID, including its plan and metadata. The caller must be a member."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN"
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V`, {
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
            },
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.get(
              f"{BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	req, _ := http.NewRequest("GET", base+"/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V", nil)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      tags: [Organizations]
      responses:
        '200':
          description: The requested organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeveloperOrg'
              example:
                id: ef35953a-44f5-473a-a86f-1341163fa40e
                name: Propagate F1 Test
                slug: propagate-f1-test-a085
                description: ''
                website_url: ''
                logo_url: ''
                owner_user_id: 06c5090f-ea5b-4841-a611-e8c9d67df0c5
                tier: free
                rate_limit_rpm: 100
                created_at: '2026-07-03T10:47:17.133403Z'
                updated_at: '2026-07-03T10:47:17.133403Z'
        '403':
          description: The authenticated user is not a member of this organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Forbidden
                message: not a member of this organization
                code: 403
        '404':
          description: Organization not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Not Found
                message: developer org not found
                code: 404
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
    put:
      operationId: updateOrg
      summary: Update a developer organization
      description: "Updates mutable fields (such as the name) on a developer organization and returns the updated resource."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X PUT "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN" \
            -H "Content-Type: application/json" \
            -d '{
            "name": "Acme Safety Labs",
            "description": "Updated integration description."
          }'
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V`, {
            method: "PUT",
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              "name": "Acme Safety Labs",
              "description": "Updated integration description."
            }),
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.put(
              f"{BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
              json={
                  "name": "Acme Safety Labs",
                  "description": "Updated integration description."
              },
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"bytes"
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	body := bytes.NewBufferString(`{
            "name": "Acme Safety Labs",
            "description": "Updated integration description."
          }`)
          	req, _ := http.NewRequest("PUT", base+"/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V", body)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	req.Header.Set("Content-Type", "application/json")
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      tags: [Organizations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                description:
                  type: string
                website_url:
                  type: string
      responses:
        '200':
          description: Updated organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeveloperOrg'
              example:
                id: ef35953a-44f5-473a-a86f-1341163fa40e
                name: Propagate F1 Test
                slug: propagate-f1-test-a085
                description: ''
                website_url: ''
                logo_url: ''
                owner_user_id: 06c5090f-ea5b-4841-a611-e8c9d67df0c5
                tier: free
                rate_limit_rpm: 100
                created_at: '2026-07-03T10:47:17.133403Z'
                updated_at: '2026-07-03T10:47:17.133403Z'
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Bad Request
                message: invalid request body
                code: 400
        '403':
          description: The authenticated user does not have admin rights in this organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Forbidden
                message: insufficient role for this action
                code: 403
        '404':
          description: Organization not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Not Found
                message: developer org not found
                code: 404
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
    delete:
      operationId: deleteOrg
      summary: Delete a developer organization
      description: "Permanently deletes a developer organization along with its API keys and members. This cannot be undone."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X DELETE "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN"
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V`, {
            method: "DELETE",
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
            },
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.delete(
              f"{BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	req, _ := http.NewRequest("DELETE", base+"/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V", nil)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      tags: [Organizations]
      responses:
        '204':
          description: Organization deleted.
        '403':
          description: The authenticated user does not have admin rights in this organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Forbidden
                message: insufficient role for this action
                code: 403
        '404':
          description: Organization not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Not Found
                message: developer org not found
                code: 404
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
  /developers/orgs/{orgId}/members:
    parameters:
    - name: orgId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID of the developer organization.
    get:
      operationId: listOrgMembers
      summary: List members of a developer organization
      description: "Returns the members of a developer organization and their roles."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/members" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN"
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/members`, {
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
            },
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.get(
              f"{BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/members",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	req, _ := http.NewRequest("GET", base+"/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/members", nil)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      tags: [Members]
      responses:
        '200':
          description: Array of organization members.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DeveloperOrgMember'
              # Live wire body captured from the sandbox census.
              example:
              - id: 0a7024fe-118b-4380-8d12-52cde86ffda4
                org_id: ef35953a-44f5-473a-a86f-1341163fa40e
                user_id: 06c5090f-ea5b-4841-a611-e8c9d67df0c5
                role: owner
                created_at: '2026-07-03T10:47:17.147343Z'
        '403':
          description: The authenticated user is not a member of this organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Forbidden
                message: not a member of this organization
                code: 403
        '404':
          description: Organization not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Not Found
                message: developer org not found
                code: 404
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
  /developers/orgs/{orgId}/keys:
    parameters:
    - name: orgId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID of the developer organization.
    post:
      operationId: createApiKey
      summary: Create an API key for a developer organization
      description: "Creates an API key for the organization and returns the full secret exactly once. Store it securely — it cannot be retrieved again."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X POST "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN" \
            -H "Content-Type: application/json" \
            -d '{
            "name": "Server (sandbox)",
            "environment": "test",
            "scopes": [
              "policies:write",
              "enforcement:write"
            ]
          }'
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys`, {
            method: "POST",
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              "name": "Server (sandbox)",
              "environment": "test",
              "scopes": [
                "policies:write",
                "enforcement:write"
              ]
            }),
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.post(
              f"{BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
              json={
                  "name": "Server (sandbox)",
                  "environment": "test",
                  "scopes": [
                      "policies:write",
                      "enforcement:write"
                  ]
              },
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"bytes"
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	body := bytes.NewBufferString(`{
            "name": "Server (sandbox)",
            "environment": "test",
            "scopes": [
              "policies:write",
              "enforcement:write"
            ]
          }`)
          	req, _ := http.NewRequest("POST", base+"/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys", body)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	req.Header.Set("Content-Type", "application/json")
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      tags: [API Keys]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, environment]
              properties:
                name:
                  type: string
                  description: Human-readable label for the key.
                environment:
                  type: string
                  enum: [test, live]
                  description: Key environment — test keys cannot call production endpoints.
                scopes:
                  type: array
                  description: >
                    Optional permission scopes granted to this key. Omit the field
                    entirely (or send `[]`) to mint an unscoped key — the documented
                    zero-to-key body `{ "name": …, "environment": "test" }` does exactly
                    that. Every entry must be one of the values in the enum below;
                    an unknown scope is rejected `400 invalid scope requested`.
                  items:
                    type: string
                    enum:
                    - read:families
                    - write:families
                    - read:children
                    - write:children
                    - read:policies
                    - write:policies
                    - read:enforcement
                    - write:enforcement
                    - read:devices
                    - write:devices
                    - read:webhooks
                    - write:webhooks
                    - read:ratings
                    - read:platforms
                  example: ["read:children", "read:policies"]
      responses:
        '201':
          description: >
            Key created. The raw secret (`key`) is returned **only** in this response — store it
            now, it can never be retrieved again. A freshly minted key has no `last_used_at`,
            `last_used_ip`, or `expires_at` yet (those fields are omitted until set), and
            `key_prefix` is the first token of the secret (`phosra_test_<8 hex>`), safe to display
            in a dashboard. (Body captured verbatim from the live sandbox.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeveloperApiKeyWithSecret'
              # Live wire body captured from the sandbox census key-create.
              example:
                id: 6df6a2a1-ed1b-42b6-8901-3c16aca1ff85
                org_id: ef35953a-44f5-473a-a86f-1341163fa40e
                name: docs-dx24-sample
                key_prefix: phosra_test_d0a474be
                environment: test
                scopes:
                - read:children
                - read:policies
                created_by: 06c5090f-ea5b-4841-a611-e8c9d67df0c5
                created_at: '2026-07-06T09:23:04.301651883Z'
                key: phosra_test_d0a474be988b6d558b350d3552e9d8fdd8e7ace606110155ada71849be7f71ff
        '400':
          description: Invalid request body, missing required field, or invalid scope.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Bad Request
                message: 'invalid scope requested: bogus:scope'
                code: 400
        '403':
          description: The authenticated user does not have admin rights in this organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Forbidden
                message: insufficient role for this action
                code: 403
        '404':
          description: Organization not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Not Found
                message: developer org not found
                code: 404
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
    get:
      operationId: listApiKeys
      summary: List API keys for a developer organization
      description: "Returns the API keys for a developer organization. Only key metadata is returned; the secret is shown once, at creation."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN"
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys`, {
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
            },
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.get(
              f"{BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	req, _ := http.NewRequest("GET", base+"/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys", nil)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      tags: [API Keys]
      responses:
        '200':
          description: Array of API keys (secrets are never included in list responses).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/DeveloperApiKey'
              # Live wire body captured from the sandbox census (a never-used key omits
              # last_used_at / last_used_ip / expires_at / revoked_at entirely).
              example:
              - id: 6e40f3da-d4e8-4be0-87c6-a33e803adafc
                org_id: ef35953a-44f5-473a-a86f-1341163fa40e
                name: f1-key2
                key_prefix: phosra_live_b4e5cb24
                environment: live
                scopes: []
                created_by: 06c5090f-ea5b-4841-a611-e8c9d67df0c5
                created_at: '2026-07-03T10:47:35.107215Z'
        '403':
          description: The authenticated user is not a member of this organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Forbidden
                message: not a member of this organization
                code: 403
        '404':
          description: Organization not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Not Found
                message: developer org not found
                code: 404
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
  /developers/orgs/{orgId}/keys/{keyId}:
    parameters:
    - name: orgId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID of the developer organization.
    - name: keyId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID of the API key.
    delete:
      operationId: revokeApiKey
      summary: Revoke (delete) an API key
      description: "Permanently revokes an API key. Requests authenticated with it start failing immediately; this cannot be undone."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X DELETE "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys/key_01HXV3M9A4RK6P7Q8S9T0U1V2W" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN"
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys/key_01HXV3M9A4RK6P7Q8S9T0U1V2W`, {
            method: "DELETE",
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
            },
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.delete(
              f"{BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys/key_01HXV3M9A4RK6P7Q8S9T0U1V2W",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	req, _ := http.NewRequest("DELETE", base+"/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys/key_01HXV3M9A4RK6P7Q8S9T0U1V2W", nil)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      tags: [API Keys]
      responses:
        '204':
          description: >
            Key revoked. Verified idempotent against the live sandbox — revoking an
            already-revoked or non-existent (but well-formed UUID) keyId also returns
            204 with an empty body.
        '403':
          description: The authenticated user does not have admin rights in this organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Forbidden
                message: insufficient role for this action
                code: 403
        '400':
          description: The path `keyId` is not a well-formed UUID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Live wire body captured from the sandbox census (keyId = "not-a-uuid").
              example:
                error: Bad Request
                message: invalid key ID
                code: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
  /developers/orgs/{orgId}/keys/{keyId}/regenerate:
    parameters:
    - name: orgId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID of the developer organization.
    - name: keyId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID of the API key to regenerate.
    post:
      operationId: regenerateApiKey
      summary: Regenerate an API key secret
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X POST "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys/key_01HXV3M9A4RK6P7Q8S9T0U1V2W/regenerate" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN"
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys/key_01HXV3M9A4RK6P7Q8S9T0U1V2W/regenerate`, {
            method: "POST",
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
            },
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.post(
              f"{BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys/key_01HXV3M9A4RK6P7Q8S9T0U1V2W/regenerate",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	req, _ := http.NewRequest("POST", base+"/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/keys/key_01HXV3M9A4RK6P7Q8S9T0U1V2W/regenerate", nil)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      description: >
        Issues a new secret for the given key, invalidating the previous secret.
        The raw secret (`key`) is returned only in this response.
      tags: [API Keys]
      responses:
        '201':
          description: >
            Key regenerated. The new raw secret (`key`) is returned **only** in this response and
            the previous secret stops working immediately. `key_prefix` changes to match the new
            secret. (Body captured verbatim from the live sandbox.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DeveloperApiKeyWithSecret'
              # Live wire body captured from the sandbox census regenerate (note: a fresh
              # `id` + `key_prefix` — regenerate replaces the row, it does not mutate in place).
              example:
                id: 33211807-5a8c-46db-ab7e-96bfa0a4a0b5
                org_id: ef35953a-44f5-473a-a86f-1341163fa40e
                name: docs-dx24-regen
                key_prefix: phosra_test_f3e67094
                environment: test
                scopes:
                - read:children
                - read:policies
                created_by: 06c5090f-ea5b-4841-a611-e8c9d67df0c5
                created_at: '2026-07-06T09:24:17.407347592Z'
                key: phosra_test_f3e6709484d961a872f06e459a77e668d946f4317a3f1a26b276788e589b6bd9
        '403':
          description: The authenticated user does not have admin rights in this organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Forbidden
                message: insufficient role for this action
                code: 403
        '404':
          description: Organization or key not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Not Found
                message: API key not found
                code: 404
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          $ref: '#/components/responses/BadRequest'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
  /developers/orgs/{orgId}/usage:
    parameters:
    - name: orgId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID of the developer organization.
    get:
      operationId: getOrgUsage
      summary: Get hourly API usage rollups for a developer organization
      description: "Returns hourly API-request rollups for the organization over the requested window, for monitoring volume and rate-limit headroom."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS "https://phosra-api-sandbox-production.up.railway.app/api/v1/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/usage" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN"
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/usage`, {
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
            },
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.get(
              f"{BASE}/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/usage",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	req, _ := http.NewRequest("GET", base+"/developers/orgs/org_01HXV2K8Z3QJ5N6P7R8S9T0U1V/usage", nil)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      tags: [Usage]
      parameters:
      - name: days
        in: query
        required: false
        schema:
          type: integer
          minimum: 1
          maximum: 365
          default: 30
        description: Number of past days to include in the usage rollup (default 30, max 365).
      responses:
        '200':
          description: >
            Array of hourly usage records grouped by key and endpoint. NOTE: an org
            with no traffic in the window returns JSON `null` (not `[]`) — verified
            live against the sandbox. The array element shape is shown below.
          content:
            application/json:
              schema:
                type: [array, 'null']
                items:
                  $ref: '#/components/schemas/DeveloperUsage'
              example:
              - id: 8a1e6c37-4b90-42d5-81f6-3c0a9e7b2d48
                key_id: 5d0e9a37-2c81-4b64-9f36-1a8c7e0b4d29
                org_id: ef35953a-44f5-473a-a86f-1341163fa40e
                hour: '2026-07-05T22:00:00Z'
                endpoint: /api/v1/children
                status_2xx: 0
                status_4xx: 0
                status_5xx: 0
                total_requests: 0
        '400':
          description: Invalid `days` parameter value.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Bad Request
                message: days must be between 1 and 365
                code: 400
        '403':
          description: The authenticated user is not a member of this organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Forbidden
                message: not a member of this organization
                code: 403
        '404':
          description: Organization not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Not Found
                message: developer org not found
                code: 404
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
  /advisors/register:
    post:
      operationId: registerAdvisor
      summary: Register an advisor agent into the OCSS federation
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X POST "https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/register" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN" \
            -H "Content-Type: application/json" \
            -d '{
            "slug": "acme-advisor",
            "display_name": "Acme Advisor",
            "publisher_org": "Acme Safety Labs",
            "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...replace-with-your-ed25519-public-key...\n-----END PUBLIC KEY-----",
            "key_id": "acme-advisor#2026-07",
            "capability_domains": [
              "content_rating"
            ],
            "scope": "global"
          }'
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/advisors/register`, {
            method: "POST",
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              "slug": "acme-advisor",
              "display_name": "Acme Advisor",
              "publisher_org": "Acme Safety Labs",
              "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...replace-with-your-ed25519-public-key...\n-----END PUBLIC KEY-----",
              "key_id": "acme-advisor#2026-07",
              "capability_domains": [
                "content_rating"
              ],
              "scope": "global"
            }),
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.post(
              f"{BASE}/advisors/register",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
              json={
                  "slug": "acme-advisor",
                  "display_name": "Acme Advisor",
                  "publisher_org": "Acme Safety Labs",
                  "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...replace-with-your-ed25519-public-key...\n-----END PUBLIC KEY-----",
                  "key_id": "acme-advisor#2026-07",
                  "capability_domains": [
                      "content_rating"
                  ],
                  "scope": "global"
              },
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"bytes"
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	body := bytes.NewBufferString(`{
            "slug": "acme-advisor",
            "display_name": "Acme Advisor",
            "publisher_org": "Acme Safety Labs",
            "public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...replace-with-your-ed25519-public-key...\n-----END PUBLIC KEY-----",
            "key_id": "acme-advisor#2026-07",
            "capability_domains": [
              "content_rating"
            ],
            "scope": "global"
          }`)
          	req, _ := http.NewRequest("POST", base+"/advisors/register", body)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	req.Header.Set("Content-Type", "application/json")
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      description: >
        Registers an advisor agent at provisional tier. For scope=global a DNS
        verification token is returned — publish it as a DNS TXT record at
        `_phosra-verify.<callback domain>` to complete ownership verification.
        An unrecognized capability name causes the request to be rejected with
        400 (`unknown_capability: <name>`). A recognized capability above the
        provisional band is silently dropped from the accepted set.
      security:
      - SessionAuth: []
      tags: [Advisors]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AdvisorRegistration'
      responses:
        '200':
          description: Advisor registered at provisional tier.
          content:
            application/json:
              schema:
                type: object
                properties:
                  advisor_id:
                    type: string
                    format: uuid
                  slug:
                    type: string
                  trust_tier:
                    type: string
                    description: Always `provisional` on initial registration.
                  verification_token:
                    type: string
                    description: DNS verification token. Present only when scope=global.
                  verification_instructions:
                    type: string
                    description: Human-readable DNS TXT record instructions. Present only when scope=global.
              example:
                advisor_id: 7e4c1a90-3b28-4d67-95f1-0a8c6b2e9d43
                slug: touchstone
                trust_tier: provisional
                verification_token: phosra-verify=Xk3n9QpL2mV8sT4wRc7bYd1fHzA0gJ6u
                verification_instructions: Add a DNS TXT record at _phosra-verify.rivera-safety.com with the value from verification_token, then call POST /advisors/{id}/verify.
        '400':
          description: Invalid request body, unknown capability, scope constraint violation, or duplicate slug (`insert_failed`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Live wire body captured from the sandbox census (empty body → scope check).
              example:
                error: Bad Request
                message: 'scope must be "family" or "global"'
                code: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
        '500':
          $ref: '#/components/responses/ServerError'
  /advisors/self-register:
    post:
      operationId: selfRegisterAdvisor
      summary: Self-register a DID onto the sandbox Trust List (sandbox only)
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X POST "https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/self-register" \
            -H "Content-Type: application/json" \
            -d '{
            "did": "did:ocss:acme-advisor",
            "public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
            "roles": [
              "advising-agency"
            ],
            "entry_role": "verifying-agency"
          }'
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/advisors/self-register`, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              "did": "did:ocss:acme-advisor",
              "public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
              "roles": [
                "advising-agency"
              ],
              "entry_role": "verifying-agency"
            }),
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.post(
              f"{BASE}/advisors/self-register",
              json={
                  "did": "did:ocss:acme-advisor",
                  "public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
                  "roles": [
                      "advising-agency"
                  ],
                  "entry_role": "verifying-agency"
              },
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"bytes"
          	"fmt"
          	"io"
          	"net/http"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	body := bytes.NewBufferString(`{
            "did": "did:ocss:acme-advisor",
            "public_key_b64url": "REPLACE_WITH_YOUR_ED25519_PUBLIC_KEY_B64URL",
            "roles": [
              "advising-agency"
            ],
            "entry_role": "verifying-agency"
          }`)
          	req, _ := http.NewRequest("POST", base+"/advisors/self-register", body)
          	req.Header.Set("Content-Type", "application/json")
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      description: >
        Sandbox-only self-service onboarding. Inserts a `did:ocss:<slug>` entry at
        `provisional` tier from a raw Ed25519 public key, then recompiles the served
        Trust List so the DID appears immediately. Requires `SANDBOX_MODE=true` on the
        census — production returns `403 self_register_sandbox_only`. Cannot overwrite an
        existing entry (`409`) and never escalates above `provisional`. No auth is
        required (the sandbox guard is the gate).
      security: []
      tags: [Advisors]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [did, public_key_b64url]
              properties:
                did:
                  type: string
                  description: The DID to register, `did:ocss:<slug>`.
                  example: did:ocss:touchstone
                public_key_b64url:
                  type: string
                  description: >
                    Raw 32-byte Ed25519 public key, base64url **unpadded**
                    (RFC 4648 §5, no `=`). The census PKIX-wraps it for the Trust List.
                  example: CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU
                roles:
                  type: array
                  items:
                    type: string
                  description: Informational role tags, stored verbatim on the provisional entry.
                entry_role:
                  type: string
                  enum: [verifying-agency]
                  description: >
                    Optional Trust-List role marker. Only `verifying-agency` (§5.4 open
                    assessor market) is accepted; any other value returns `400`.
      responses:
        '200':
          description: DID registered at provisional tier and published to the Trust List.
          content:
            application/json:
              schema:
                type: object
                required: [advisor_id, did, key_id, kid, published_key_x, trust_tier]
                properties:
                  advisor_id:
                    type: string
                    format: uuid
                    description: UUID of the advisor row created for this DID (returned first in the live wire body).
                  did:
                    type: string
                  key_id:
                    type: string
                    description: >
                      Your full signing key id, `did:ocss:<slug>#<kid>` — use verbatim as
                      SenderKey.keyID. The census now returns this directly in the 200 body.
                    example: "did:ocss:touchstone#2026-07"
                  kid:
                    type: string
                    description: >
                      The bare kid the census bound — the current UTC month (YYYY-MM) at
                      registration time.
                    example: "2026-07"
                  published_key_x:
                    type: string
                    description: The base64url public key now published on the Trust List entry.
                  trust_tier:
                    type: string
                    description: Always `provisional` — self-service never elevates the tier.
                  entry_role:
                    type: string
                    description: Echoed only when an `entry_role` was requested.
              # Live wire body captured from the sandbox census (self-register a fresh DID).
              # advisor_id is returned FIRST on the wire; entry_role is echoed only when requested.
              example:
                advisor_id: 7e4c1a90-3b28-4d67-95f1-0a8c6b2e9d43
                did: "did:ocss:touchstone"
                key_id: "did:ocss:touchstone#2026-07"
                kid: "2026-07"
                published_key_x: CMHWy3vUAiEcYDdE_bDvkRuEqwxkklS0tV-TYHJTlWU
                trust_tier: provisional
                entry_role: verifying-agency
        '400':
          description: >
            Missing/invalid `did`, invalid `public_key_b64url` (bad base64url or not 32
            bytes), or an `entry_role` other than `verifying-agency`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Live wire body captured from the sandbox census ({did} only, no key).
              example:
                error: Bad Request
                message: public_key_b64url is required
                code: 400
        '403':
          description: Census is not in sandbox mode (`self_register_sandbox_only`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Real handler wire body (advisor_handler.go:190).
              example:
                error: Forbidden
                message: 'self_register_sandbox_only: self-service DID registration is only permitted in sandbox environments (SANDBOX_MODE=true)'
                code: 403
        '409':
          description: The DID/slug is already on the Trust List (`did_already_registered`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Live wire body captured from the sandbox census (re-register same DID).
              example:
                error: Conflict
                message: 'did_already_registered: this DID is already on the trust list; self-register cannot overwrite existing entries'
                code: 409
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
        '500':
          $ref: '#/components/responses/ServerError'
  /advisors/consult:
    post:
      operationId: consultAdvisor
      summary: Request an advisory verdict for a family capability
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X POST "https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/consult" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN" \
            -H "Content-Type: application/json" \
            -d '{
            "family_id": "f0000000-0000-4000-8000-0000000000f1",
            "capability": "content_rating",
            "context": {
              "title": "Bluey",
              "platform": "streaming"
            }
          }'
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/advisors/consult`, {
            method: "POST",
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              "family_id": "f0000000-0000-4000-8000-0000000000f1",
              "capability": "content_rating",
              "context": {
                "title": "Bluey",
                "platform": "streaming"
              }
            }),
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.post(
              f"{BASE}/advisors/consult",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
              json={
                  "family_id": "f0000000-0000-4000-8000-0000000000f1",
                  "capability": "content_rating",
                  "context": {
                      "title": "Bluey",
                      "platform": "streaming"
                  }
              },
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"bytes"
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	body := bytes.NewBufferString(`{
            "family_id": "f0000000-0000-4000-8000-0000000000f1",
            "capability": "content_rating",
            "context": {
              "title": "Bluey",
              "platform": "streaming"
            }
          }`)
          	req, _ := http.NewRequest("POST", base+"/advisors/consult", body)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	req.Header.Set("Content-Type", "application/json")
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      description: >
        Dispatches a consultation to the best available advisor for the given
        capability domain. Returns a verdict and a consultation ID for audit
        purposes. If no suitable advisor is registered, verdict is `unavailable`
        with reason `no_advisor` — this is still a 200 response.
      security:
      - SessionAuth: []
      tags: [Advisors]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [family_id, capability]
              properties:
                family_id:
                  type: string
                  format: uuid
                  description: UUID of the family for which the consultation is requested.
                capability:
                  type: string
                  description: Advisory capability domain being consulted.
                context:
                  type: object
                  additionalProperties: true
                  description: Freeform key-value context passed verbatim to the advisor.
                advisor_id:
                  type: [string, "null"]
                  description: UUID of a specific advisor to consult. Omit to use the default for this capability.
      responses:
        '200':
          description: Consultation result. Verdict may be `unavailable` if no suitable advisor is registered.
          content:
            application/json:
              schema:
                type: object
                properties:
                  verdict:
                    type: string
                    description: Advisory verdict — one of `allow`, `deny`, `flag`, or `unavailable`.
                  consultation_id:
                    type: string
                    description: Audit ID for this consultation. Empty string when no advisor is available (verdict `unavailable`).
                  reason:
                    type: string
                    description: Explanation for the verdict. Present on `unavailable` (e.g. `no_advisor`).
              example:
                verdict: allow
                consultation_id: 1f6b3d84-9c07-42a5-8e19-0d7a3c6b2f84
                reason: matched_accredited_advisor
        '400':
          description: >
            Invalid request body or consultation error — the message is always
            prefixed `consult_failed:` (the underlying detail follows).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Real handler wire body shape (advisor_handler.go:527 — "consult_failed: "+err).
              example:
                error: Bad Request
                message: 'consult_failed: invalid request body'
                code: 400
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
        '500':
          $ref: '#/components/responses/ServerError'
  /advisors/{id}/payload-key:
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: string
      description: Advisor agent ID.
    post:
      operationId: declareAdvisorPayloadKey
      summary: Declare the advisor's OCSS envelope payload public key
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X POST "https://phosra-api-sandbox-production.up.railway.app/api/v1/advisors/did:ocss:loopline/payload-key" \
            -H "Content-Type: application/json" \
            -d '{
            "payload_public_key_jwk": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"...\",\"y\":\"...\"}",
            "sig": "base64url-ed25519-signature-over-the-jwk",
            "issued_at": "2026-07-06T14:30:00Z"
          }'
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/advisors/did:ocss:loopline/payload-key`, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              "payload_public_key_jwk": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"...\",\"y\":\"...\"}",
              "sig": "base64url-ed25519-signature-over-the-jwk",
              "issued_at": "2026-07-06T14:30:00Z"
            }),
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.post(
              f"{BASE}/advisors/did:ocss:loopline/payload-key",
              json={
                  "payload_public_key_jwk": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"...\",\"y\":\"...\"}",
                  "sig": "base64url-ed25519-signature-over-the-jwk",
                  "issued_at": "2026-07-06T14:30:00Z"
              },
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"bytes"
          	"fmt"
          	"io"
          	"net/http"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	body := bytes.NewBufferString(`{
            "payload_public_key_jwk": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"...\",\"y\":\"...\"}",
            "sig": "base64url-ed25519-signature-over-the-jwk",
            "issued_at": "2026-07-06T14:30:00Z"
          }`)
          	req, _ := http.NewRequest("POST", base+"/advisors/did:ocss:loopline/payload-key", body)
          	req.Header.Set("Content-Type", "application/json")
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      description: >
        Self-service possession-auth declaration. Authentication is by the
        advisor's registered Ed25519 signing key: the body must carry a detached
        signature over the exact `payload_public_key_jwk` string bytes plus a
        freshness timestamp. No bearer token scheme applies to this operation —
        authorization is established by possession of the registered private key.
        Clearing a declared key is not self-service — use the admin revoke flow.
      security: []
      tags: [Advisors]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [payload_public_key_jwk, sig, issued_at]
              properties:
                payload_public_key_jwk:
                  type: string
                  description: >
                    Serialized EC P-256 public JWK (≤4 KiB). The `kid` must form
                    a valid `did#kid` distinct from the advisor's signing `key_id`.
                sig:
                  type: string
                  description: >
                    Detached Ed25519 signature over the exact `payload_public_key_jwk`
                    bytes, signed with the advisor's registered private key.
                issued_at:
                  type: string
                  format: date-time
                  description: RFC3339 timestamp of this declaration (freshness-bound; stale declarations are rejected).
      responses:
        '200':
          description: Payload key declared and stored in the registry.
          content:
            application/json:
              schema:
                type: object
                properties:
                  advisor_id:
                    type: string
                  payload_key_kid:
                    type: string
                  status:
                    type: string
                    enum: [declared]
              example:
                advisor_id: 7e4c1a90-3b28-4d67-95f1-0a8c6b2e9d43
                payload_key_kid: "did:ocss:touchstone#payload-2026-07"
                status: declared
        '400':
          description: Invalid body, missing required field, or JWK rejected by the registry validator.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Live wire body captured from the sandbox census (empty body).
              example:
                error: Bad Request
                message: payload_public_key_jwk is required (clearing a declared key is not self-service)
                code: 400
        '401':
          description: Declaration signature is invalid or stale.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Real handler wire body (advisor_handler.go:729).
              example:
                error: Unauthorized
                message: declaration signature invalid or stale
                code: 401
        '403':
          description: Advisor is revoked.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Real handler wire body (advisor_handler.go:720).
              example:
                error: Forbidden
                message: advisor is revoked
                code: 403
        '404':
          description: Advisor not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Real handler wire body (advisor_handler.go:713).
              example:
                error: Not Found
                message: advisor not found
                code: 404
        '413':
          description: Request body exceeds the 16 KiB declaration size bound.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              # Real handler wire body (advisor_handler.go:694).
              example:
                error: Request Entity Too Large
                message: request body exceeds the declaration size bound
                code: 413
        '429':
          $ref: '#/components/responses/RateLimited'
        '502':
          $ref: '#/components/responses/BadGateway'
        '503':
          $ref: '#/components/responses/ServiceUnavailable'
        '500':
          $ref: '#/components/responses/ServerError'
  /mcp-tokens:
    get:
      operationId: listMcpTokens
      summary: List MCP tokens for the authenticated user
      description: "Returns the MCP tokens issued to the authenticated user. Only token metadata is returned, never the secret."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS "https://phosra-api-sandbox-production.up.railway.app/api/v1/mcp-tokens" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN"
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/mcp-tokens`, {
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
            },
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.get(
              f"{BASE}/mcp-tokens",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	req, _ := http.NewRequest("GET", base+"/mcp-tokens", nil)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      security:
      - SessionAuth: []
      tags: [MCP Tokens]
      responses:
        '200':
          description: >
            Object wrapping the array of MCP tokens. NOTE: a user with no tokens gets
            `{"tokens": null}` (the array is `null`, not `[]`) — verified live against
            the sandbox. A populated element is shown below.
          content:
            application/json:
              schema:
                type: object
                properties:
                  tokens:
                    type: [array, 'null']
                    items:
                      $ref: '#/components/schemas/McpToken'
              example:
                tokens:
                - id: f456e4ac-d77c-411b-9061-88a7a1befc3b
                  user_id: 06c5090f-ea5b-4841-a611-e8c9d67df0c5
                  family_id: 63069024-1a49-4b95-80e8-f1b1b769355e
                  label: Untitled
                  token_prefix: phomcp_ZTFPEH7X
                  last_used_at: null
                  created_at: '2026-07-06T09:25:30Z'
                  revoked_at: null
        '401':
          description: Authentication required.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Unauthorized
                message: auth required
                code: 401
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
    post:
      operationId: createMcpToken
      summary: Create a new MCP token
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X POST "https://phosra-api-sandbox-production.up.railway.app/api/v1/mcp-tokens" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN" \
            -H "Content-Type: application/json" \
            -d '{
            "label": "Claude Desktop"
          }'
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/mcp-tokens`, {
            method: "POST",
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              "label": "Claude Desktop"
            }),
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.post(
              f"{BASE}/mcp-tokens",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
              json={
                  "label": "Claude Desktop"
              },
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"bytes"
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	body := bytes.NewBufferString(`{
            "label": "Claude Desktop"
          }`)
          	req, _ := http.NewRequest("POST", base+"/mcp-tokens", body)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	req.Header.Set("Content-Type", "application/json")
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      description: >
        Issues a new MCP token bound to the authenticated user's first family.
        The plaintext token (`plain`, prefix `phomcp_`) is returned once in
        `CreatedMcpToken` and never stored server-side — the caller must display
        it to the user immediately.
      security:
      - SessionAuth: []
      tags: [MCP Tokens]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                label:
                  type: string
                  description: Human-readable label for the token. Defaults to `Untitled` if omitted or empty.
      responses:
        '201':
          description: Token created. The raw `plain` token is returned only in this response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreatedMcpToken'
              # Live wire body captured from the sandbox census (empty-label create ⇒
              # label "Untitled"; created_at is the zero time in this response and is
              # backfilled server-side). token_prefix carries the 8-char public id suffix.
              example:
                token:
                  id: f456e4ac-d77c-411b-9061-88a7a1befc3b
                  user_id: 06c5090f-ea5b-4841-a611-e8c9d67df0c5
                  family_id: 63069024-1a49-4b95-80e8-f1b1b769355e
                  label: Untitled
                  token_prefix: phomcp_ZTFPEH7X
                  created_at: '0001-01-01T00:00:00Z'
                plain: phomcp_ZTFPEH7X4BJLABIULVPZE6CCZY5WVEU6
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Bad Request
                message: invalid body
                code: 400
        '401':
          description: Authentication required.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Unauthorized
                message: auth required
                code: 401
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
  /mcp-tokens/{id}:
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: UUID of the MCP token to revoke.
    delete:
      operationId: revokeMcpToken
      summary: Revoke an MCP token
      description: "Permanently revokes an MCP token so the associated MCP client can no longer authenticate. This cannot be undone."
      x-codeSamples:
      - lang: Shell
        label: cURL
        source: |
          curl -sS -X DELETE "https://phosra-api-sandbox-production.up.railway.app/api/v1/mcp-tokens/mtk_01HXV4N0B5SL7Q8R9T0U1V2W3X" \
            -H "Authorization: Bearer $PHOSRA_SESSION_TOKEN"
      - lang: JavaScript
        label: Node.js
        source: |
          const BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1";
          const res = await fetch(`${BASE}/mcp-tokens/mtk_01HXV4N0B5SL7Q8R9T0U1V2W3X`, {
            method: "DELETE",
            headers: {
              "Authorization": `Bearer ${process.env.PHOSRA_SESSION_TOKEN}`,
            },
          });
          console.log(res.status, await res.json());
      - lang: Python
        label: Python
        source: |
          import os, requests

          BASE = "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          res = requests.delete(
              f"{BASE}/mcp-tokens/mtk_01HXV4N0B5SL7Q8R9T0U1V2W3X",
              headers={"Authorization": f"Bearer {os.environ['PHOSRA_SESSION_TOKEN']}"},
          )
          print(res.status_code, res.json())
      - lang: Go
        label: Go
        source: |
          package main

          import (
          	"fmt"
          	"io"
          	"net/http"
          	"os"
          )

          func main() {
          	base := "https://phosra-api-sandbox-production.up.railway.app/api/v1"
          	req, _ := http.NewRequest("DELETE", base+"/mcp-tokens/mtk_01HXV4N0B5SL7Q8R9T0U1V2W3X", nil)
          	req.Header.Set("Authorization", "Bearer "+os.Getenv("PHOSRA_SESSION_TOKEN"))
          	resp, err := http.DefaultClient.Do(req)
          	if err != nil {
          		panic(err)
          	}
          	defer resp.Body.Close()
          	out, _ := io.ReadAll(resp.Body)
          	fmt.Println(resp.Status, string(out))
          }
      security:
      - SessionAuth: []
      tags: [MCP Tokens]
      responses:
        '204':
          description: Token revoked.
        '400':
          description: Invalid token ID (not a valid UUID).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Bad Request
                message: invalid id
                code: 400
        '401':
          description: Authentication required.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Unauthorized
                message: auth required
                code: 401
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/ServerError'
