{
  "openapi": "3.1.0",
  "info": {
    "title": "Witbitz Platform API",
    "version": "1.0.0-beta",
    "x-availability": "private-beta",
    "x-access": "invite-only",
    "description": "The production surface — only operations callable at https://api.witbitz.chat/v1 today. The full design spec (legacy + proposed) is at `/v1/openapi.full.json`.\n\nEvery success response is enveloped as `{ \"data\": <payload>, \"meta\"?: {...} }`; errors as `{ \"error\": { \"code\", \"message\", \"request_id\"? } }` (see `components.schemas.Error`). The response schemas below are the ENVELOPED shapes.\n\n## Authentication & capabilities\nTwo tenant API keys, resolved by the /v1 authorizer to a `tenantId`: a **secret** key (`wsk_`, full scope, server-side only) and a **publishable** key (`wpk_`, browser-safe, public reads + funnel-entry writes). A publishable key IDENTIFIES a tenant — it does NOT authenticate an end user. Owner actions on a collection also need the per-collection **capability token** (`x-collection-token`): the owner `admin_token` or the add-only `contributor_token`. Wallet/license credentials travel in the request BODY, never a URL.\n\n## Capability-token lifecycle\n`admin_token` / `contributor_token` / `wallet_code` / `whsec_` / raw API keys are shown ONCE at creation and stored only as a hash (or KMS-sealed, for the recoverable webhook secret). Store them SERVER-SIDE — a collection `admin_token` is the full owner capability and must NOT be embedded in a browser bundle. Lost = re-create (a new collection / key); there is no retrieval.\n\n## Idempotency\nMoney/billable ops (`POST /sessions`, `/wallets/grant`, `/collections/{id}/paint`) REQUIRE an `Idempotency-Key`. The fingerprint is SHA-256 of method + path + the RAW body bytes. Same key + same request → the first 2xx is replayed byte-for-byte; same key + different request → `409 idempotency_key_reused`; a concurrent duplicate → `409 idempotency_request_in_progress` (carries `Retry-After`). Every response carries `Idempotency-Status: created | replayed | in-progress`. Non-2xx is NOT stored (a corrected retry proceeds); the record is reaped after 24h.\n\n## Rate limiting & kill switch\n`/v1` is throttled platform-wide at the gateway (429 + `Retry-After`). PER-TENANT + per-key-type request limits are enforced at the authorizer (a flood → 403 at the door, before any handler; publishable keys get a tighter cap). An operator can SUSPEND a tenant (`POST /tenants/{tenant_id}/status`) → every key is denied at the authorizer within ~15s. Per-tenant SPEND is separately capped (e.g. managed paint).\n\n## Secret handling\nResponses carrying a one-time secret (signup, API-key creation, wallet grant/redeem, webhook-secret rotation, session launch, TURN credentials) are sent `Cache-Control: no-store`. Sensitive fields are `format: password` + `x-sensitive`. BYOK provider keys + the webhook signing secret are sealed to KMS at rest.\n\n## Public-content threat model\nCORS is NOT an authorization boundary — a publishable key works from any client. Treat all user-supplied content (item content, collection metadata) as untrusted and size-bounded (413 over the byte cap). The keyless render page (`GET /collections/{id}/render`) is a SHARE link, not access control: the unguessable id lets anyone VIEW published content; it exposes only published artifacts + public metadata, never items.\n\n## Beta compatibility policy\nPRIVATE BETA — the contract may change, including BREAKING changes, during beta (coordinated with invited integrators). The strict path-level guarantee (breaking changes only under a new prefix) takes effect at GA, at which point `/v1` freezes. `info.version` is the spec revision (semver); CI lints (Redocly) + flags breaking diffs (oasdiff), so every change is explicit.\n\n## Operational limitations (NOT yet enforced — do not rely on them)\nPer-IP / abuse-cluster throttling; CAPTCHA / proof-of-work on funnel endpoints (`POST /collections`, `POST /wallets/redeem`, `POST /checkout`, `GET /turn`); endpoint-specific TURN/redeem/checkout mint limits. The `/usage` feed is a bounded scan (`meta.truncated` signals an incomplete window — narrow with `from`/`to`); it is not cursor-paginated.\n\n## Recipes\n1. **Create a collection + retain the owner token** — `POST /collections` (publishable key) → `{ id, admin_token }`. Store `admin_token` SERVER-SIDE (it is shown once). Pass it as `x-collection-token` for owner ops.\n2. **Add an item to an OPEN collection** — `POST /collections/{id}/items` with just the publishable key (open policy auto-approves).\n3. **Add an item to a TOKEN-protected collection** — same, but ALSO send `x-collection-token: <contributor_token>` (or the owner token). Missing it → `403 contributor_token_required`.\n4. **Generate + poll an image** — `POST /collections/{id}/paint` (Idempotency-Key required) → `201 { id, status: painting }` with `Location` + `Retry-After`. Poll `GET /collections/{id}/artifacts/{artifactId}` until `ready` (+ `url`) | `failed` (+ `failure_code`) | `quota_exceeded`. If a webhook is set, `artifact.ready` is also delivered.\n5. **Launch a session with an idempotent retry** — `POST /sessions` with `Idempotency-Key` + `{ agent_id, room, wallet }`. A retry with the SAME key + body replays the first result (no double-charge). `arm=true` provisions without charging (returns `SessionArmed` + `summon_key`).\n6. **Verify a webhook (raw bytes)** — read `Witbitz-Signature: t=...,v1=...`; compute `HMAC-SHA256(secret, t + \".\" + rawBody)` over the RAW body BEFORE JSON-parsing; constant-time compare the lowercase hex; reject if `|now - t| > 300s`; dedupe on the event `id`. See the webhook operation for a test vector.\n7. **Rotate a webhook secret** — `PUT /tenant/webhooks { url }` re-mints the `whsec_` secret (returned once) and INVALIDATES the previous one (no overlap window today — rotate during a maintenance pause).\n8. **Recover from 429 / in-progress 409** — on `429` honor `Retry-After` and back off. On `409 idempotency_request_in_progress` (a concurrent duplicate), wait the `Retry-After` and retry the SAME key; on `409 idempotency_key_reused` you sent a different body for a used key — use a fresh key.",
    "contact": {
      "name": "witbitz",
      "url": "https://witbitz.chat",
      "email": "hello@witbitz.chat"
    },
    "license": {
      "name": "Proprietary — © Witbitz",
      "url": "https://witbitz.chat/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.witbitz.chat/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "tenantSecretKey": []
    }
  ],
  "tags": [
    {
      "name": "Agents",
      "description": "Define the AI participant (manifest registry)"
    },
    {
      "name": "Collections",
      "description": "Gather content around a room (generalized \"gift\")"
    },
    {
      "name": "Sessions",
      "description": "Run an agent in a room"
    },
    {
      "name": "Artifacts",
      "description": "What the agent produced (keepsakes)"
    },
    {
      "name": "Credits",
      "description": "Fund + bill your users"
    },
    {
      "name": "Tenants",
      "description": "Developer identity + API keys + BYOK"
    },
    {
      "name": "Infra",
      "description": "Machine/utility endpoints"
    }
  ],
  "paths": {
    "/catalog": {
      "get": {
        "tags": [
          "Agents"
        ],
        "summary": "Public marketplace catalog (active agents)",
        "description": "The public catalog of active agents.",
        "security": [
          {
            "tenantPublishableKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "responses": {
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/CatalogAgent"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "getCatalog",
        "x-status": "live"
      }
    },
    "/collections": {
      "post": {
        "tags": [
          "Collections"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "summary": "Create a collection",
        "description": "A tenant-owned collection: opaque `metadata` (your app's own config), typed items, curation, a forward-only state, and a hashed owner capability. \"A birthday gift\" is just metadata.kind='gift' + item.type='message' — the platform stays app-agnostic. Returns the collection id + a one-time admin_token (the owner capability), and — when contributor_policy=token — a one-time contributor_token (add-only, safe to hand to contributors). Both are shown once.",
        "security": [
          {
            "tenantPublishableKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CollectionCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "headers": {
              "Idempotency-Status": {
                "$ref": "#/components/headers/IdempotencyStatus"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CollectionCreated"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "createCollection",
        "x-status": "live"
      }
    },
    "/collections/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        }
      ],
      "get": {
        "tags": [
          "Collections"
        ],
        "summary": "Publishable-key view (metadata + state + count; never items)",
        "description": "Requires a tenant publishable key (NOT unauthenticated — that is /collections/{id}/render). Returns metadata, state, and item_count only — never the items themselves.",
        "security": [
          {
            "tenantPublishableKey": []
          }
        ],
        "responses": {
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CollectionPublic"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "getCollection",
        "x-status": "live"
      },
      "delete": {
        "tags": [
          "Collections"
        ],
        "summary": "Delete the collection (permanent)",
        "description": "Permanently deletes the collection, its items, and its artifacts. Owner capability required.",
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "deleteCollection",
        "x-status": "live"
      }
    },
    "/collections/{id}/render": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        }
      ],
      "get": {
        "tags": [
          "Collections"
        ],
        "summary": "Public shareable page (HTML, no key)",
        "description": "Server-rendered, OpenGraph-tagged HTML page for social link previews: the published hero artifact + the app's share copy/theme from metadata (share_title, share_description, theme). PUBLIC — no API key. This is a deliberately shareable page: knowing the (high-entropy, unguessable) collection id is enough to VIEW the published content, so treat the URL as a share link, not a secret. It exposes only published artifacts + the public metadata — never items or unpublished artifacts. There is no per-view revocation today; a distinct revocable share token is a planned option.",
        "security": [],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        },
        "operationId": "renderCollectionPage",
        "x-status": "live"
      }
    },
    "/collections/{id}/items": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        }
      ],
      "get": {
        "tags": [
          "Collections"
        ],
        "summary": "Owner list (all items incl. unapproved + archived)",
        "description": "The item list with curation flags — a PAGE. Page with limit + cursor, follow meta.next_cursor. Owner capability required.",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "responses": {
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/CollectionOwner"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "listCollectionItems",
        "x-status": "live"
      },
      "post": {
        "tags": [
          "Collections"
        ],
        "summary": "Contribute an item",
        "description": "Adds a typed item while the collection is `open`, auto-approved. CONDITIONAL auth (OpenAPI can't express a requirement that depends on the resource's policy, so the alternatives below are the SUPERSET): if contributor_policy=open, the tenant publishable key alone suffices; if contributor_policy=token, a collection CONTRIBUTOR token (issued at create) OR the owner token is ALSO required (enforced in-handler). The owner token is never required for an open collection.",
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "security": [
          {
            "tenantPublishableKey": []
          },
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "x-conditional-security": {
          "description": "The required credentials depend on the collection's contributor_policy (not expressible in standard OpenAPI security). On a violation the server returns 403 with a stable code.",
          "cases": [
            {
              "when": "contributor_policy == open",
              "requires": [
                "tenantPublishableKey"
              ]
            },
            {
              "when": "contributor_policy == token",
              "requires": [
                "tenantPublishableKey",
                "collectionToken"
              ]
            }
          ],
          "on_violation": {
            "status": 403,
            "code": "contributor_token_required"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CollectionItemCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Added",
            "headers": {
              "Idempotency-Status": {
                "$ref": "#/components/headers/IdempotencyStatus"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "oneOf": [
                        {
                          "type": "object",
                          "required": [
                            "id"
                          ],
                          "additionalProperties": false,
                          "properties": {
                            "id": {
                              "$ref": "#/components/schemas/ItemId"
                            }
                          }
                        },
                        {
                          "type": "object",
                          "required": [
                            "ok"
                          ],
                          "additionalProperties": false,
                          "properties": {
                            "ok": {
                              "const": true
                            }
                          },
                          "description": "sealed-append: the box is appended blind — no item id is returned"
                        }
                      ]
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "403": {
            "$ref": "#/components/responses/ContributorTokenRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "addCollectionItem",
        "x-status": "live"
      }
    },
    "/collections/{id}/items/{itemId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        },
        {
          "$ref": "#/components/parameters/itemId"
        }
      ],
      "patch": {
        "tags": [
          "Collections"
        ],
        "summary": "Curate an item",
        "description": "Set approved / archived / hero (booleans), or use the access model (access / grant / revoke). Owner capability required.",
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CurationPatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "curateCollectionItem",
        "x-status": "live"
      },
      "delete": {
        "tags": [
          "Collections"
        ],
        "summary": "Delete an item",
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "deleteCollectionItem",
        "x-status": "live"
      }
    },
    "/collections/{id}/state": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        }
      ],
      "post": {
        "tags": [
          "Collections"
        ],
        "summary": "Transition state and/or patch app_state",
        "description": "open → ready → closed — forward-only and idempotent (a no-op transition returns 200). `state` is OPTIONAL: omit it to patch only `app_state` (the opaque app-config blob, deep-merged). At least one of the two is required. Owner capability required.",
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "minProperties": 1,
                "additionalProperties": false,
                "properties": {
                  "state": {
                    "type": "string",
                    "enum": [
                      "open",
                      "ready",
                      "closed"
                    ],
                    "description": "OPTIONAL forward-only transition; omit to patch app_state only"
                  },
                  "app_state": {
                    "type": "object",
                    "additionalProperties": true,
                    "description": "opaque app-config blob, deep-merged into the collection's app_state"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "state"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "state": {
                          "type": "string",
                          "enum": [
                            "open",
                            "ready",
                            "closed"
                          ]
                        },
                        "app_state": {
                          "type": "object",
                          "additionalProperties": true,
                          "description": "the merged app_state — present only when the request patched it"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "transitionCollectionState",
        "x-status": "live"
      }
    },
    "/sessions": {
      "post": {
        "tags": [
          "Sessions"
        ],
        "summary": "Launch an agent into a room",
        "description": "Pay-as-you-go: launching bills a coupon + starts a task. An Idempotency-Key is REQUIRED so a retry replays the first session instead of launching + charging twice. arm=true provisions the room WITHOUT launching or charging (the wallet is stashed for the eventual summon). A 200 (vs 201) means the same agent was already present in the room.",
        "security": [
          {
            "tenantSecretKey": []
          }
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKeyRequired"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SessionLaunch"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Started (Session) — or, for arm=true, provisioned (SessionArmed)",
            "headers": {
              "Idempotency-Status": {
                "$ref": "#/components/headers/IdempotencyStatus"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/Session"
                        },
                        {
                          "$ref": "#/components/schemas/SessionArmed"
                        }
                      ]
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "200": {
            "description": "Duplicate (already present)",
            "headers": {
              "Idempotency-Status": {
                "$ref": "#/components/headers/IdempotencyStatus"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/SessionAlreadyPresent"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredit"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "launchSession",
        "x-status": "live"
      }
    },
    "/collections/{id}/artifacts": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        }
      ],
      "get": {
        "tags": [
          "Artifacts"
        ],
        "summary": "List artifacts (owner sees all + status; public sees published only)",
        "description": "Produced outputs attached to the collection — a PAGE (; page with limit + cursor, follow meta.next_cursor). The owner (collection admin token) sees every artifact incl. status; without it, only published artifacts are returned.",
        "parameters": [
          {
            "$ref": "#/components/parameters/limit"
          },
          {
            "$ref": "#/components/parameters/cursor"
          }
        ],
        "security": [
          {
            "tenantPublishableKey": []
          },
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "responses": {
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Artifact"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "listArtifacts",
        "x-status": "live"
      },
      "post": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Attach an artifact by URL",
        "description": "Records a produced output (image/clip/card) that already has a hosted URL. Owner capability required. To GENERATE an image instead, use POST /collections/{id}/paint.",
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKey"
          }
        ],
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ArtifactCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "headers": {
              "Idempotency-Status": {
                "$ref": "#/components/headers/IdempotencyStatus"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "id": {
                          "$ref": "#/components/schemas/ArtifactId"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "addArtifact",
        "x-status": "live"
      }
    },
    "/collections/{id}/artifacts/{artifactId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        },
        {
          "$ref": "#/components/parameters/artifactId"
        }
      ],
      "get": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Read one artifact (poll a paint here)",
        "description": "The efficient way to poll a paint result — fetch just this artifact, not the whole list. The owner sees any artifact incl. status (painting/ready/failed/quota_exceeded); a non-owner sees it only once published.",
        "security": [
          {
            "tenantPublishableKey": []
          },
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "responses": {
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Artifact"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "getArtifact",
        "x-status": "live"
      },
      "patch": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Publish / set hero / curate",
        "description": "Set published / hero (booleans), or use the access model (access / grant / revoke); also accepts approved/archived. A published hero is what GET /collections/{id}/render shows. Owner capability required.",
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CurationPatch"
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "updateArtifact",
        "x-status": "live"
      },
      "delete": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Delete an artifact",
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "deleteArtifact",
        "x-status": "live"
      }
    },
    "/collections/{id}/paint": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        }
      ],
      "post": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Generate an image (async)",
        "description": "Creates an image artifact from a prompt. Uses the tenant's BYOK key if set (PUT /tenant/provider-keys), else the platform's managed key (metered + daily-capped per tenant). Returns 201 immediately with status=painting; the response carries a Location header (the artifact resource) + a Retry-After header (suggested initial poll interval, seconds). POLL GET /collections/{id}/artifacts/{artifactId} until a TERMINAL status: ready (+ url) | failed (+ failure_code/failure_message) | quota_exceeded. Terminal is guaranteed — the async worker always writes one. If a tenant webhook is configured, an artifact.ready event is ALSO delivered on success (durable). RETRY: the Idempotency-Key is REQUIRED so a retried request replays the SAME artifact (never a second billable paint) for 24h. To start a NEW paint after a terminal failure, use a NEW Idempotency-Key.",
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKeyRequired"
          }
        ],
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PaintRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Painting started (poll the Location, or await the artifact.ready webhook)",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri-reference"
                },
                "description": "the artifact resource to poll"
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "suggested initial poll interval (seconds)"
              },
              "Idempotency-Status": {
                "$ref": "#/components/headers/IdempotencyStatus"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "status"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "id": {
                          "$ref": "#/components/schemas/ArtifactId"
                        },
                        "status": {
                          "const": "painting"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "paintArtifact",
        "x-status": "live"
      }
    },
    "/collections/{id}/produce": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        }
      ],
      "post": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Run a multi-step recipe (async)",
        "description": "Runs a multi-step recipe into the collection — the generic successor to /paint (a single paint step) and witz's cardPaint. The recipe (a small backward-only DAG of steps: compose_text | paint_image | overlay_text | render | transform | store_artifact) is inline in the body and MUST contain at least one store_artifact step, which becomes ONE pending artifact the async worker fills. Same custody + billing as /paint (the tenant's BYOK key if set, else the platform's managed key — metered + daily-capped) since a recipe may run paint steps. Returns 201 immediately with status=painting + a Location header (the artifact resource) + a Retry-After header (suggested initial poll interval, seconds); POLL GET /collections/{id}/artifacts/{artifactId} until a TERMINAL status: ready (+ url) | failed (+ failure_code/failure_message) | quota_exceeded. Owner capability required. RETRY: the Idempotency-Key is REQUIRED so a retried request replays the SAME artifact (never a second billable run) for 24h. See docs/collections-recipes.md for the full step catalog.",
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKeyRequired"
          }
        ],
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProduceRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Recipe started (poll the Location, or await the artifact.ready webhook)",
            "headers": {
              "Location": {
                "schema": {
                  "type": "string",
                  "format": "uri-reference"
                },
                "description": "the artifact resource to poll"
              },
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "suggested initial poll interval (seconds)"
              },
              "Idempotency-Status": {
                "$ref": "#/components/headers/IdempotencyStatus"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "id",
                        "status"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "id": {
                          "$ref": "#/components/schemas/ArtifactId"
                        },
                        "status": {
                          "const": "painting"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "produceArtifact",
        "x-status": "live"
      }
    },
    "/collections/{id}/artifacts/upload": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        }
      ],
      "post": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Presign a direct upload (large binaries)",
        "description": "The tenant-produced counterpart to /paint: for a LARGE binary the tenant renders itself (e.g. a video keepsake clip too big to attach by URL), the platform hands back a presigned PUT so the client uploads straight to storage — the bytes never transit the control-plane. Returns 201 with an upload_url + a pending artifact id. FLOW: (1) POST here → upload_url; (2) PUT the binary to upload_url with the exact `Content-Type: <content_type>`; (3) POST /collections/{id}/artifacts/{artifactId}/ready to verify + flip it to ready. The artifact is status=pending until step 3 (poll GET …/artifacts/{artifactId}); treat it as stale after pending_expires_at. Owner capability required.",
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ArtifactUpload"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Upload slot created (PUT the file, then stamp it ready)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/UploadSlot"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "413": {
            "$ref": "#/components/responses/PayloadTooLarge"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "uploadArtifact",
        "x-status": "live"
      }
    },
    "/collections/{id}/artifacts/{artifactId}/ready": {
      "parameters": [
        {
          "$ref": "#/components/parameters/collectionId"
        },
        {
          "$ref": "#/components/parameters/artifactId"
        }
      ],
      "post": {
        "tags": [
          "Artifacts"
        ],
        "summary": "Stamp an uploaded artifact ready",
        "description": "Call after PUTting the binary to the upload_url from POST …/artifacts/upload. The platform verifies the object actually landed in storage, then flips the artifact pending→ready and returns it with its hosted url. 409 if the object isn't there yet (PUT it first); 404 if there's no pending upload artifact by this id. Idempotent — stamping an already-ready artifact returns it unchanged. Owner capability required.",
        "security": [
          {
            "tenantPublishableKey": [],
            "collectionToken": []
          }
        ],
        "responses": {
          "200": {
            "description": "Ready",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Artifact"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "the upload has not landed yet — PUT the file to the upload_url first",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "readyArtifact",
        "x-status": "live"
      }
    },
    "/wallets/lookup": {
      "post": {
        "tags": [
          "Credits"
        ],
        "summary": "Wallet balance + minutes",
        "description": "The credit `code` is a bearer credential, so it travels in the BODY — never the URL, matching the legacy POST /wallet.",
        "security": [
          {
            "tenantSecretKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "code"
                ],
                "additionalProperties": false,
                "properties": {
                  "code": {
                    "type": "string",
                    "format": "password",
                    "writeOnly": true,
                    "x-sensitive": true,
                    "description": "SENSITIVE — the wallet/coupon credential (bearer; body-only)"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Wallet"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "lookupWallet",
        "x-status": "live"
      }
    },
    "/wallets/redeem": {
      "post": {
        "tags": [
          "Credits"
        ],
        "summary": "Redeem an LS license key into a wallet (idempotent per key)",
        "security": [
          {
            "tenantPublishableKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "license_key"
                ],
                "additionalProperties": false,
                "properties": {
                  "license_key": {
                    "type": "string",
                    "format": "password",
                    "writeOnly": true,
                    "x-sensitive": true,
                    "description": "SENSITIVE — the Lemon Squeezy license key (bearer; body-only)"
                  },
                  "room": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Minted — wallet_code shown ONCE",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/WalletRedeem"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "200": {
            "description": "Already redeemed — status only, the wallet_code is NOT returned again",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/WalletRedeemStatus"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "redeemWallet",
        "x-status": "live"
      }
    },
    "/wallets/grant": {
      "post": {
        "tags": [
          "Credits"
        ],
        "summary": "Provision credit to a wallet directly",
        "description": "Lets a tenant fund its own users without reselling LS keys; debits the tenant's own balance. Does NOT change merchant of record (still Lemon Squeezy).",
        "x-status": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "amount_cents"
                ],
                "additionalProperties": false,
                "properties": {
                  "amount_cents": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 100000,
                    "description": "cents to fund (<=100000 per grant)"
                  },
                  "room": {
                    "type": "string",
                    "description": "bind the credit to a single room (a gift); omit for an unbound wallet"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "tenantSecretKey": []
          }
        ],
        "responses": {
          "201": {
            "description": "Granted",
            "headers": {
              "Idempotency-Status": {
                "$ref": "#/components/headers/IdempotencyStatus"
              },
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/WalletGrant"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "402": {
            "$ref": "#/components/responses/InsufficientCredit"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "409": {
            "$ref": "#/components/responses/IdempotencyConflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "parameters": [
          {
            "$ref": "#/components/parameters/idempotencyKeyRequired"
          }
        ],
        "operationId": "grantWallet"
      }
    },
    "/checkout": {
      "post": {
        "tags": [
          "Credits"
        ],
        "summary": "Mint an LS checkout (room baked server-side)",
        "description": "Country + postal are inferred at the Cloudflare edge from the buyer's IP. The optional `country` body field is a CLIENT HINT used only as a fallback — edge geo, when available, takes precedence. The request body is OPTIONAL: an empty body {} is valid (unbound checkout); room/country/name are all optional.",
        "security": [
          {
            "tenantPublishableKey": []
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "room": {
                    "type": "string",
                    "maxLength": 128
                  },
                  "country": {
                    "type": "string",
                    "pattern": "^[A-Z]{2}$",
                    "description": "ISO 3166-1 alpha-2 (client hint; edge geo overrides)"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 120
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "url"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "createCheckout",
        "x-status": "live"
      }
    },
    "/usage": {
      "get": {
        "tags": [
          "Credits"
        ],
        "summary": "Per-tenant usage feed",
        "description": "The per-tenant billing feed (settled sessions + charged tool calls), scoped to the authenticated tenant (isolation), newest first. Bounded (a large scan is capped) — narrow it with from/to. NOT cursor-paginated. from/to are OPTIONAL, INCLUSIVE RFC-3339 date-times; omit both for the full bounded feed. The server does not enforce from<=to (an empty window returns []). meta.total_spent_cents is the total for the RETURNED window.",
        "x-status": "live",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "inclusive lower bound (RFC 3339); omit for no lower bound"
          },
          {
            "name": "to",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "inclusive upper bound (RFC 3339); omit for no upper bound"
          }
        ],
        "security": [
          {
            "tenantSecretKey": []
          }
        ],
        "responses": {
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Usage"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "getUsage"
      }
    },
    "/tenants/signup": {
      "post": {
        "tags": [
          "Tenants"
        ],
        "summary": "Self-serve tenant signup (author coupon)",
        "description": "Self-service signup — requires a valid author coupon in the X-Author-Coupon header. Returns the tenant + its first secret/publishable key pair (shown once). Operator-driven creation uses POST /tenants instead (review Phase 1.1: one credential per operation, no body-dependent authz).",
        "x-status": "live",
        "security": [
          {
            "authorCoupon": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TenantCreated"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "signupTenant"
      }
    },
    "/tenants": {
      "post": {
        "tags": [
          "Tenants"
        ],
        "summary": "Operator provisioning of a tenant",
        "description": "OPERATOR-only tenant provisioning — requires an admin token in the X-Admin-Token header. Returns the tenant + its first key pair (shown once). Self-serve signup uses POST /tenants/signup (review Phase 1.1).",
        "x-status": "live",
        "security": [
          {
            "adminToken": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TenantCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/TenantCreated"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "provisionTenant"
      }
    },
    "/tenants/{tenant_id}/status": {
      "post": {
        "tags": [
          "Tenants"
        ],
        "summary": "Operator kill switch — suspend / reactivate a tenant",
        "description": "OPERATOR-only (X-Admin-Token). Sets a tenant's status; a suspended tenant is DENIED at the authorizer (every key → 401) within ~15s. The emergency control for a compromised or abusive tenant. Reactivate with status=active.",
        "x-status": "live",
        "security": [
          {
            "adminToken": []
          }
        ],
        "parameters": [
          {
            "name": "tenant_id",
            "in": "path",
            "required": true,
            "schema": {
              "$ref": "#/components/schemas/TenantId"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "additionalProperties": false,
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "active",
                      "suspended"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "tenant"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "tenant": {
                          "$ref": "#/components/schemas/Tenant"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "setTenantStatus"
      }
    },
    "/tenant": {
      "get": {
        "tags": [
          "Tenants"
        ],
        "summary": "Read your tenant",
        "x-status": "live",
        "security": [
          {
            "tenantSecretKey": []
          }
        ],
        "responses": {
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Tenant"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "getTenant"
      }
    },
    "/tenant/keys": {
      "get": {
        "tags": [
          "Tenants"
        ],
        "summary": "List your API keys (masked)",
        "x-status": "live",
        "security": [
          {
            "tenantSecretKey": []
          }
        ],
        "responses": {
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "keys"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "keys": {
                          "type": "array",
                          "items": {
                            "$ref": "#/components/schemas/ApiKeyMasked"
                          }
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "listApiKeys"
      },
      "post": {
        "tags": [
          "Tenants"
        ],
        "summary": "Mint an API key",
        "description": "Mints a tenant API key. The body is OPTIONAL: an empty body {} mints a SECRET key with no label (type defaults to secret).",
        "x-status": "live",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "type": {
                    "type": "string",
                    "enum": [
                      "publishable",
                      "secret"
                    ],
                    "default": "secret",
                    "description": "key type; defaults to secret when omitted"
                  },
                  "label": {
                    "type": "string",
                    "maxLength": 80
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "tenantSecretKey": []
          }
        ],
        "responses": {
          "201": {
            "description": "Created (raw key shown once)",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ApiKeyCreated"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "createApiKey"
      }
    },
    "/tenant/keys/{keyId}": {
      "parameters": [
        {
          "name": "keyId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "delete": {
        "tags": [
          "Tenants"
        ],
        "summary": "Revoke an API key",
        "x-status": "live",
        "security": [
          {
            "tenantSecretKey": []
          }
        ],
        "responses": {
          "204": {
            "description": "Revoked"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "revokeApiKey"
      }
    },
    "/tenant/provider-keys": {
      "put": {
        "tags": [
          "Tenants"
        ],
        "summary": "Set BYOK provider keys (write-only)",
        "description": "Bring-your-own-key: the tenant's provider keys are sealed (KMS) at rest and used for paint/agent runs on the tenant's own account (uncapped). Write-only — the raw keys are never returned; the response lists which providers are set. PUT REPLACES the full set : send every provider key you want to keep — a provider omitted from the body is CLEARED.",
        "x-status": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "minProperties": 1,
                "description": "CLOSED : an UNKNOWN provider name is rejected (400), never silently dropped. PUT replaces the whole set.",
                "properties": {
                  "OPENAI": {
                    "type": "string",
                    "format": "password",
                    "writeOnly": true,
                    "x-sensitive": true,
                    "minLength": 16,
                    "maxLength": 512
                  },
                  "ELEVENLABS": {
                    "type": "string",
                    "format": "password",
                    "writeOnly": true,
                    "x-sensitive": true,
                    "minLength": 16,
                    "maxLength": 512
                  },
                  "GEMINI": {
                    "type": "string",
                    "format": "password",
                    "writeOnly": true,
                    "x-sensitive": true,
                    "minLength": 16,
                    "maxLength": 512
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "tenantSecretKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "required": [
                        "providers"
                      ],
                      "additionalProperties": false,
                      "properties": {
                        "providers": {
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "OPENAI",
                              "ELEVENLABS",
                              "GEMINI"
                            ]
                          }
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "setProviderKeys"
      }
    },
    "/tenant/webhooks": {
      "put": {
        "tags": [
          "Tenants"
        ],
        "summary": "Set webhook endpoint + secret",
        "description": "Sets the delivery URL (public https only) + allowed origins. When a url is set, a signing secret is MINTED and returned ONCE (SEALED at rest — recoverable so Witbitz can sign, never hashed) — use it to verify the Witbitz-Signature. PARTIAL merge : an omitted field is PRESERVED. Clearing : url:null DISABLES the webhook (removes the url + secret); url:string sets it.",
        "x-status": "live",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "minProperties": 1,
                "description": "at least one of url/origins (an empty body is rejected)",
                "properties": {
                  "url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uri",
                    "pattern": "^https://",
                    "description": "public https delivery endpoint (https-only; runtime rejects private/loopback/metadata hosts). null CLEARS/disables the webhook; omit to leave unchanged. Setting a url mints a NEW signing secret (rotation) — the previous secret stops being valid."
                  },
                  "origins": {
                    "type": "array",
                    "maxItems": 50,
                    "items": {
                      "type": "string",
                      "pattern": "^https?://[^/]+$",
                      "description": "exact origin: scheme://host[:port], no path/trailing slash, no wildcard"
                    },
                    "description": "allowed CORS origins (exact-match; CORS is NOT an authorization boundary)"
                  }
                }
              }
            }
          }
        },
        "security": [
          {
            "tenantSecretKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/WebhookConfig"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "setWebhook"
      }
    },
    "/turn": {
      "get": {
        "tags": [
          "Infra"
        ],
        "summary": "Ephemeral TURN/ICE credentials",
        "description": "Short-lived ICE/TURN servers for the browser's RTCPeerConnection. Credentials expire — fetch fresh before a call; unconfigured/unreachable upstream → iceServers:null (fall back to STUN).",
        "security": [
          {
            "tenantPublishableKey": []
          }
        ],
        "responses": {
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "200": {
            "description": "OK",
            "headers": {
              "Cache-Control": {
                "$ref": "#/components/headers/NoStore"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "data"
                  ],
                  "properties": {
                    "data": {
                      "type": "object",
                      "additionalProperties": false,
                      "properties": {
                        "iceServers": {
                          "type": [
                            "array",
                            "null"
                          ],
                          "items": {
                            "$ref": "#/components/schemas/IceServer"
                          },
                          "description": "null when TURN is unconfigured/unreachable → use STUN"
                        },
                        "tier": {
                          "type": "string"
                        },
                        "configured": {
                          "type": "boolean"
                        }
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/ResponseMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "operationId": "getTurnCredentials",
        "x-status": "live"
      }
    }
  },
  "webhooks": {
    "event": {
      "post": {
        "operationId": "webhookEvent",
        "summary": "Signed event delivered to the tenant's webhook_url (session/collection/artifact/rating events live; credit.* proposed)",
        "description": "Witbitz POSTs a signed JSON event to the URL set via PUT /tenant/webhooks. DELIVERY STATUS — LIVE events: `session.started` + `session.ended` (a tenant session launched / stopped or expired; data.reason is `stopped`|`expired`), `collection.item_added` (a contributor added an item), `artifact.ready` (an async paint finished), and `rating.created` (a tenant session was rated). DURABLE delivery: each event is signed and POSTed with bounded exponential-backoff retries; if the endpoint keeps failing it is retried again over the following minutes and then dead-lettered (never silently dropped). A delivery failure never affects the originating operation. Still PROPOSED: `credit.low` / `credit.exhausted` (a balance-threshold signal — pending the tenant-metering model) and `tool.charged` (no per-tool tenant charge in today's coupon-metered billing). The signing/verification contract below is final and identical for every event type.\nSIGNATURE: header `Witbitz-Signature: t={unix_seconds},v1={hex}` (brace placeholders — no angle brackets, which HTML/markdown renderers eat). The signed payload is ASCII(t) + \".\" + the RAW request body BYTES (verify BEFORE any JSON parse): `signedPayload = t + \".\" + rawBody`. v1 = lowercase-hex(HMAC-SHA256(key = the webhook_secret as its LITERAL utf-8 string (the `whsec_…` value; NOT base64-decoded), message = signedPayload ) ). Exactly ONE v1 is sent (no multi-signature during rotation — re-setting the url mints a new secret and invalidates the old). Verify in CONSTANT time; REJECT if the header is missing/duplicated/malformed, or if |now - t| > 300s (replay defence). Dedupe on the event `id`. Return 2xx to ack (200 recommended); non-2xx or timeout is retried with exponential backoff, then dead-lettered. TEST VECTOR — secret `whsec_test_5f3a`, t `1700000000`, body `{\"id\":\"evt_demo\",\"type\":\"artifact.ready\"}` → signedPayload `1700000000.{\"id\":\"evt_demo\",\"type\":\"artifact.ready\"}` → v1 `1c8158c5099a7667c5dc011df039639a4115556c9463662ea78eeeecc66f2df7`.",
        "parameters": [
          {
            "name": "Witbitz-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "t={unix},v1={hmac-sha256 hex} (brace placeholders — literal header has real values)"
          },
          {
            "name": "Witbitz-Event",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "the event type (mirrors body.type) — for cheap routing before parsing"
          },
          {
            "name": "Witbitz-Event-Id",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "the event id (mirrors body.id) — dedupe on this; deliveries may repeat"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acknowledged (any 2xx)"
          },
          "400": {
            "description": "Rejected — the receiver couldn't verify/parse (Witbitz will retry)"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "tenantSecretKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Witbitz secret key (wsk_)",
        "description": "LIVE: server-side tenant key wsk_… (full tenant scope). Resolved by the /v1 Lambda authorizer → tenantId."
      },
      "tenantPublishableKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "Witbitz publishable key (wpk_)",
        "description": "LIVE: browser-safe tenant key wpk_… (public reads + funnel-entry writes; owner ops still need the collection token). IDENTIFIES a tenant; does NOT authenticate an end user."
      },
      "collectionToken": {
        "type": "apiKey",
        "in": "header",
        "name": "x-collection-token",
        "description": "The per-collection capability (owner admin_token OR add-only contributor_token). ONE header; the server resolves the ROLE and enforces it per operation (owner-only ops require the admin_token). Header only (never a URL query). 401 missing/invalid · 403 valid-but-wrong-role · 404 hidden/absent."
      },
      "adminToken": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Admin-Token",
        "description": "Operator only."
      },
      "authorCoupon": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Author-Coupon",
        "description": "Self-service signup credential — an author coupon, sent as the X-Author-Coupon header on POST /tenants/signup."
      }
    },
    "parameters": {
      "collectionId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "the collection id (col_…) returned by POST /collections",
        "schema": {
          "$ref": "#/components/schemas/CollectionId"
        }
      },
      "itemId": {
        "name": "itemId",
        "in": "path",
        "required": true,
        "description": "an item id",
        "schema": {
          "$ref": "#/components/schemas/ItemId"
        }
      },
      "artifactId": {
        "name": "artifactId",
        "in": "path",
        "required": true,
        "description": "an artifact id",
        "schema": {
          "$ref": "#/components/schemas/ArtifactId"
        }
      },
      "cursor": {
        "name": "cursor",
        "in": "query",
        "schema": {
          "type": "string"
        },
        "description": "opaque page cursor from a prior response's meta.next_cursor. Bound to this query (endpoint + tenant + filters); not reusable elsewhere. Omit for the first page."
      },
      "limit": {
        "name": "limit",
        "in": "query",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 25,
          "maximum": 100
        }
      },
      "idempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": false,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "Optional. A retried write with the same key REPLAYS the first stored 2xx (byte-for-byte, same status). Per-tenant scope; 24h TTL. Responses carry an Idempotency-Status header (created | replayed). Use a high-entropy key (e.g. a UUID)."
      },
      "idempotencyKeyRequired": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "maxLength": 255
        },
        "description": "REQUIRED on money/billable ops — no key → 400 idempotency_key_required. Fingerprint = SHA-256 of method + path + the RAW request body bytes (send byte-identical retries; the platform does not re-canonicalize your JSON). SAME key + SAME request → replays the first stored 2xx byte-for-byte (same status code); SAME key + DIFFERENT request → 409 idempotency_key_reused; a still-in-flight duplicate → 409 idempotency_request_in_progress. Every response carries an Idempotency-Status header: created | replayed | in-progress. NON-2xx results are NOT stored — the claim is released so a corrected retry proceeds. Per-tenant scope; after the 24h TTL the record is reaped, so a later reuse is treated as a fresh request."
      }
    },
    "headers": {
      "IdempotencyStatus": {
        "description": "Present on responses to an idempotent (Idempotency-Key) request: created = first execution stored · replayed = a prior stored 2xx returned byte-for-byte · in-progress = a concurrent duplicate is still running (on the 409).",
        "schema": {
          "type": "string",
          "enum": [
            "created",
            "replayed",
            "in-progress"
          ]
        }
      },
      "NoStore": {
        "description": "Cache-Control: no-store — this response carries a one-time credential and must not be cached/stored.",
        "schema": {
          "type": "string",
          "const": "no-store"
        }
      },
      "RetryAfter": {
        "description": "Seconds to wait before retrying (on 409 idempotency_request_in_progress and 429).",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "responses": {
      "Ok": {
        "description": "OK",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "required": [
                "data"
              ],
              "properties": {
                "data": {
                  "type": "object",
                  "required": [
                    "ok"
                  ],
                  "additionalProperties": false,
                  "properties": {
                    "ok": {
                      "const": true
                    }
                  }
                },
                "meta": {
                  "$ref": "#/components/schemas/ResponseMeta"
                }
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Forbidden — a valid credential of the wrong role (e.g. a contributor token on an owner-only op)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "forbidden",
                "message": "owner capability required"
              }
            }
          }
        }
      },
      "ContributorTokenRequired": {
        "description": "Forbidden — this collection's contributor_policy=token, so a contributor (or owner) token is required; a publishable key alone is not enough",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "contributor_token_required",
                "message": "a contributor or owner capability is required for this collection"
              }
            }
          }
        }
      },
      "InsufficientCredit": {
        "description": "Insufficient credit",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "BadRequest": {
        "description": "Invalid request (400)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid credentials (401)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "Conflict — concurrent change (409)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "IdempotencyConflict": {
        "description": "Idempotency conflict (409): idempotency_key_reused (same key, different request) or idempotency_request_in_progress (a concurrent duplicate is still running — carries Retry-After).",
        "headers": {
          "Idempotency-Status": {
            "$ref": "#/components/headers/IdempotencyStatus"
          },
          "Retry-After": {
            "$ref": "#/components/headers/RetryAfter"
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "idempotency_request_in_progress",
                "message": "a request with this Idempotency-Key is already in progress — retry shortly"
              }
            }
          }
        }
      },
      "PayloadTooLarge": {
        "description": "Payload too large (413) — a JSON field exceeded its serialized-byte cap (code metadata_too_large | content_too_large)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "metadata_too_large",
                "message": "metadata exceeds 16000 bytes (serialized UTF-8)"
              }
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limited (429)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "seconds to wait"
          }
        }
      },
      "ServerError": {
        "description": "Internal error (500)",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "UnixTimeMilliseconds": {
        "type": "integer",
        "format": "int64",
        "minimum": 0,
        "description": "Milliseconds since the Unix epoch (1970-01-01T00:00:00Z).",
        "examples": [
          1783785600000
        ]
      },
      "CollectionId": {
        "type": "string",
        "pattern": "^col_[a-f0-9]{24}$",
        "description": "a collection id (col_ + 24 hex)",
        "examples": [
          "col_9f12471c5ffde765a36e947d"
        ]
      },
      "ItemId": {
        "type": "string",
        "pattern": "^[0-9a-z]+-[0-9a-f]{8}$",
        "minLength": 5,
        "maxLength": 64,
        "description": "an item id",
        "examples": [
          "mrgq5rgt-bee8e453"
        ]
      },
      "ArtifactId": {
        "type": "string",
        "pattern": "^[0-9a-z]+-[0-9a-f]{8}$",
        "minLength": 5,
        "maxLength": 64,
        "description": "an artifact id",
        "examples": [
          "mrgp95ef-9c297c89"
        ]
      },
      "TenantId": {
        "type": "string",
        "pattern": "^t_[0-9a-f]{32}$",
        "description": "a tenant id",
        "examples": [
          "t_a7f49fc0e2d54d92b5ba9253ef9285b1"
        ]
      },
      "EventId": {
        "type": "string",
        "pattern": "^evt_[0-9a-f]{32}$",
        "description": "a webhook event id (dedupe on this)",
        "examples": [
          "evt_1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d"
        ]
      },
      "LocalizedString": {
        "description": "A display string that is either a plain string OR a { langCode: string } map (e.g. { en: 'Hi', he: '…' }).",
        "oneOf": [
          {
            "type": "string"
          },
          {
            "type": "object",
            "minProperties": 1,
            "additionalProperties": {
              "type": "string"
            }
          }
        ]
      },
      "IceServer": {
        "type": "object",
        "description": "One WebRTC ICE/TURN server (RTCIceServer shape). credential is short-lived — refresh before it expires.",
        "required": [
          "urls"
        ],
        "additionalProperties": false,
        "properties": {
          "urls": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ],
            "description": "stun:/turn:/turns: URL(s)"
          },
          "username": {
            "type": "string"
          },
          "credential": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "description": "SENSITIVE — the short-lived TURN credential"
          }
        }
      },
      "ErrorCode": {
        "type": "string",
        "x-extensible-enum": [
          "invalid_request",
          "unauthorized",
          "forbidden",
          "not_found",
          "conflict",
          "rate_limited",
          "insufficient_credit",
          "contributor_token_required",
          "metadata_too_large",
          "content_too_large",
          "idempotency_key_required",
          "idempotency_key_reused",
          "idempotency_request_in_progress",
          "internal_error"
        ],
        "description": "A STABLE machine error code. Treat as an open set (x-extensible-enum): handle the known codes; fall back gracefully on an unknown one (new codes may appear without a breaking version bump)."
      },
      "Error": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "error": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "$ref": "#/components/schemas/ErrorCode"
              },
              "message": {
                "type": "string"
              },
              "request_id": {
                "type": "string",
                "examples": [
                  "req_123"
                ]
              },
              "details": {
                "type": "object",
                "additionalProperties": true,
                "description": "optional structured context"
              }
            }
          }
        },
        "required": [
          "error"
        ]
      },
      "CatalogRating": {
        "type": "object",
        "description": "Rolling rating shown on a catalog card (denormalized; never exposes raw running sums).",
        "required": [
          "avg",
          "calls",
          "count",
          "verified"
        ],
        "additionalProperties": false,
        "properties": {
          "avg": {
            "type": "number",
            "description": "0–5, one decimal"
          },
          "calls": {
            "type": "integer",
            "minimum": 0
          },
          "count": {
            "type": "integer",
            "minimum": 0
          },
          "verified": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "CatalogAgent": {
        "type": "object",
        "description": "A public catalog card (GET /catalog). Distinct from the legacy owner/register Agent schema.",
        "required": [
          "agentId",
          "display",
          "media",
          "author",
          "authorName",
          "authorVerified",
          "official",
          "rating"
        ],
        "additionalProperties": false,
        "properties": {
          "agentId": {
            "type": "string",
            "pattern": "^[a-z0-9][a-z0-9-]{0,63}$"
          },
          "defaultLang": {
            "type": "string"
          },
          "display": {
            "type": "object",
            "required": [
              "name"
            ],
            "additionalProperties": false,
            "properties": {
              "name": {
                "$ref": "#/components/schemas/LocalizedString"
              },
              "emoji": {
                "type": "string"
              },
              "tagline": {
                "$ref": "#/components/schemas/LocalizedString"
              }
            }
          },
          "i18n": {
            "type": "object",
            "additionalProperties": true,
            "description": "localized name/tagline maps (present when localized)"
          },
          "media": {
            "type": "string",
            "enum": [
              "audio",
              "video",
              "text"
            ]
          },
          "disclosure": {
            "type": "string"
          },
          "author": {
            "type": "string"
          },
          "authorName": {
            "type": "string"
          },
          "authorVerified": {
            "type": "boolean"
          },
          "official": {
            "type": "boolean"
          },
          "rating": {
            "$ref": "#/components/schemas/CatalogRating"
          },
          "settings": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            },
            "description": "the agent's launch-form field definitions (safe to publish)"
          }
        }
      },
      "ResponseMeta": {
        "type": "object",
        "description": "Pagination/context on a success envelope. Ordering is per-endpoint: cursor-paginated LISTS (items/artifacts) are oldest→newest by creation time; the /usage feed is newest-first. Cursor pagination is NOT snapshot-consistent: while you traverse a changing dataset, concurrent inserts/deletes can cause a row to be skipped OR repeated across pages — DEDUPLICATE by resource id. Cursors are OPAQUE and bound to the exact query (endpoint + tenant + filters) — do not parse, mutate, or reuse one across endpoints or tenants; pass meta.next_cursor back verbatim as ?cursor. No cursor expiry.",
        "properties": {
          "next_cursor": {
            "type": "string",
            "description": "opaque cursor for the NEXT page; absent on the last page. Pass verbatim as ?cursor."
          },
          "count": {
            "type": "integer",
            "description": "number of items in THIS page/response (not the total result count)"
          },
          "total_spent_cents": {
            "type": "integer",
            "minimum": 0,
            "description": "usage feed: total spend for the RETURNED window"
          },
          "truncated": {
            "type": "boolean",
            "description": "usage feed: true when the source scan hit its row cap → the feed is INCOMPLETE; narrow it with from/to"
          },
          "oldest_returned_at": {
            "$ref": "#/components/schemas/UnixTimeMilliseconds"
          }
        },
        "additionalProperties": false
      },
      "Usage": {
        "type": "object",
        "description": "One usage-feed row (a billed session-minute or tool call).",
        "required": [
          "type",
          "at"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "session_minute",
              "tool_call"
            ]
          },
          "at": {
            "$ref": "#/components/schemas/UnixTimeMilliseconds"
          },
          "agent_id": {
            "type": "string"
          },
          "room": {
            "type": "string"
          },
          "cost_cents": {
            "type": "integer",
            "minimum": 0
          },
          "tool": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "CollectionCreate": {
        "type": "object",
        "required": [
          "metadata"
        ],
        "properties": {
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "x-max-json-bytes": 16000,
            "description": "opaque, app-defined config for this collection. Serialized-UTF-8-byte capped at 16000 (x-max-json-bytes) — over-cap → 413 metadata_too_large (schema maxLength counts characters, so the byte cap is enforced server-side). E.g. a gift uses {kind:'gift', occasion, share_title, share_description, theme}. The platform never interprets it. PUBLIC : it is echoed on GET /{id} (any publishable key) and the KEYLESS render page — do NOT store secrets here. `theme` values (bg/accent/text/font) are sanitized to CSS-safe forms before rendering; anything else falls back to the default."
          },
          "contributor_policy": {
            "type": "string",
            "enum": [
              "open",
              "token"
            ],
            "default": "open",
            "description": "open = anyone with a publishable key may contribute; token = a distinct contributor token (returned once at create) is also required."
          },
          "ttl_days": {
            "type": "integer",
            "minimum": 1,
            "maximum": 365,
            "default": 45,
            "description": "how long before the collection + its items/artifacts auto-expire."
          }
        },
        "additionalProperties": false
      },
      "CollectionCreated": {
        "type": "object",
        "required": [
          "id",
          "admin_token",
          "state"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/CollectionId"
          },
          "admin_token": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "description": "SENSITIVE — the one-time OWNER capability. Shown ONCE, stored only as a hash; pass it as x-collection-token for owner ops. Do not log."
          },
          "contributor_token": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "description": "Present ONLY when contributor_policy=token: a one-time, add-only capability to hand to contributors (they can POST items but nothing else). Shown ONCE, stored hashed."
          },
          "state": {
            "const": "open"
          }
        },
        "additionalProperties": false
      },
      "CollectionPublic": {
        "type": "object",
        "description": "The public view — never exposes items. `app_state` is added ONLY when the caller holds the owner capability token (a publishable-key-only caller does not see it).",
        "required": [
          "id",
          "metadata",
          "state",
          "item_count",
          "created_at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/CollectionId"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "state": {
            "type": "string",
            "enum": [
              "open",
              "ready",
              "closed"
            ]
          },
          "item_count": {
            "type": "integer"
          },
          "created_at": {
            "$ref": "#/components/schemas/UnixTimeMilliseconds"
          },
          "app_state": {
            "type": "object",
            "additionalProperties": true,
            "description": "owner view only — the opaque app-config blob"
          }
        },
        "additionalProperties": false
      },
      "CollectionOwner": {
        "type": "object",
        "description": "The owner view. `items` is a PAGE — every item incl. unapproved/archived, each flagged; page with ?limit&cursor and follow meta.next_cursor.",
        "required": [
          "id",
          "metadata",
          "state",
          "contributor_policy",
          "items"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/CollectionId"
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "state": {
            "type": "string",
            "enum": [
              "open",
              "ready",
              "closed"
            ]
          },
          "created_at": {
            "$ref": "#/components/schemas/UnixTimeMilliseconds"
          },
          "contributor_policy": {
            "type": "string",
            "enum": [
              "open",
              "token"
            ]
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CollectionItem"
            }
          },
          "app_state": {
            "type": "object",
            "additionalProperties": true,
            "description": "the opaque app-config blob (always present in the owner view)"
          }
        },
        "additionalProperties": false
      },
      "CollectionItemCreate": {
        "type": "object",
        "description": "Contribute an item. The special type `sealed-append` carries an E2EE sealed box as `content` (an object like {alg:'box',...}) that is appended BLIND — capped at 2400 bytes per box and 150 boxes per collection (over-cap → 413 / 409) — and returns {ok:true} with no item id.",
        "required": [
          "type",
          "content"
        ],
        "properties": {
          "type": {
            "type": "string",
            "maxLength": 40,
            "description": "app-defined item type, e.g. 'message'"
          },
          "content": {
            "oneOf": [
              {
                "type": "string",
                "maxLength": 8000
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ],
            "x-max-json-bytes": 8000,
            "description": "string or object; serialized-UTF-8-byte capped at 8000 (x-max-json-bytes) — over-cap → 413 content_too_large"
          },
          "from": {
            "type": "string",
            "maxLength": 80
          }
        },
        "additionalProperties": false
      },
      "CollectionItem": {
        "type": "object",
        "required": [
          "id",
          "type",
          "content",
          "at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/ItemId"
          },
          "type": {
            "type": "string"
          },
          "content": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "object",
                "additionalProperties": true
              }
            ]
          },
          "from": {
            "type": [
              "string",
              "null"
            ]
          },
          "access": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "hero": {
            "type": "boolean"
          },
          "archived": {
            "type": "boolean"
          },
          "at": {
            "$ref": "#/components/schemas/UnixTimeMilliseconds"
          }
        },
        "additionalProperties": false
      },
      "CurationPatch": {
        "type": "object",
        "description": "Curation ops for an item OR an artifact — at least one required. Access model: `access` replaces the whole grant-label set; `grant`/`revoke` add/remove ONE label. Legacy booleans (kept through the cutover): approved/archived/hero on items, published on artifacts. This is the validated SUPERSET — the handler applies only the ops meaningful for that resource.",
        "minProperties": 1,
        "properties": {
          "approved": {
            "type": "boolean"
          },
          "archived": {
            "type": "boolean"
          },
          "hero": {
            "type": "boolean"
          },
          "published": {
            "type": "boolean"
          },
          "access": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "replace the whole grant-label set"
          },
          "grant": {
            "type": "string",
            "description": "add ONE access label"
          },
          "revoke": {
            "type": "string",
            "description": "remove ONE access label"
          }
        },
        "additionalProperties": false
      },
      "SessionLaunch": {
        "type": "object",
        "required": [
          "agent_id",
          "room",
          "wallet"
        ],
        "properties": {
          "agent_id": {
            "type": "string"
          },
          "room": {
            "type": "string"
          },
          "wallet": {
            "type": "string",
            "format": "password",
            "writeOnly": true,
            "x-sensitive": true,
            "description": "SENSITIVE — coupon/credit code that funds the run (a bearer credential; body-only, never a URL). REQUIRED even for arm=true: it is STASHED for the eventual summon, not charged until the agent actually launches."
          },
          "arm": {
            "type": "boolean",
            "default": false,
            "description": "arm=true PROVISIONS the room without launching or CHARGING now (the wallet is stashed); the agent launches — and billing starts — when a human later summons it. Returns 201 SessionArmed (armed + summon_key; no session_id/ctrl_token, since nothing is running yet)."
          },
          "keys": {
            "type": "object",
            "additionalProperties": {
              "type": "string",
              "maxLength": 4096
            },
            "maxProperties": 32,
            "description": "BYOK { ENV_NAME: value } — only override VALUES for the agent's declared required keys; ephemeral task env, never persisted/returned. ALLOWLISTED : reserved/loader/cloud-cred names (LD_*/AWS_*/NODE_*/PATH/PYTHON*/WITBITZ_*/AGENT_*/ECS_*…) are rejected, and a name the agent doesn't require is ignored — so nothing arbitrary reaches the task env.",
            "writeOnly": true
          },
          "settings": {
            "type": "object",
            "additionalProperties": true,
            "description": "launch-time knobs (lang, persona vars, placement, theme…)"
          },
          "mk_commit": {
            "type": "string",
            "description": "base64url SHA-256 of the room key — enables encrypted memory."
          }
        },
        "additionalProperties": false
      },
      "Session": {
        "type": "object",
        "description": "A launched session. ctrl_token + summon_key are SENSITIVE bearer credentials returned ONCE at launch — store them server-side; they are NOT safe to expose in a browser/public client, and they live for the session's lifetime (ctrl_token) / until re-summon (summon_key).",
        "required": [
          "session_id",
          "status"
        ],
        "properties": {
          "session_id": {
            "type": "string",
            "minLength": 3,
            "maxLength": 128
          },
          "ctrl_token": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "description": "SENSITIVE — controls this session (stop/steer). Shown once; server-side only, not browser-safe."
          },
          "status": {
            "type": "string",
            "examples": [
              "starting",
              "already-present"
            ]
          },
          "agent_id": {
            "type": "string"
          },
          "room": {
            "type": "string"
          },
          "wallet": {
            "type": "object",
            "properties": {
              "balance_cents": {
                "type": "integer",
                "minimum": 0
              },
              "minutes_funded": {
                "type": "integer"
              }
            }
          },
          "cents_per_minute": {
            "type": "integer",
            "minimum": 0
          },
          "summon_key": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "description": "SENSITIVE — re-launches the same agent/wallet. Shown once; server-side only, not browser-safe."
          },
          "gate_params": {
            "type": "object",
            "additionalProperties": true,
            "description": "appended to the room link so the call app admits this agent (g/gk/gm)."
          },
          "duplicate": {
            "type": "boolean"
          }
        }
      },
      "SessionAlreadyPresent": {
        "type": "object",
        "description": "Returned 200 when the agent is ALREADY present in the room — no new launch, no re-billing. One-time launch credentials (summon_key, wallet) are NOT reissued; ctrl_token may be re-provided so you can re-control the existing session.",
        "required": [
          "duplicate",
          "status",
          "agent_id",
          "room"
        ],
        "additionalProperties": false,
        "properties": {
          "duplicate": {
            "const": true
          },
          "status": {
            "const": "already-present"
          },
          "session_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "agent_id": {
            "type": "string"
          },
          "room": {
            "type": "string"
          },
          "ctrl_token": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "description": "SENSITIVE — control token for the EXISTING session (may be re-provided; not browser-safe)"
          }
        }
      },
      "SessionArmed": {
        "type": "object",
        "description": "Returned 201 for arm=true: the room is PROVISIONED (not launched, not charged) and a summon_key is minted so a human can summon the agent later. No session_id / ctrl_token yet (there is no running session).",
        "required": [
          "armed",
          "agent_id",
          "room",
          "summon_key"
        ],
        "additionalProperties": false,
        "properties": {
          "armed": {
            "const": true
          },
          "agent_id": {
            "type": "string"
          },
          "room": {
            "type": "string"
          },
          "balance_cents": {
            "type": "integer",
            "minimum": 0
          },
          "gate_params": {
            "type": "object",
            "additionalProperties": true,
            "description": "room-link gate params (g/gk/gm) committing the agent's key"
          },
          "summon_key": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "description": "SENSITIVE — one-time key to summon (launch) the armed agent; shown once, not browser-safe"
          }
        }
      },
      "ArtifactBase": {
        "type": "object",
        "required": [
          "id",
          "kind",
          "status",
          "access",
          "published",
          "hero",
          "archived",
          "at"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/ArtifactId"
          },
          "kind": {
            "type": "string",
            "enum": [
              "image",
              "clip",
              "card",
              "video",
              "audio",
              "file"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "painting",
              "ready",
              "failed",
              "quota_exceeded"
            ]
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "access": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "the grant-label set on this artifact (e.g. [\"public\"])"
          },
          "published": {
            "type": "boolean"
          },
          "hero": {
            "type": "boolean"
          },
          "archived": {
            "type": "boolean"
          },
          "at": {
            "$ref": "#/components/schemas/UnixTimeMilliseconds"
          }
        }
      },
      "ArtifactPainting": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ArtifactBase"
          },
          {
            "type": "object",
            "required": [
              "status"
            ],
            "properties": {
              "status": {
                "const": "painting"
              }
            }
          }
        ],
        "unevaluatedProperties": false
      },
      "ArtifactReady": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ArtifactBase"
          },
          {
            "type": "object",
            "required": [
              "status",
              "url"
            ],
            "properties": {
              "status": {
                "const": "ready"
              },
              "url": {
                "type": "string",
                "format": "uri",
                "description": "the hosted asset URL (present only when ready)"
              }
            }
          }
        ],
        "unevaluatedProperties": false
      },
      "ArtifactFailed": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ArtifactBase"
          },
          {
            "type": "object",
            "required": [
              "status",
              "failure_code"
            ],
            "properties": {
              "status": {
                "const": "failed"
              },
              "failure_code": {
                "type": "string",
                "description": "coarse, non-sensitive reason (e.g. paint_failed) — enough to decide whether to retry; upstream detail is never exposed"
              },
              "failure_message": {
                "type": "string",
                "description": "human-readable, non-sensitive summary"
              }
            }
          }
        ],
        "unevaluatedProperties": false
      },
      "ArtifactQuotaExceeded": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ArtifactBase"
          },
          {
            "type": "object",
            "required": [
              "status"
            ],
            "properties": {
              "status": {
                "const": "quota_exceeded"
              }
            }
          }
        ],
        "unevaluatedProperties": false
      },
      "ArtifactPending": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ArtifactBase"
          },
          {
            "type": "object",
            "required": [
              "status"
            ],
            "properties": {
              "status": {
                "const": "pending"
              },
              "pending_expires_at": {
                "$ref": "#/components/schemas/UnixTimeMilliseconds"
              }
            }
          }
        ],
        "unevaluatedProperties": false,
        "description": "Awaiting a direct upload (POST …/artifacts/upload). No url until the client PUTs the file and stamps it ready; treat as stale after pending_expires_at."
      },
      "Artifact": {
        "description": "A produced output. `url` is present ONLY when status=ready — never an empty-string sentinel. Discriminated on `status`.",
        "oneOf": [
          {
            "$ref": "#/components/schemas/ArtifactPending"
          },
          {
            "$ref": "#/components/schemas/ArtifactPainting"
          },
          {
            "$ref": "#/components/schemas/ArtifactReady"
          },
          {
            "$ref": "#/components/schemas/ArtifactFailed"
          },
          {
            "$ref": "#/components/schemas/ArtifactQuotaExceeded"
          }
        ],
        "discriminator": {
          "propertyName": "status",
          "mapping": {
            "pending": "#/components/schemas/ArtifactPending",
            "painting": "#/components/schemas/ArtifactPainting",
            "ready": "#/components/schemas/ArtifactReady",
            "failed": "#/components/schemas/ArtifactFailed",
            "quota_exceeded": "#/components/schemas/ArtifactQuotaExceeded"
          }
        }
      },
      "ArtifactCreate": {
        "type": "object",
        "required": [
          "kind",
          "url"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "image",
              "clip",
              "card",
              "video",
              "audio",
              "file"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://",
            "description": "https URL of the already-hosted asset. HTTPS only — it is embedded on the public render page. No whitespace/quote chars. Runtime additionally rejects private/loopback/link-local/metadata hosts (post-DNS + after any redirect)."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "additionalProperties": false
      },
      "ArtifactUpload": {
        "type": "object",
        "required": [
          "kind",
          "content_type"
        ],
        "description": "Ask for a direct-upload slot for a LARGE binary the tenant produced itself (e.g. a rendered video). The platform returns a presigned PUT; you upload straight to storage, then stamp it ready. content_type is pinned into the presigned PUT — the client MUST send that exact Content-Type or the PUT is rejected.",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "image",
              "clip",
              "card",
              "video",
              "audio",
              "file"
            ]
          },
          "content_type": {
            "type": "string",
            "enum": [
              "video/mp4",
              "video/webm",
              "audio/mpeg",
              "audio/mp4",
              "audio/webm",
              "image/png",
              "image/jpeg",
              "image/webp",
              "image/gif",
              "application/pdf",
              "application/octet-stream"
            ],
            "description": "the exact Content-Type of the binary you will PUT. Bounds what can land in public storage; determines the storage extension. application/octet-stream is for opaque/sealed binaries (e.g. an E2EE keepsake)."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "additionalProperties": false
      },
      "UploadSlot": {
        "type": "object",
        "required": [
          "id",
          "status",
          "upload_url",
          "upload_method",
          "content_type",
          "expires_in",
          "pending_expires_at"
        ],
        "additionalProperties": false,
        "description": "A presigned direct-upload slot. PUT the binary to upload_url with `Content-Type: <content_type>` (and no extra headers) before it expires, then POST …/artifacts/{artifactId}/ready. The artifact stays status=pending until then.",
        "properties": {
          "id": {
            "$ref": "#/components/schemas/ArtifactId"
          },
          "status": {
            "const": "pending"
          },
          "upload_url": {
            "type": "string",
            "format": "uri",
            "description": "the presigned URL to PUT the binary to (short-lived, single content-type)."
          },
          "upload_method": {
            "const": "PUT"
          },
          "content_type": {
            "type": "string",
            "description": "echoes the request content_type — send it verbatim as the PUT's Content-Type header."
          },
          "expires_in": {
            "type": "integer",
            "description": "seconds until upload_url expires."
          },
          "pending_expires_at": {
            "$ref": "#/components/schemas/UnixTimeMilliseconds"
          }
        }
      },
      "PaintRequest": {
        "type": "object",
        "required": [
          "prompt"
        ],
        "properties": {
          "prompt": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4000
          },
          "size": {
            "type": "string",
            "enum": [
              "1024x1024",
              "1536x1024",
              "1024x1536",
              "auto"
            ],
            "default": "1024x1024",
            "description": "output image size (the managed gpt-image model's supported set); invalid → 400"
          }
        },
        "additionalProperties": false
      },
      "ProduceRequest": {
        "type": "object",
        "description": "Run a multi-step recipe (POST /collections/{id}/produce). The recipe is inline; `params` supplies key-values it interpolates via the `params` scope root.",
        "required": [
          "recipe"
        ],
        "properties": {
          "recipe": {
            "$ref": "#/components/schemas/Recipe"
          },
          "params": {
            "type": "object",
            "additionalProperties": true,
            "description": "optional key-values the recipe reads from the `params` scope root"
          }
        },
        "additionalProperties": false
      },
      "Recipe": {
        "type": "object",
        "description": "A backward-only DAG of steps run into the collection. Must contain at least one store_artifact step (its terminal kind becomes the produced artifact's kind). Max 16 steps.",
        "required": [
          "id",
          "steps"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]{0,31}$"
          },
          "steps": {
            "type": "array",
            "minItems": 1,
            "maxItems": 16,
            "items": {
              "$ref": "#/components/schemas/RecipeStep"
            }
          }
        },
        "additionalProperties": false
      },
      "RecipeStep": {
        "type": "object",
        "description": "One recipe step. Step-type-specific fields (beyond those below) are allowed and may reference a scope root (meta | items | params) or an EARLIER step's output — backward only, no forward/self refs.",
        "required": [
          "id",
          "type"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]{0,31}$",
            "description": "unique within the recipe"
          },
          "type": {
            "type": "string",
            "enum": [
              "compose_text",
              "paint_image",
              "overlay_text",
              "store_artifact",
              "render",
              "transform"
            ]
          },
          "out": {
            "type": "string",
            "pattern": "^[a-z][a-z0-9_]{0,31}$",
            "description": "names this step's output for a later step to reference"
          },
          "optional": {
            "type": "boolean",
            "description": "a failing optional step is skipped, not fatal"
          },
          "passthrough": {
            "type": "boolean",
            "description": "only valid on an optional step"
          }
        },
        "additionalProperties": true
      },
      "Wallet": {
        "type": "object",
        "description": "The wallet READ model (POST /wallets/lookup) — STATUS only; the submitted `code` is NOT echoed back (review 1.2 — don't return a bearer credential the caller already holds).",
        "required": [
          "balance_cents"
        ],
        "properties": {
          "balance_cents": {
            "type": "integer",
            "minimum": 0
          },
          "spent_cents": {
            "type": "integer",
            "minimum": 0
          },
          "cents_per_minute": {
            "type": "integer",
            "minimum": 0
          },
          "minutes_left": {
            "type": "integer"
          },
          "can_start": {
            "type": "boolean"
          },
          "gift": {
            "type": "boolean",
            "description": "single-room binding"
          }
        },
        "additionalProperties": false
      },
      "WalletGrant": {
        "type": "object",
        "description": "Result of a grant — carries the NEWLY-MINTED wallet_code (a bearer credit credential, shown once). Distinct from the Wallet read model.",
        "required": [
          "wallet_code",
          "balance_cents"
        ],
        "properties": {
          "wallet_code": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "description": "SENSITIVE — the minted credit credential; hand it to the user, store it as their capability"
          },
          "balance_cents": {
            "type": "integer",
            "minimum": 0
          },
          "gift": {
            "type": "boolean"
          },
          "room": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "additionalProperties": false
      },
      "WalletRedeem": {
        "type": "object",
        "description": "FIRST redemption (201) — carries the wallet_code credential, shown ONCE. A repeat returns WalletRedeemStatus instead (no code).",
        "required": [
          "wallet_code",
          "balance_cents"
        ],
        "properties": {
          "wallet_code": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "description": "SENSITIVE — the credit credential this key redeemed to; shown once"
          },
          "balance_cents": {
            "type": "integer",
            "minimum": 0
          },
          "cents_per_minute": {
            "type": "integer",
            "minimum": 0
          },
          "minutes_left": {
            "type": "integer"
          }
        },
        "additionalProperties": false
      },
      "WalletRedeemStatus": {
        "type": "object",
        "description": "REPLAY of an already-redeemed license key (200) — wallet STATUS only; the wallet_code is NEVER returned again, so a leaked license key can't re-fetch the credential.",
        "required": [
          "redeemed",
          "balance_cents"
        ],
        "properties": {
          "redeemed": {
            "const": true
          },
          "balance_cents": {
            "type": "integer",
            "minimum": 0
          },
          "cents_per_minute": {
            "type": "integer",
            "minimum": 0
          },
          "minutes_left": {
            "type": "integer"
          }
        },
        "additionalProperties": false
      },
      "Tenant": {
        "type": "object",
        "description": "The tenant READ model — never carries secrets (see TenantCreated for signup, which returns the key pair once).",
        "required": [
          "tenant_id",
          "name",
          "status"
        ],
        "properties": {
          "tenant_id": {
            "$ref": "#/components/schemas/TenantId"
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "origins": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "webhook_url": {
            "type": [
              "string",
              "null"
            ],
            "format": "uri",
            "pattern": "^https://"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "suspended"
            ]
          },
          "balance_cents": {
            "type": "integer",
            "minimum": 0
          },
          "created_at": {
            "$ref": "#/components/schemas/UnixTimeMilliseconds"
          }
        },
        "additionalProperties": false
      },
      "TenantCreate": {
        "type": "object",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "origins": {
            "type": "array",
            "maxItems": 50,
            "items": {
              "type": "string",
              "pattern": "^https?://[^/]+$",
              "description": "exact origin: scheme://host[:port], no path/trailing slash, no wildcard"
            }
          },
          "webhook_url": {
            "type": "string",
            "format": "uri",
            "pattern": "^https://",
            "description": "public https delivery endpoint for webhooks (validated: https-only, no private/loopback/metadata host)"
          }
        },
        "additionalProperties": false
      },
      "TenantCreated": {
        "type": "object",
        "description": "Signup result — carries the first API key pair (the secret is shown ONCE). Distinct from the Tenant read model, which never returns secrets.",
        "required": [
          "tenant",
          "keys"
        ],
        "properties": {
          "tenant": {
            "$ref": "#/components/schemas/Tenant"
          },
          "keys": {
            "type": "object",
            "required": [
              "secret",
              "publishable"
            ],
            "properties": {
              "secret": {
                "type": "string",
                "format": "password",
                "readOnly": true,
                "x-sensitive": true,
                "pattern": "^wsk_[A-Za-z0-9_-]+$",
                "description": "SENSITIVE — wsk_…, shown once; store it server-side"
              },
              "publishable": {
                "type": "string",
                "pattern": "^wpk_[A-Za-z0-9_-]+$",
                "description": "wpk_… (browser-safe)"
              }
            },
            "additionalProperties": false
          }
        },
        "additionalProperties": false
      },
      "ApiKeyCreated": {
        "type": "object",
        "description": "Result of minting a key — the raw `key` is a SENSITIVE credential shown ONCE. Distinct from ApiKeyMasked (the read/list model), which never returns it.",
        "required": [
          "key",
          "key_id",
          "type"
        ],
        "properties": {
          "key": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "pattern": "^(wpk|wsk)_[A-Za-z0-9_-]+$",
            "description": "SENSITIVE — the raw key (wpk_/wsk_…), shown once; store it now"
          },
          "key_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "publishable",
              "secret"
            ]
          },
          "note": {
            "type": "string"
          }
        },
        "additionalProperties": false
      },
      "ApiKeyMasked": {
        "type": "object",
        "description": "A key in the list view — the raw secret is never shown, only a masked display prefix.",
        "required": [
          "key_id",
          "type",
          "revoked",
          "display"
        ],
        "properties": {
          "key_id": {
            "type": "string"
          },
          "type": {
            "type": "string",
            "enum": [
              "publishable",
              "secret"
            ]
          },
          "label": {
            "type": "string"
          },
          "created_at": {
            "$ref": "#/components/schemas/UnixTimeMilliseconds"
          },
          "revoked": {
            "type": "boolean"
          },
          "display": {
            "type": "string",
            "description": "masked, e.g. wsk_…"
          }
        },
        "additionalProperties": false
      },
      "WebhookConfig": {
        "type": "object",
        "description": "Result of PUT /tenant/webhooks. webhook_secret is present ONLY when a url was set, and shown ONCE — store it to verify the Witbitz-Signature.",
        "required": [
          "tenant"
        ],
        "properties": {
          "tenant": {
            "$ref": "#/components/schemas/Tenant"
          },
          "webhook_secret": {
            "type": "string",
            "format": "password",
            "readOnly": true,
            "x-sensitive": true,
            "pattern": "^whsec_[A-Za-z0-9_-]+$",
            "description": "SENSITIVE — the whsec_ signing secret, shown once; sealed at rest (KMS), never returned again. Re-setting the webhook url rotates it."
          }
        },
        "additionalProperties": false
      },
      "WebhookEvent": {
        "type": "object",
        "required": [
          "id",
          "type",
          "created_at",
          "tenant_id",
          "data"
        ],
        "properties": {
          "id": {
            "$ref": "#/components/schemas/EventId"
          },
          "type": {
            "type": "string",
            "enum": [
              "session.started",
              "session.ended",
              "collection.item_added",
              "artifact.ready",
              "rating.created"
            ]
          },
          "created_at": {
            "$ref": "#/components/schemas/UnixTimeMilliseconds"
          },
          "tenant_id": {
            "$ref": "#/components/schemas/TenantId"
          },
          "data": {
            "type": "object",
            "additionalProperties": true,
            "description": "type-specific payload. Live shapes: artifact.ready → { collection_id, artifact_id, kind, url, lane }; session.started → { session_id, agent_id, room }; session.ended → { session_id, reason (stopped|expired), agent_id, room }; collection.item_added → { collection_id, item_id, item_type, approved }; rating.created → { agent_id, session_id, stars, verified }."
          }
        },
        "additionalProperties": false,
        "examples": [
          {
            "id": "evt_1a2b",
            "type": "artifact.ready",
            "created_at": 1783785600000,
            "tenant_id": "t_abc",
            "data": {
              "collection_id": "col_9f12471c5ffde765a36e947d",
              "artifact_id": "mrgp95ef-9c297c89",
              "kind": "image",
              "url": "https://witbitz.chat/keepsake/col_…/mrgp95ef-9c297c89.png",
              "lane": "managed"
            }
          }
        ]
      }
    }
  }
}