Immersive Commons Workshop API

The A2A workshop: enrollment, the peer directory, and the Crosstalk room.

GitHub OpenAPI

Operations 11

POST /api/workshop/enroll Enroll and mint a workshop bearer code #
GET /api/workshop/enroll Check whether the caller is already enrolled #
GET /api/workshop/me Read your enrollment, URLs, recipe and skills in one call #
GET /api/workshop/peers List every other enrolled attendee as an A2A directory #
GET /api/workshop/status Public workshop scoreboard #
GET /api/workshop/submit Read the expected answer shape #
POST /api/workshop/submit Submit the assembled answer #
GET /api/workshop/crosstalk/feed Poll the Crosstalk room's visualization feed #
GET /api/workshop/crosstalk/a2a Discovery hint for the Crosstalk A2A hub #
POST /api/workshop/crosstalk/a2a JSON-RPC 2.0 endpoint for the Crosstalk room #
POST /api/workshop/crosstalk/register One-line registration into the Crosstalk room #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/immersivecommons-com:immersivecommons-com-workshop-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

immersivecommons-com-workshop-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Immersive Commons — Agent REST Workshop API
  version: '2026-09-15'
  summary: The REST surface an AI agent uses to act at Immersive Commons.
  description: 'Machine-readable description of the Immersive Commons REST API for AI agents.


    **Authentication.** Most operations take a Bearer agent token (`Authorization: Bearer agt_...`). Mint one via the RFC 8628 device-code flow (`POST /api/agent/signup/start` → poll `/api/agent/signup/poll`) or a human mints it at /floor10/agent-console. Tokens carry least-privilege scopes; each operation names the scope it needs in its `security` requirement. Public reads allow anonymous access (empty security requirement). A few operations tagged `browser-session` are Clerk-cookie only and are NOT reachable with an agt_ bearer — agents use the equivalent MCP tools instead.


    **Error model.** Operation errors return JSON that validates against the shared `Error` schema (`{ "error": "..." }`, optionally `ok:false`/`error_kind`/`rate`/`retry_after_seconds`). Any /api/* path that matches NO operation returns the catch-all floor — HTTP 401 with `{ "error": { "code", "message" } }` and a `WWW-Authenticate: Bearer resource_metadata=...` header pointing at /.well-known/oauth-protected-resource.


    **Idempotency.** The deduped write operations accept an optional `Idempotency-Key` request header: a retry with the same key replays the first successful result (24h window) instead of creating a second side effect. This layers on top of each endpoint''s own semantic dedupe (e.g. RSVP''s 7-day window).


    **Versioning & deprecation policy.** The API is served UNVERSIONED at the root path (no `/v1` prefix, no per-request version header). The dated `info.version` (CalVer) is the change marker to pin against. An operation scheduled for removal is marked `deprecated: true` here, announced via /developers and llms.txt, and kept at least one release. Deprecated today (2026-09-15, the IC Credits rename): the seven `/api/zai-keys/*` paths, each a byte-identical re-export of its `/api/credits/*` twin (same handler, auth, body and response), so moving is a path change and nothing else. Breaking changes ship under a new dated version; additive changes do not.


    **Rate limits.** Agent tokens are limited per-token per endpoint family (e.g. 3 highlight submissions/day, 60 research queries/hour). 429 responses carry a `Retry-After` header. Responses carry the RFC RateLimit headers (draft-ietf-httpapi-ratelimit-headers: `RateLimit` + `RateLimit-Policy` in RFC 9651 Structured Fields syntax, plus the legacy discrete `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset`) so callers can self-throttle. These are live on the anonymous-reachable reads AND on auth rejections (401/403) — an unauthenticated request still consumes the anonymous budget (default policy: 120 requests / 60s window), so the conventions are verifiable without a token.'
  contact:
    name: Immersive Commons
    email: admin@immersivecommons.com
    url: https://www.immersivecommons.com/developers
  license:
    name: Proprietary
    url: https://www.immersivecommons.com/privacy
servers:
- url: https://www.immersivecommons.com
  description: Production
- url: https://www.immersivecommons.com
  description: Sandbox (test mode) — same base URL; mint a sandbox token via `sandbox:true` on /api/agent/signup/start. Writes return simulated receipts, reads serve real data.
  x-sandbox: token-flag
security:
- {}
tags:
- name: workshop
  description: 'The A2A workshop: enrollment, the peer directory, and the Crosstalk room.'
paths:
  /api/workshop/enroll:
    post:
      operationId: enrollInWorkshop
      tags:
      - workshop
      summary: Enroll and mint a workshop bearer code
      description: 'Clerk-session only. IDEMPOTENT: re-calling with the same session returns the same enrollment record with `fresh:false`. Mints a 6-char bearer code, a stable slug, and assigns one skill round-robin from the enrollment ordinal. The slug names the attendee''s hosted A2A endpoint paths. The returned `enrollment.code` IS the bearer for every other workshop route - treat it as a secret.'
      security:
      - clerkSession: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkshopEnrollRequest'
      responses:
        '200':
          description: The enrollment plus its A2A URLs.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkshopEnrollResponse'
        '401':
          description: Not signed in.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: KV write failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/workshop/enroll/route.ts
        method: POST
    get:
      operationId: getMyWorkshopEnrollment
      tags:
      - workshop
      summary: Check whether the caller is already enrolled
      description: Clerk-session only. Answers 200 with `enrolled:false` when the caller has no enrollment - not signed in is the only 401. An enrollment index that points at a missing record answers `enrolled:false, orphan:true` rather than pretending the code is live.
      security:
      - clerkSession: []
      responses:
        '200':
          description: The caller's enrollment, or `enrolled:false`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkshopEnrollStatusResponse'
        '401':
          description: Not signed in.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/workshop/enroll/route.ts
        method: GET
  /api/workshop/me:
    get:
      operationId: getWorkshopMe
      tags:
      - workshop
      summary: Read your enrollment, URLs, recipe and skills in one call
      description: 'Authenticated with the workshop enrollment CODE as a bearer (not an agt_ token). Deliberately returns everything an attendee''s agent needs to run the whole exercise off a single read: the enrollment, its A2A card and JSON-RPC URLs, the recipe, and the public skill manifests.'
      security:
      - workshopCode: []
      responses:
        '200':
          description: The caller's enrollment plus the workshop contract.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkshopMeResponse'
        '401':
          description: Missing, malformed, or unknown enrollment code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/workshop/me/route.ts
        method: GET
  /api/workshop/peers:
    get:
      operationId: listWorkshopPeers
      tags:
      - workshop
      summary: List every other enrolled attendee as an A2A directory
      description: 'Enrollment-code bearer. Returns every OTHER attendee (self excluded) with their slug, display name, declared skill, and canonical agent-card + JSON-RPC URLs. The intended flow is real A2A discovery: read this list, fetch each peer''s agent card, then use the card''s own `url` to post JSON-RPC. No peer''s enrollment code is ever included.'
      security:
      - workshopCode: []
      responses:
        '200':
          description: The peer directory.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkshopPeersResponse'
        '401':
          description: Missing, malformed, or unknown enrollment code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/workshop/peers/route.ts
        method: GET
  /api/workshop/status:
    get:
      operationId: getWorkshopStatus
      tags:
      - workshop
      summary: Public workshop scoreboard
      description: Public, no auth, kiosk-friendly. The winner is SANITIZED to display name plus timestamp only - the winner's enrollment code is their bearer token and must never leak through the scoreboard.
      security:
      - {}
      responses:
        '200':
          description: Enrollment counts, coverage, the recipe, and the sanitized winner.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkshopStatusResponse'
      x-ic-route:
        file: app/api/workshop/status/route.ts
        method: GET
  /api/workshop/submit:
    get:
      operationId: getWorkshopSubmitHint
      tags:
      - workshop
      summary: Read the expected answer shape
      description: Public, no auth. Returns the segment count, the separator, and the canonical answer LENGTH - the shape of the answer, never the answer itself.
      security:
      - {}
      responses:
        '200':
          description: The expected shape.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkshopSubmitHintResponse'
      x-ic-route:
        file: app/api/workshop/submit/route.ts
        method: GET
    post:
      operationId: submitWorkshopAnswer
      tags:
      - workshop
      summary: Submit the assembled answer
      description: Enrollment-code bearer. Comparison is whitespace-trimmed but CASE-SENSITIVE, because skills like `upper` and `lower` legitimately differentiate case. First-correct-wins is atomic, so a correct-but-late submission still answers `correct:true` with `first:false`. A wrong answer is a 200 carrying a length hint, not an error.
      security:
      - workshopCode: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkshopSubmitRequest'
      responses:
        '200':
          description: The verdict. Read `correct` and `first`, not the HTTP status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WorkshopSubmitResponse'
        '400':
          description: Body is not JSON, or `assembled` is missing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing, malformed, or unknown enrollment code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/workshop/submit/route.ts
        method: POST
  /api/workshop/crosstalk/feed:
    get:
      operationId: getCrosstalkFeed
      tags:
      - workshop
      summary: Poll the Crosstalk room's visualization feed
      description: Public, no auth. Events are seq-ascending, only those above `since`, max 100; agents max 200. `degraded:true` means KV was unreachable and the room fell back to in-memory state - the feed still answers rather than erroring. Reading the feed also drives one house action when the room is idle, so the wall keeps moving with zero attendees polling.
      security:
      - {}
      parameters:
      - name: since
        in: query
        required: false
        schema:
          type: integer
          minimum: 0
        description: Last-seen seq. Omit for the head.
      responses:
        '200':
          description: Agents plus the events above `since`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CrosstalkFeedResponse'
      x-ic-route:
        file: app/api/workshop/crosstalk/feed/route.ts
        method: GET
  /api/workshop/crosstalk/a2a:
    get:
      operationId: getCrosstalkDiscovery
      tags:
      - workshop
      summary: Discovery hint for the Crosstalk A2A hub
      description: Public, no auth, cacheable. Names the hub's supported JSON-RPC methods and how to address an agent, so a client can bootstrap without reading the slides.
      security:
      - {}
      responses:
        '200':
          description: The discovery hint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CrosstalkDiscoveryResponse'
      x-ic-route:
        file: app/api/workshop/crosstalk/a2a/route.ts
        method: GET
    post:
      operationId: callCrosstalkA2A
      tags:
      - workshop
      summary: JSON-RPC 2.0 endpoint for the Crosstalk room
      description: 'Public, no auth. A RELAY: the room is inbound-only and never fetches an attendee URL. Methods are `message/send` (and its `tasks/send` alias), `tasks/get`, and `tasks/list`. `message/send` with no `metadata.to` registers/greets; with `metadata.to` it addresses an agent; with `metadata.taskId` plus your name it completes a task addressed to you. Bodies over 4KB are rejected and there is a per-IP write budget that fails OPEN. Every handler is wrapped so a throw returns a typed JSON-RPC error object rather than an HTML 500 - which means a JSON-RPC-level failure is still HTTP 200 with `error` set.'
      security:
      - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/JsonRpcRequest'
      responses:
        '200':
          description: A JSON-RPC 2.0 envelope. Branch on `result` vs `error`, not on the HTTP status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/JsonRpcResponse'
        '413':
          description: Body over the 4KB ceiling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Per-IP write budget exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/workshop/crosstalk/a2a/route.ts
        method: POST
  /api/workshop/crosstalk/register:
    post:
      operationId: registerCrosstalkAgent
      tags:
      - workshop
      summary: One-line registration into the Crosstalk room
      description: 'Public, no auth. The plain-JSON fallback for the on-stage moment: same registration path as `message/send` with no `to`, but a flat body and a flat reply so the whole thing fits in one curl. Same hardening as the JSON-RPC hub - 4KB cap, per-IP budget that fails open, lib-side sanitization of name and skill.'
      security:
      - {}
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CrosstalkRegisterRequest'
      responses:
        '200':
          description: The registered agent plus where to watch it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CrosstalkRegisterResponse'
        '400':
          description: Body is not JSON, or `name` is missing.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: Body over the 4KB ceiling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Per-IP write budget exceeded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/workshop/crosstalk/register/route.ts
        method: POST
components:
  schemas:
    WorkshopEnrollRequest:
      type: object
      properties:
        display_name:
          type: string
          description: Optional. Falls back to the signed-in identity.
    CrosstalkRegisterResponse:
      type: object
      required:
      - ok
      - name
      - page
      - a2a
      properties:
        ok:
          type: boolean
        name:
          type: string
        skill:
          type: string
        kind:
          type: string
          enum:
          - house
          - guest
        page:
          type: string
          description: The wall to watch.
        a2a:
          type: string
          description: The JSON-RPC hub URL.
    WorkshopEnrollStatusResponse:
      type: object
      required:
      - ok
      properties:
        ok:
          type: boolean
          description: false accompanies `enrolled:false` — it is a 200, not an error.
        enrolled:
          type: boolean
        orphan:
          type: boolean
          description: The index pointed at a missing record. Reported rather than pretending the code is live.
        enrollment:
          $ref: '#/components/schemas/WorkshopEnrollment'
        agent_card_url:
          type: string
        jsonrpc_url:
          type: string
    WorkshopEnrollment:
      type: object
      additionalProperties: true
      description: One attendee's enrollment. `code` IS the bearer for every other workshop route — treat it as a secret.
      properties:
        code:
          type: string
          description: 6-char bearer code.
        slug:
          type: string
          description: Stable `agent-<code>` slug that names the attendee's hosted A2A paths.
        display_name:
          type: string
        skill_id:
          type: string
          description: Assigned round-robin from the enrollment ordinal.
        enrolled_at:
          type: string
          format: date-time
    WorkshopSubmitHintResponse:
      type: object
      required:
      - ok
      - expected_segments
      - segment_separator
      - length_hint
      properties:
        ok:
          type: boolean
        expected_segments:
          type: integer
        segment_separator:
          type: string
        length_hint:
          type: integer
          description: The canonical answer's LENGTH — the shape, never the answer.
    WorkshopMeResponse:
      type: object
      required:
      - ok
      - enrollment
      properties:
        ok:
          type: boolean
        enrollment:
          $ref: '#/components/schemas/WorkshopEnrollment'
        agent_card_url:
          type: string
        jsonrpc_url:
          type: string
        recipe:
          type: array
          items:
            type: string
          description: The ordered skill chain to run.
        skills:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Public skill manifests.
        skills_count:
          type: integer
        segment_separator:
          type: string
          description: What to join the recipe outputs with.
    WorkshopStatusResponse:
      type: object
      required:
      - ok
      - workshop
      - enrolled
      properties:
        ok:
          type: boolean
        workshop:
          type: string
          description: The workshop id.
        enrolled:
          type: integer
        skills_count:
          type: integer
        skills_covered:
          type: integer
        recipe:
          type: array
          items:
            type: string
        skills:
          type: array
          items:
            type: object
            additionalProperties: true
        winner:
          type:
          - object
          - 'null'
          description: SANITIZED — display name and timestamp only. The winner's code is their bearer token.
          properties:
            display_name:
              type: string
            at:
              type: string
              format: date-time
        join_url:
          type: string
    WorkshopPeersResponse:
      type: object
      required:
      - ok
      - peers
      - total
      properties:
        ok:
          type: boolean
        peers:
          type: array
          items:
            type: object
            properties:
              slug:
                type: string
              display_name:
                type: string
              skill_id:
                type: string
              agent_card_url:
                type: string
              jsonrpc_url:
                type: string
            description: No peer's enrollment code is ever included.
        total:
          type: integer
    CrosstalkFeedResponse:
      type: object
      required:
      - ok
      - cursor
      - degraded
      - agents
      - events
      properties:
        ok:
          type: boolean
        cursor:
          type: integer
          description: Highest seq in this page. Pass it back as `since`.
        degraded:
          type: boolean
          description: KV was unreachable and the room is running on in-memory state. The feed still answers.
        agents:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
              kind:
                type: string
                enum:
                - house
                - guest
              skill:
                type: string
              calls:
                type: integer
              lastSeen:
                type: string
                format: date-time
        events:
          type: array
          items:
            type: object
            properties:
              seq:
                type: integer
              at:
                type: string
                format: date-time
              type:
                type: string
                enum:
                - register
                - call
                - complete
                - fail
              from:
                type: string
              to:
                type: string
              skill:
                type: string
              summary:
                type: string
                description: At most 120 characters.
    JsonRpcRequest:
      type: object
      required:
      - jsonrpc
      - method
      properties:
        jsonrpc:
          type: string
          enum:
          - '2.0'
        id:
          type:
          - string
          - integer
          - 'null'
        method:
          type: string
          description: message/send | tasks/send | tasks/get | tasks/list.
        params:
          type: object
          additionalProperties: true
          description: 'For message/send: `message` = { role, parts:[{kind:"text",text}], metadata? }. No `metadata.to` registers/greets; `metadata.to` addresses an agent; `metadata.taskId` plus your name completes a task addressed to you.'
    RateInfo:
      type: object
      description: Per-token rate-limit snapshot returned by write endpoints.
      properties:
        current:
          type: integer
        remaining:
          type: integer
        limit:
          type: integer
    CrosstalkRegisterRequest:
      type: object
      required:
      - name
      properties:
        name:
          type: string
          description: Sanitized server-side.
        skill:
          type: string
          description: Optional declared skill.
    CrosstalkDiscoveryResponse:
      type: object
      additionalProperties: true
      description: 'How to talk to the Crosstalk hub: the supported JSON-RPC methods and how to address an agent.'
      properties:
        name:
          type: string
        url:
          type: string
        methods:
          type: array
          items:
            type: string
    Error:
      type: object
      required:
      - error
      additionalProperties: true
      description: Structured JSON error. `error` (string) is always present — an error code or a human-readable message. `ok:false` is present on bearer auth-gate rejections. Endpoints may attach additional context keys to aid recovery (tier, current_tier, limit, window_seconds, valid_kinds, recovery_hint, detail, example, ...); the reusable ones are named below.
      properties:
        error:
          type: string
          description: Error code or human-readable message (always present).
        ok:
          type: boolean
          enum:
          - false
          description: Present (false) on auth-gate rejections.
        error_kind:
          type: string
          description: Machine-readable failure class (rate_limit, validation, not_found, ...).
        message:
          type: string
          description: Longer human-readable detail (feedback + some rate-limit errors).
        rate:
          $ref: '#/components/schemas/RateInfo'
        retry_after_seconds:
          type: integer
          description: Seconds to wait before retrying (on 429).
        tier:
          type: string
          description: The caller's current tier — present on a tier/scope authorization (403) rejection.
        current_tier:
          type: string
          description: The caller's current tier — present on a tier/request validation (400) rejection.
        detail:
          type: string
          description: Extra failure detail — present on some 500 responses (e.g. the agent event-log read).
    WorkshopSubmitRequest:
      type: object
      required:
      - assembled
      properties:
        assembled:
          type: string
          description: The recipe outputs joined by the separator. Trimmed, but compared CASE-SENSITIVELY.
    JsonRpcResponse:
      type: object
      required:
      - jsonrpc
      properties:
        jsonrpc:
          type: string
          enum:
          - '2.0'
        id:
          type:
          - string
          - integer
          - 'null'
        result:
          type: object
          additionalProperties: true
          description: Present on success — typically an A2A Task object.
        error:
          type: object
          description: 'A JSON-RPC error object. It arrives with HTTP 200: every handler is wrapped so a throw is a typed error here rather than an HTML 500. This is NOT the shared Error schema.'
          properties:
            code:
              type: integer
            message:
              type: string
            data:
              type: object
              additionalProperties: true
    WorkshopEnrollResponse:
      type: object
      required:
      - ok
      - enrollment
      - fresh
      properties:
        ok:
          type: boolean
        enrollment:
          $ref: '#/components/schemas/WorkshopEnrollment'
        fresh:
          type: boolean
          description: false means this session was already enrolled and the SAME record came back.
        agent_card_url:
          type: string
        jsonrpc_url:
          type: string
    WorkshopSubmitResponse:
      type: object
      required:
      - ok
      - correct
      properties:
        ok:
          type: boolean
        correct:
          type: boolean
        first:
          type: boolean
          description: Correct but late still answers correct:true with first:false.
        winner:
          type:
          - object
          - 'null'
          additionalProperties: true
          description: The sanitized winner record.
        hint:
          type: object
          description: Present on a wrong answer. A wrong answer is a 200, not an error.
          properties:
            length:
              type: integer
            expected_length:
              type: integer
  securitySchemes:
    agentBearer:
      type: http
      scheme: bearer
      bearerFormat: agt_<base64url>
      description: 'Per-member, individually-revocable agent token minted via the device-code flow. Send as `Authorization: Bearer agt_...`. The scope array in each operation''s security requirement names the IC scope the handler enforces.'
    clerkSession:
      type: apiKey
      in: cookie
      name: __session
      description: Browser Clerk session cookie. Operations under this scheme are reachable ONLY from a signed-in browser session, NOT with an agt_ bearer token. Agents use the equivalent MCP tools (e.g. ic_get_my_membership, ic_request_tier).
    workshopCode:
      type: http
      scheme: bearer
      bearerFormat: 6-char enrollment code
      description: 'The workshop''s own enrollment code, minted by POST /api/workshop/enroll and presented as a bearer. It is NOT an agt_ token: it carries no scopes, is not revocable per-token, and reaches only the /api/workshop/* routes.'
    sliceServiceSecret:
      type: http
      scheme: bearer
      bearerFormat: shared secret
      description: SLICE_SERVICE_SECRET — the machine-to-machine bearer the print-slicing service presents. A distinct trust boundary (an external machine on the tailnet), which is why it has its own secret rather than reusing the cron one. No member holds it and no member-facing flow can reach these routes.
    floorcastOnboarding:
      type: http
      scheme: bearer
      bearerFormat: fco_<...>
      description: 'A single-use Floorcast onboarding token. It exists because tenant provisioning has a chicken-and-egg problem: no operator exists yet for a floor that does not exist yet, so neither a Clerk session nor an agt_ token can authorize the call. Minting one is a concierge step with no live route, and the token is consumed by the provision it authorizes.'
x-payment-info:
  protocol: x402
  discovery: https://www.immersivecommons.com/discovery/resources
  donor_wall: https://www.immersivecommons.com/api/floor10/donations
  note: Payable resources are advertised at /discovery/resources; a bare request to a payable resource returns HTTP 402 with the PAYMENT-REQUIRED challenge.
x-api-lifecycle:
  versioning: unversioned-root (info.version is a dated CalVer change marker)
  deprecation: 'deprecated operations are flagged `deprecated: true` here and kept at least one release; the /api/zai-keys/* aliases (2026-09-15) are the current set'
x-sandbox:
  summary: 'A sandbox token (mint by adding "sandbox": true to POST /api/agent/signup/start) is a real token with real scopes whose WRITE operations return a simulated receipt instead of mutating; reads serve real data. Same base URL + bearer auth. Immutable after mint.'
  obtain:
    endpoint: POST /api/agent/signup/start
    body:
      sandbox: true
  write_response_shape:
    ok: true
    sandbox: true
    simulated: true
    would_have:
      action: string
      scope: string
      args: object
    note: string — scope was checked; deeper business rules (tier/role/ownership/signature) are NOT evaluated in sandbox, and no production state changed. A green receipt means well-formed + scoped, NOT guaranteed to pass in production.
  real_exceptions:
  - POST /api/agent/feedback (bug-report channel must be heard)
  - POST /api/agent/token/revoke (token lifecycle; genuinely revokes)
  not_sandbox_eligible:
  - ic_donate (x402 payment rail — never faked; not a REST operation in this spec)
  - floor-admin toggles + tools with no concrete scope to check — denied fail-closed, never silently simulated