Immersive Commons Startups API

Startup pages: discover a slug, claim it, then run it.

GitHub OpenAPI

Operations 8

GET /api/startups Discover claimable startup slugs #
GET /api/startups/{slug}/profile Read the full owner view of a startup profile #
POST /api/startups/{slug}/profile Write a startup's public page #
POST /api/startups/{slug}/news Append one news item to a startup profile #
POST /api/startups/{slug}/claim Ask to be bound as a startup's founder #
GET /api/startups/{slug}/claim Poll the caller's own ownership claim #

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-startups-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-startups-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Immersive Commons — Agent REST Startups 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: startups
  description: 'Startup pages: discover a slug, claim it, then run it.'
paths:
  /api/startups:
    get:
      operationId: listStartups
      tags:
      - startups
      summary: Discover claimable startup slugs
      description: Clerk session or agt_ token with `startup:edit` (live tier re-checked) - the SAME gate the claim route uses, so anyone who can file a claim can discover slugs and listing never exposes more than the claim flow already assumes. `bound:false` means unbound and therefore claimable; `bound:true` means the slug already has a founder and a claim would be a no-op an operator can never approve. This closes the dead end where the claim required a slug that nothing agent-reachable could enumerate.
      security:
      - clerkSession: []
      - agentBearer:
        - startup:edit
      responses:
        '200':
          description: Every seeded startup, name-sorted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartupsListResponse'
        '401':
          description: No auth.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Scope/tier not authorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/startups/route.ts
        method: GET
  /api/startups/{slug}/profile:
    get:
      operationId: getStartupProfile
      tags:
      - startups
      summary: Read the full owner view of a startup profile
      description: 'Owner or operator. Two-layer gate: the capability scope `startup:edit` with a live tier re-check, then slug ownership (an operator carrying `admin:ownership_review` may read any slug). This is the FULL record - founder identity and every news item - not the public projection the marketing pages render.'
      security:
      - clerkSession: []
      - agentBearer:
        - startup:edit
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The seeded startup slug.
      responses:
        '200':
          description: The full profile record.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartupProfileResponse'
        '400':
          description: Missing slug.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: No auth.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Not the bound owner and not an operator.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such startup.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/startups/[slug]/profile/route.ts
        method: GET
    post:
      operationId: updateStartupProfile
      tags:
      - startups
      summary: Write a startup's public page
      description: 'Owner or operator, same two-layer gate as the read. Content auto-publishes with no per-post review, which is why the AGENT path additionally requires a SIGNATURE-ENFORCED token: a leaked bearer alone must be useless against a startup''s public page. The Clerk session path is not subject to that (the human is the identity). Text fields are plain text defanged server-side; the repo ships no HTML sanitizer and this route accepts no HTML.'
      security:
      - clerkSession: []
      - agentBearer:
        - startup:edit
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The seeded startup slug.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartupProfileUpdateRequest'
      responses:
        '200':
          description: The saved profile.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartupProfileResponse'
        '400':
          description: Missing slug, invalid JSON, or a field that fails validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: No auth.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Not the bound owner, not an operator, or a bearer token without signature enforcement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such startup.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/startups/[slug]/profile/route.ts
        method: POST
  /api/startups/{slug}/news:
    post:
      operationId: postStartupNews
      tags:
      - startups
      summary: Append one news item to a startup profile
      description: Owner or operator, same gate as the profile write (including the agent signature requirement). STRUCTURED PLAIN TEXT only - no HTML body; the title is defanged and the optional link is URL-validated. Rate limited to 10 posts per FOUNDER per UTC day, keyed on the founder's identity rather than the token so minting more tokens cannot multiply the budget. The item is prepended and the list is capped at 8; when it is already full the OLDEST item is dropped.
      security:
      - clerkSession: []
      - agentBearer:
        - startup:edit
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The seeded startup slug.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartupNewsRequest'
      responses:
        '201':
          description: The stored item plus the caller's remaining daily budget.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartupNewsResponse'
        '400':
          description: Missing slug, invalid JSON, or a title/link that fails validation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: No auth.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Not the bound owner, not an operator, or a bearer token without signature enforcement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such startup.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Daily news budget exhausted for this founder.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/startups/[slug]/news/route.ts
        method: POST
  /api/startups/{slug}/logo:
    post:
      operationId: uploadStartupLogo
      tags:
      - startups
      summary: Upload a startup logo
      description: Owner or operator, same gate as the profile write. The server crops the supplied image to a 256px square WebP (the same pipeline as member photos) and stores the result as the HIGHEST-precedence logo source, above the bespoke seed asset and the favicon fallback. Send either `data_url` or bare `base64`.
      security:
      - clerkSession: []
      - agentBearer:
        - startup:edit
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The seeded startup slug.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartupLogoUploadRequest'
      responses:
        '200':
          description: The saved profile plus the stored payload size.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartupLogoUploadResponse'
        '400':
          description: Missing slug, no image supplied, or bytes that will not decode/crop.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: No auth.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Not the bound owner, not an operator, or a bearer token without signature enforcement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such startup.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: Image over the size ceiling.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/startups/[slug]/logo/route.ts
        method: POST
    delete:
      operationId: deleteStartupLogo
      tags:
      - startups
      summary: Remove an uploaded startup logo
      description: Owner or operator, same gate as the upload. Clears only the uploaded logo, so the public card falls back to the bespoke seed asset or the favicon automatically; the seed path is never touched. `had_logo:false` means there was nothing to clear.
      security:
      - clerkSession: []
      - agentBearer:
        - startup:edit
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The seeded startup slug.
      responses:
        '200':
          description: The profile after the clear.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartupLogoDeleteResponse'
        '400':
          description: Missing slug.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: No auth.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Not the bound owner, not an operator, or a bearer token without signature enforcement.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such startup.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/startups/[slug]/logo/route.ts
        method: DELETE
  /api/startups/{slug}/claim:
    post:
      operationId: requestStartupOwnership
      tags:
      - startups
      summary: Ask to be bound as a startup's founder
      description: 'Clerk session or agt_ token with `startup:edit` (live tier re-checked). This binds NOTHING - it enqueues an ownership request into the operator review queue, and only an operator approval writes the founder onto the profile. Unlike the profile/news writes it does NOT require a signature-enforced token, because the operator approval is the gate and the request itself is not consequential. The body MUST carry `agree_content_responsibility: true`; that acceptance and its timestamp are recorded in the audit ring. Answers 202 with a `Location` header pointing at the status poll.'
      security:
      - clerkSession: []
      - agentBearer:
        - startup:edit
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The seeded startup slug being claimed.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StartupClaimRequest'
      responses:
        '202':
          description: Queued for operator review. Poll the same path with GET.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartupClaimQueuedResponse'
        '400':
          description: Missing slug, or `agree_content_responsibility` not true.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: No auth.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Scope/tier not authorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such startup.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The slug already has a bound founder, or the caller already has a live request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/startups/[slug]/claim/route.ts
        method: POST
    get:
      operationId: getStartupClaimStatus
      tags:
      - startups
      summary: Poll the caller's own ownership claim
      description: 'Same capability gate as the POST. Grounded in real state rather than a fabricated job store: `approved` means the caller is now in the slug''s bound owner set, `pending` means they still have a live row in the operator queue, and `gone` means neither - rejected, expired, or never filed. Only the caller''s own claim is ever returned.'
      security:
      - clerkSession: []
      - agentBearer:
        - startup:edit
      parameters:
      - name: slug
        in: path
        required: true
        schema:
          type: string
        description: The seeded startup slug.
      responses:
        '200':
          description: The caller's claim status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StartupClaimStatusResponse'
        '400':
          description: Missing slug.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: No auth.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: Scope/tier not authorized.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No such startup.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
      x-ic-route:
        file: app/api/startups/[slug]/claim/route.ts
        method: GET
components:
  schemas:
    StartupClaimStatusResponse:
      type: object
      required:
      - ok
      - startup_slug
      - status
      properties:
        ok:
          type: boolean
        startup_slug:
          type: string
        startup_name:
          type: string
        status:
          type: string
          enum:
          - pending
          - approved
          - gone
          description: '`gone` covers rejected, expired, and never-filed — they are one state from the caller''s side.'
        owner:
          type: boolean
          description: true once the caller is in the slug's bound owner set.
        request_id:
          type: string
          description: Present while pending.
        job_id:
          type: string
          description: Present while pending.
        submitted_at:
          type: string
          format: date-time
          description: Present while pending.
    StartupsListResponse:
      type: object
      required:
      - ok
      - count
      - startups
      properties:
        ok:
          type: boolean
        count:
          type: integer
        startups:
          type: array
          items:
            type: object
            required:
            - slug
            - name
            - bound
            properties:
              slug:
                type: string
              name:
                type: string
              tagline:
                type: string
              bound:
                type: boolean
                description: true means the slug already has a founder — a claim would be a no-op no operator can approve.
        via:
          type: string
          enum:
          - clerk
          - agent
    StartupProfileRecord:
      type: object
      additionalProperties: true
      description: The full owner/operator view of a startup profile, including founder identity and every news item.
      properties:
        slug:
          type: string
        name:
          type: string
        tagline:
          type: string
        description:
          type: string
        website:
          type: string
        logo_data_url:
          type: string
          description: The uploaded logo. Highest-precedence logo source.
        news:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Newest first, capped at 8.
        updated_at:
          type: string
          format: date-time
    StartupLogoUploadRequest:
      type: object
      description: Send EITHER `data_url` or bare `base64`.
      properties:
        data_url:
          type: string
          description: '`data:image/png;base64,...`'
        base64:
          type: string
          description: Bare base64 bytes.
    StartupClaimRequest:
      type: object
      required:
      - agree_content_responsibility
      properties:
        agree_content_responsibility:
          type: boolean
          enum:
          - true
          description: MUST be true. News auto-publishes with no per-post review, so this acceptance and its timestamp are recorded in the audit ring.
        note:
          type: string
          description: Free text for the reviewing operator.
    StartupNewsRequest:
      type: object
      required:
      - title
      properties:
        title:
          type: string
          description: Plain text, defanged server-side.
        link:
          type: string
          description: Optional. URL-validated.
        date:
          type: string
          description: Optional display date.
    StartupLogoDeleteResponse:
      type: object
      required:
      - ok
      - had_logo
      - profile
      properties:
        ok:
          type: boolean
        had_logo:
          type: boolean
          description: false means there was nothing to clear.
        profile:
          $ref: '#/components/schemas/StartupProfileRecord'
        via:
          type: string
          enum:
          - clerk
          - agent
    StartupProfileResponse:
      type: object
      required:
      - ok
      - profile
      properties:
        ok:
          type: boolean
        profile:
          $ref: '#/components/schemas/StartupProfileRecord'
        via:
          type: string
          enum:
          - clerk
          - agent
    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).
    StartupLogoUploadResponse:
      type: object
      required:
      - ok
      - profile
      properties:
        ok:
          type: boolean
        base64_len:
          type: integer
          description: Size of the stored 256px WebP payload.
        profile:
          $ref: '#/components/schemas/StartupProfileRecord'
        via:
          type: string
          enum:
          - clerk
          - agent
    StartupNewsResponse:
      type: object
      required:
      - ok
      - item
      - news_count
      properties:
        ok:
          type: boolean
        item:
          type: object
          additionalProperties: true
          description: The stored item.
        news_count:
          type: integer
          description: Items on the profile after the write — capped at 8, oldest dropped.
        rate:
          $ref: '#/components/schemas/RateInfo'
        via:
          type: string
          enum:
          - clerk
          - agent
    StartupProfileUpdateRequest:
      type: object
      additionalProperties: true
      description: Read-modify-write of the writable fields. STRUCTURED PLAIN TEXT only — there is no HTML sanitizer in the repo and no field accepts HTML.
      properties:
        name:
          type: string
        tagline:
          type: string
        description:
          type: string
        website:
          type: string
          description: Validated as a URL.
    StartupClaimQueuedResponse:
      type: object
      required:
      - ok
      - request_id
      - status
      - poll_url
      properties:
        ok:
          type: boolean
        request_id:
          type: string
        job_id:
          type: string
          description: Mirrors `request_id` — the handle to poll with.
        status:
          type: string
          enum:
          - pending
        poll_url:
          type: string
          description: Also sent as the `Location` header.
        startup_slug:
          type: string
        startup_name:
          type: string
        via:
          type: string
          enum:
          - clerk
          - agent
        message:
          type: string
    RateInfo:
      type: object
      description: Per-token rate-limit snapshot returned by write endpoints.
      properties:
        current:
          type: integer
        remaining:
          type: integer
        limit:
          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