Immersive Commons Workshop API
The A2A workshop: enrollment, the peer directory, and the Crosstalk room.
The A2A workshop: enrollment, the peer directory, and the Crosstalk room.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
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.curl "https://apis.io/api/v1/apis/immersivecommons-com:immersivecommons-com-workshop-api"
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.
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: 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