# REST API of the chat server, OpenAPI 3.1. Normative (ADR 0013).
# Conventions: docs/spec/06-api.md. Authorization actions: docs/spec/05-actions.md.
# Lint: npx -y @redocly/cli@2.62.1 lint --config docs/api/redocly.yaml

openapi: 3.1.0

info:
  title: Chat Server API
  version: 0.2.0
  summary: REST API of the team chat with agents that its instance operator hosts.
  description: |
    Normative REST contract of the chat server (spec-first, ADR 0013).

    Conventions (names, time, IDs, errors, pagination, idempotency, concurrency, rate limits,
    versioning) are in docs/spec/06-api.md. Every operation names its authorization action in
    `x-authz-action` (catalogue docs/spec/05-actions.md) and its rate-limit class in
    `x-rate-limit-class`. The WebSocket gateway is described in asyncapi.yaml; its frame and event
    schemas live in `components.schemas` of this file.

    The server never sends JSON `null`; an absent member means "no value". Clients ignore unknown
    members, unknown enum values and unknown event types.
  license:
    name: Apache-2.0
    identifier: Apache-2.0

servers:
- url: /api/v1
  description: Relative to the instance origin.

security:
- dpopAuth: []
- bearerAuth: []
- consoleSession: []

tags:
- name: Discovery
  description: Public metadata, keys and problem types. Health probes are not part of this API (internal listener).
- name: Auth
  description: Login flows, token refresh with DPoP, logout and step-up.
- name: Me
  description: 'The calling principal: profile, credentials, mentions, personal templates.'
- name: Users
  description: People and owners.
- name: Devices
  description: Devices, sessions and push targets.
- name: Tokens
  description: API tokens of agents, apps and personal tools.
- name: Groups
  description: Groups of people as grant subjects.
- name: Access
  description: Grants, grant preview, who has access, view as person.
- name: Admin
  description: Instance settings, branding, feature flags, keys, identity providers.
- name: Spaces
  description: Spaces and their default operator.
- name: Channels
  description: Channels, behavior, operator, project binding, tags, templates, direct messages.
- name: Threads
  description: Threads, titles, done state, moving, tags, agent sessions.
- name: Messages
  description: Messages, history, edits, reactions, pins.
- name: Interactions
  description: Buttons, selects and forms on messages.
- name: Files
  description: Upload sessions, downloads, thumbnails, transcripts.
- name: Search
  description: Full-text search filtered by permissions.
- name: Sync
  description: Snapshot and catch-up over REST.
- name: Preferences
  description: Read state, drafts, settings, sidebar, notifications, quiet hours.
- name: Agents
  description: Agent administration and backend policy.
- name: Agent runtime
  description: 'Endpoints for agent adapters: heartbeat, consumer, config, run lifecycle.'
- name: Runs
  description: Runs (live box) for people.
- name: Commands
  description: Slash commands, autocomplete, invocation.
- name: Apps
  description: Installed apps, manifests and scopes.
- name: Webhooks
  description: Outgoing webhook subscriptions and deliveries, incoming webhooks.
- name: Audit
  description: Audit log query, export and chain verification.
- name: Stats
  description: Usage, runs, tool calls, errors, costs and prices.
- name: Status
  description: Status page.
- name: Import
  description: Import jobs.
- name: Privacy
  description: Privacy requests and exports.
- name: Hosts
  description: 'Connected hosts for the terminal: registration, pairing, fingerprint, LLM login declaration (I6).'
- name: Terminal
  description: Terminal sessions on connected hosts; the stream itself is end-to-end encrypted and runs over the channels in asyncapi.yaml.
- name: Webhook events
  description: Events delivered to subscribed apps (Standard Webhooks).

paths:

  /instance/discovery:
    get:
      operationId: getDiscovery
      summary: Public instance metadata
      tags: [Discovery]
      x-authz-action: instance.discovery.read
      x-rate-limit-class: rl.public_auth
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Discovery'
              examples:
                default:
                  summary: Response
                  value:
                    instance_id: 0192f4a0-7b1c-7db7-89b9-2fe4c48dcb3e
                    setup_required: false
                    api_versions: [v1]
                    spec_version: 1.0.0
                    gateway:
                      url: /api/v1/gateway
                      protocol_versions: [1]
                    branding:
                      product_name: Team Chat
                      accent_color: loch
                      logo_url: /api/v1/instance/branding/logo
                    auth_providers:
                    - id: 0192f4a0-7b1c-7474-8636-4dbb076ee0db
                      kind: oidc
                      name: Company SSO
                    jwks_uri: /api/v1/instance/jwks
                    webhook_keys_uri: /api/v1/instance/webhook-keys
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /instance/jwks:
    get:
      operationId: getJwks
      summary: Public keys for access and invocation tokens
      tags: [Discovery]
      x-authz-action: instance.discovery.read
      x-rate-limit-class: rl.public_auth
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/jwk-set+json:
              schema:
                $ref: '#/components/schemas/Jwks'
              examples:
                default:
                  summary: Response
                  value:
                    keys:
                    - {}
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /instance/webhook-keys:
    get:
      operationId: getWebhookKeys
      summary: Public keys for webhook signatures
      tags: [Discovery]
      x-authz-action: instance.discovery.read
      x-rate-limit-class: rl.public_auth
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookKeys'
              examples:
                default:
                  summary: Response
                  value:
                    keys:
                    - kid: whk-2026-10
                      public_key: whpk_MCowBQYDK2VwAyEA6h3uZ0Q4i1Yv1o1Wg8bQq3c0o8bq9oQm3Kc8n3m3v0I=
                      state: next
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /instance/branding/logo:
    get:
      operationId: getBrandingLogo
      summary: Download the instance logo
      tags: [Discovery]
      description: |-
        Serves the variant `medium` of the logo (longest edge `file.thumbnail.medium_px`, re-encoded by the server without metadata, 12 3.7 and 7.3), never the original. Answers 404 while no logo is set and while the logo file is not deliverable (pending, blocked, failed or deleted) or has no variant `medium`, always with the same status, body and headers. The response carries `Content-Disposition: inline`, `Cache-Control: no-store`, `ETag` and `Repr-Digest` over the variant and `Access-Control-Allow-Origin: *` without credentials; `Range` is ignored.
      x-authz-action: instance.discovery.read
      x-rate-limit-class: rl.public_auth
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            image/*:
              schema:
                type: string
                format: binary
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []
    put:
      operationId: uploadBrandingLogo
      summary: Upload the logo
      tags: [Admin]
      description: |-
        Owner with fresh step-up; nothing of the body is read before `authorize` allowed the action. Checks in this order (12 3.7): the first part is `file` (400 `validation_failed`); the declared type is JPEG, PNG or WebP (415 `unsupported_media_type`, with `reason` `file_type_denied` for a type in `file.denied_mime_types`); the part holds at most the smaller of `file.max_upload_bytes` and `file.upload_part_bytes` (413 `payload_too_large`) and at least one byte, and no further part follows (400 `validation_failed`); open sessions (429) and free space of the file system driver (503 `unavailable` with `reason` `storage_full`). The new file is linked at once and replaces the previous logo; it becomes deliverable later, after scan and metadata removal, and until then `getBrandingLogo` answers 404. Last write wins; no `If-Match` (06 8).
      x-authz-action: instance.branding.update
      x-rate-limit-class: rl.admin
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImageUpload'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Branding'
              examples:
                default:
                  summary: Response
                  value:
                    product_name: Team Chat
                    accent_color: loch
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteBrandingLogo
      summary: Remove the logo
      tags: [Admin]
      x-authz-action: instance.branding.update
      x-rate-limit-class: rl.admin
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /problems/{code}:
    get:
      operationId: getProblemType
      summary: Documentation of a problem type
      tags: [Discovery]
      x-authz-action: instance.discovery.read
      x-rate-limit-class: rl.public_auth
      parameters:
      - $ref: '#/components/parameters/CodePath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ProblemType'
              examples:
                default:
                  summary: Response
                  value:
                    code: version_conflict
                    status: 412
                    title: Version conflict
                    description: Support team
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/oidc/begin:
    post:
      operationId: beginOidcLogin
      summary: Start an OIDC login
      tags: [Auth]
      x-authz-action: auth.login.begin
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OidcBeginRequest'
            examples:
              default:
                summary: Request
                value:
                  provider_id: 0192f4a0-7b1c-7b3c-87cd-dea30414c03e
                  app_redirect_uri: com.example.teamchat:/login
                  client_challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OidcBeginResponse'
              examples:
                default:
                  summary: Response
                  value:
                    login_attempt_id: 0192f4a0-7b1c-74de-8f28-b1bfbb9fa235
                    authorization_url: https://login.example.com/authorize?client_id=chat&state=s1
                    expires_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/oidc/callback:
    get:
      operationId: handleOidcCallback
      summary: OIDC redirect target of the identity provider
      tags: [Auth]
      description: |-
        Exchanges the code at the provider and verifies the ID token. It stores only the verified external identity in the login flow; mapping, linking and provisioning happen at `completeOidcLogin` (08, 2.2). Then it redirects to `app_redirect_uri` with the query parameters `login_code` (one-time, valid `auth.server_code_ttl`) and `login_attempt_id`. After the flow was found, every failure redirects with `error` and `login_attempt_id`; `error` is one of `access_denied` (the provider sent an error), `login_failed` (a check failed) and `flow_expired`. An unknown or expired `state` gets `400` without a redirect.
      x-authz-action: auth.login.begin
      x-rate-limit-class: rl.public_auth
      parameters:
      - name: state
        in: query
        description: State from the authorization request.
        schema:
          type: string
        required: true
      - name: iss
        in: query
        description: Issuer of the authorization response (RFC 9207). Required when the provider announces `authorization_response_iss_parameter_supported`.
        schema:
          type: string
      - name: code
        in: query
        description: Authorization code.
        schema:
          type: string
      - name: error
        in: query
        description: Error code from the provider.
        schema:
          type: string
      responses:
        '302':
          description: Redirect to `app_redirect_uri` with `login_code` and `login_attempt_id`, or with `error` and `login_attempt_id`.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            Location:
              $ref: '#/components/headers/Location'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/oidc/complete:
    post:
      operationId: completeOidcLogin
      summary: Finish an OIDC login and register the device
      tags: [Auth]
      x-authz-action: auth.login.complete
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/DpopProofLogin'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OidcCompleteRequest'
            examples:
              default:
                summary: Request
                value:
                  login_code: lc_8f2d1c
                  client_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
                  device:
                    device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    name: Alex iPhone
                    platform: ios
                    app_version: 1.4.0
                    public_jwk: {}
                    alg: ES256
      responses:
        '200':
          $ref: '#/components/responses/ConsoleLogin'
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                default:
                  summary: Response
                  value:
                    access_token: eyJhbGciOiJFZERTQSJ9.e30.sig
                    token_type: DPoP
                    expires_in: 900
                    refresh_token: rt_q2Z0
                    refresh_expires_at: '2026-10-10T09:30:00.000Z'
                    session:
                      id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: oidc
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
                    device:
                      id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex iPhone
                      platform: ios
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/trusted-header/login:
    post:
      operationId: loginWithTrustedHeader
      summary: Log in with the identity header of a trusted reverse proxy
      tags: [Auth]
      description: |-
        Only enabled when the server listens on loopback and a `trusted_header` provider is active;
        otherwise 404.
      x-authz-action: auth.login.complete
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/DpopProofLogin'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TrustedHeaderLoginRequest'
            examples:
              default:
                summary: Request
                value:
                  device:
                    device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    name: Alex iPhone
                    platform: ios
                    app_version: 1.4.0
                    public_jwk: {}
                    alg: ES256
      responses:
        '200':
          $ref: '#/components/responses/ConsoleLogin'
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                default:
                  summary: Response
                  value:
                    access_token: eyJhbGciOiJFZERTQSJ9.e30.sig
                    token_type: DPoP
                    expires_in: 900
                    refresh_token: rt_q2Z0
                    refresh_expires_at: '2026-10-10T09:30:00.000Z'
                    session:
                      id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: oidc
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
                    device:
                      id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex iPhone
                      platform: ios
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/passkey/begin:
    post:
      operationId: beginPasskeyLogin
      summary: Start a passkey login
      description: 'Starts a passkey login. The ceremony state is stored on the server (`auth_challenges`) and is valid once for `auth.login_flow_ttl`. Only people with an active identity of the `local` driver can log in with a passkey. For an unknown login name the options carry one decoy credential ID derived by HMAC, so the answer reveals no accounts.'
      tags: [Auth]
      x-authz-action: auth.login.begin
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasskeyLoginBeginRequest'
            examples:
              default:
                summary: Request
                value:
                  login_name: alex
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebAuthnChallenge'
              examples:
                default:
                  summary: Response
                  value:
                    challenge_id: 0192f4a0-7b1c-747c-835d-13eca158d3c0
                    public_key: {}
                    expires_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/passkey/complete:
    post:
      operationId: completePasskeyLogin
      summary: Finish a passkey login and register the device
      description: 'Completes a passkey login with a DPoP proof of the device key. A new device stays `pending` until approved; a known active device stays active. Every rejection gives the same `401` body.'
      tags: [Auth]
      x-authz-action: auth.login.complete
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/DpopProofLogin'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasskeyLoginCompleteRequest'
            examples:
              default:
                summary: Request
                value:
                  challenge_id: 0192f4a0-7b1c-747c-835d-13eca158d3c0
                  credential: {}
                  device:
                    device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    name: Alex iPhone
                    platform: ios
                    app_version: 1.4.0
                    public_jwk: {}
                    alg: ES256
      responses:
        '200':
          $ref: '#/components/responses/ConsoleLogin'
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                default:
                  summary: Response
                  value:
                    access_token: eyJhbGciOiJFZERTQSJ9.e30.sig
                    token_type: DPoP
                    expires_in: 900
                    refresh_token: rt_q2Z0
                    refresh_expires_at: '2026-10-10T09:30:00.000Z'
                    session:
                      id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: oidc
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
                    device:
                      id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex iPhone
                      platform: ios
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/enroll/begin:
    post:
      operationId: beginEnrollment
      summary: Start the first passkey registration with an enrollment code
      description: 'Starts the first passkey registration with an enrollment code of purpose `first_login` or `recovery`. The code is checked but not redeemed; the stored challenge keeps a reference to it. Every rejected code gives `401`.'
      tags: [Auth]
      x-authz-action: auth.login.begin
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnrollBeginRequest'
            examples:
              default:
                summary: Request
                value:
                  enrollment_code: en_4KX9-2MQA
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebAuthnChallenge'
              examples:
                default:
                  summary: Response
                  value:
                    challenge_id: 0192f4a0-7b1c-747c-835d-13eca158d3c0
                    public_key: {}
                    expires_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/enroll/complete:
    post:
      operationId: completeEnrollment
      summary: Register the first passkey and log in
      description: 'Registers the first passkey and logs in; the enrollment code of the challenge is redeemed in the same transaction and the device becomes `active`. A failed verification counts as a failed attempt of the code. Every rejection gives `401`.'
      tags: [Auth]
      x-authz-action: auth.login.complete
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/DpopProof'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnrollCompleteRequest'
            examples:
              default:
                summary: Request
                value:
                  challenge_id: 0192f4a0-7b1c-747c-835d-13eca158d3c0
                  credential: {}
                  passkey_name: Laptop
                  device:
                    device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    name: Alex iPhone
                    platform: ios
                    app_version: 1.4.0
                    public_jwk: {}
                    alg: ES256
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                default:
                  summary: Response
                  value:
                    access_token: eyJhbGciOiJFZERTQSJ9.e30.sig
                    token_type: DPoP
                    expires_in: 900
                    refresh_token: rt_q2Z0
                    refresh_expires_at: '2026-10-10T09:30:00.000Z'
                    session:
                      id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: oidc
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
                    device:
                      id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex iPhone
                      platform: ios
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/enroll/redeem:
    post:
      operationId: redeemEnrollmentCode
      summary: Redeem an enrollment code with device keys and log in
      tags: [Auth]
      description: |-
        Redeems an enrollment code without a passkey; the redeeming device becomes `active` (08, 3.2). Every rejected code (unknown, used, expired, exhausted, wrong purpose, person not active) gets the same `401` body. Code and device keys are checked in one transaction; a failed attempt with a matching code counts against `auth.enrollment_code_max_attempts`.
      x-authz-action: auth.login.complete
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/DpopProof'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/EnrollRedeemRequest'
            examples:
              default:
                summary: Request
                value:
                  enrollment_code: en_EXAMPLE
                  device:
                    device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    name: Alex iPhone
                    platform: ios
                    app_version: 1.4.0
                    public_jwk: {}
                    alg: ES256
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                default:
                  summary: Response
                  value:
                    access_token: eyJhbGciOiJFZERTQSJ9.e30.sig
                    token_type: DPoP
                    expires_in: 900
                    refresh_token: rt_q2Z0
                    refresh_expires_at: '2026-10-10T09:30:00.000Z'
                    session:
                      id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: enrollment_code
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
                    device:
                      id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex iPhone
                      platform: ios
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/pairing/begin:
    post:
      operationId: beginPairing
      summary: Start a device pairing without login
      tags: [Auth]
      description: |-
        The new device registers its public keys and shows the returned code. An active device of the same person approves it with `approvePairing` (08, 5.3).
      x-authz-action: auth.login.begin
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      security: []
      parameters:
      - $ref: '#/components/parameters/DpopProof'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PairingBeginRequest'
            examples:
              default:
                summary: Request
                value:
                  device:
                    device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    name: Alex Laptop
                    platform: macos
                    app_version: 1.4.0
                    public_jwk: {}
                    alg: ES256
                    step_up_key:
                      public_jwk: {}
                      alg: ES256
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PairingStarted'
              examples:
                default:
                  summary: Response
                  value:
                    pairing_id: 0192f4a0-7b1c-7e21-8f3a-5b6c7d8e9f10
                    pairing_code: 7KQ2-M9XD
                    expires_at: '2026-10-10T09:35:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /auth/pairing/complete:
    post:
      operationId: completePairing
      summary: Collect the tokens of an approved pairing
      tags: [Auth]
      description: |-
        The DPoP proof must be signed with the `dpop` key registered at `beginPairing`; a proof signed by another key rejects the pairing. Before approval the answer is `202` with the status; after expiry or rejection `410`.
      x-authz-action: auth.login.complete
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      security: []
      parameters:
      - $ref: '#/components/parameters/DpopProof'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PairingCompleteRequest'
            examples:
              default:
                summary: Request
                value:
                  pairing_id: 0192f4a0-7b1c-7e21-8f3a-5b6c7d8e9f10
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                default:
                  summary: Response
                  value:
                    access_token: eyJhbGciOiJFZERTQSJ9.e30.sig
                    token_type: DPoP
                    expires_in: 900
                    refresh_token: rt_q2Z0
                    refresh_expires_at: '2026-10-10T09:30:00.000Z'
                    session:
                      id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: device_pairing
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
                    device:
                      id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex Laptop
                      platform: macos
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '202':
          description: Not yet approved.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PairingStatus'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /auth/password/login:
    post:
      operationId: loginWithPassword
      summary: Log in with local password and TOTP
      tags: [Auth]
      description: Fallback for local accounts. 404 when `auth.local_password_enabled` is false. A new device stays `pending` until approved (08, 5.2). Unknown names, wrong passwords and locked accounts give the same `401`.
      x-authz-action: auth.login.complete
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/DpopProofLogin'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasswordLoginRequest'
            examples:
              default:
                summary: Request
                value:
                  login_name: alex
                  password: example-password
                  totp_code: '492039'
                  device:
                    device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    name: Alex iPhone
                    platform: ios
                    app_version: 1.4.0
                    public_jwk: {}
                    alg: ES256
      responses:
        '200':
          $ref: '#/components/responses/ConsoleLogin'
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                default:
                  summary: Response
                  value:
                    access_token: eyJhbGciOiJFZERTQSJ9.e30.sig
                    token_type: DPoP
                    expires_in: 900
                    refresh_token: rt_q2Z0
                    refresh_expires_at: '2026-10-10T09:30:00.000Z'
                    session:
                      id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: oidc
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
                    device:
                      id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex iPhone
                      platform: ios
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/token/refresh:
    post:
      operationId: refreshToken
      summary: Rotate the refresh token
      tags: [Auth]
      description: |-
        The DPoP proof must use the device key of the session. Reuse of an already rotated token revokes
        the token family and the session (`session_revoked`), except within `auth.refresh_reuse_grace`
        with the same key. In maintenance mode (`server.maintenance_mode = read_only`, ADR 0065) the
        server issues a new access token and returns the same refresh token, without rotation.
      x-authz-action: auth.token.refresh
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/DpopProof'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TokenRefreshRequest'
            examples:
              default:
                summary: Request
                value:
                  refresh_token: rt_q2Z0
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                default:
                  summary: Response
                  value:
                    access_token: eyJhbGciOiJFZERTQSJ9.e30.sig
                    token_type: DPoP
                    expires_in: 900
                    refresh_token: rt_q2Z0
                    refresh_expires_at: '2026-10-10T09:30:00.000Z'
                    session:
                      id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: oidc
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
                    device:
                      id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex iPhone
                      platform: ios
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /auth/logout:
    post:
      operationId: logout
      summary: End the current session
      tags: [Auth]
      x-authz-action: auth.logout
      x-rate-limit-class: rl.write
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/ConsoleRequest'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            Set-Cookie:
              $ref: '#/components/headers/Set-Cookie'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /auth/step-up/challenge:
    post:
      operationId: createStepUpChallenge
      summary: Create a step-up challenge
      tags: [Auth]
      x-authz-action: auth.step_up
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/ConsoleRequest'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StepUpChallenge'
              examples:
                default:
                  summary: Response
                  value:
                    challenge_id: 0192f4a0-7b1c-747c-835d-13eca158d3c0
                    challenge: Zm9vYmFyYmF6
                    expires_at: '2026-10-10T09:30:00.000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /auth/step-up/passkey:
    post:
      operationId: completeStepUpWithPasskey
      summary: Confirm step-up with a passkey
      description: 'Completes a step-up with a passkey assertion over the step-up challenge. A passkey of another person fails and counts toward `auth.step_up_fail_limit`.'
      tags: [Auth]
      x-authz-action: auth.step_up
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/ConsoleRequest'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StepUpPasskeyRequest'
            examples:
              default:
                summary: Request
                value:
                  challenge_id: 0192f4a0-7b1c-747c-835d-13eca158d3c0
                  credential: {}
      responses:
        '200':
          $ref: '#/components/responses/StepUpDone'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /auth/step-up/device-key:
    post:
      operationId: completeStepUpWithDeviceKey
      summary: Confirm step-up with the device key after user verification
      tags: [Auth]
      x-authz-action: auth.step_up
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StepUpDeviceKeyRequest'
            examples:
              default:
                summary: Request
                value:
                  challenge_id: 0192f4a0-7b1c-747c-835d-13eca158d3c0
                  signature: eyJhbGciOiJFUzI1NiJ9.eyJjIjoiWm05dlltRnlZbUY2In0.sig
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StepUpResult'
              examples:
                default:
                  summary: Response
                  value:
                    step_up_at: '2026-10-10T09:30:00.000Z'
                    valid_until: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /auth/step-up/password:
    post:
      operationId: stepUpWithPassword
      summary: Console step-up with password and TOTP
      tags: [Auth]
      x-authz-action: auth.step_up
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/ConsoleRequest'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StepUpPasswordRequest'
      responses:
        '200':
          $ref: '#/components/responses/StepUpDone'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        default:
          $ref: '#/components/responses/Error'

  /auth/step-up/oidc:
    post:
      operationId: beginStepUpOidcReauth
      summary: Start a console step-up with OIDC
      tags: [Auth]
      x-authz-action: auth.step_up
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/ConsoleRequest'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StepUpOidcBeginRequest'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OidcBeginResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        default:
          $ref: '#/components/responses/Error'

  /auth/step-up/oidc/complete:
    post:
      operationId: completeStepUpOidcReauth
      summary: Finish a console step-up with OIDC
      tags: [Auth]
      x-authz-action: auth.step_up
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/ConsoleRequest'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentityLinkComplete'
      responses:
        '200':
          $ref: '#/components/responses/StepUpDone'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        default:
          $ref: '#/components/responses/Error'

  /console/session:
    get:
      operationId: getConsoleSession
      summary: Read the console session
      tags: [Auth]
      x-authz-action: self.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          $ref: '#/components/responses/ConsoleLogin'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        default:
          $ref: '#/components/responses/Error'

  /console/session/view-as:
    delete:
      operationId: endViewAs
      summary: End the stored view as person
      tags: [Auth]
      x-authz-action: access.view_as.end
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ConsoleRequest'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        default:
          $ref: '#/components/responses/Error'

  /csp-reports:
    post:
      operationId: submitCspReport
      summary: Accept a CSP violation report of the web console
      tags: [Discovery]
      description: 'Endpoint of `report-to csp` and of `Reporting-Endpoints: csp="/api/v1/csp-reports"` on every console response (19-security 5 rule 6, R-AZ-33). Public, because browsers send reports without cookies in many cases; a console cookie is not checked by the CSRF guard here, the action changes nothing. Accepts the Reporting API format (`application/reports+json`, an array of reports) and the legacy `report-uri` format (`application/csp-report`), both limited by `http.max_json_body_bytes`. The server stores no report; it logs only the effective directive, the disposition and the origin of the blocked URL and counts `bothy.console.csp_violations`. Members it does not read are ignored.'
      security: []
      x-authz-action: console.csp_report
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: telemetry
      requestBody:
        required: true
        content:
          application/reports+json:
            schema:
              $ref: '#/components/schemas/CspReportList'
            examples:
              default:
                summary: Reporting API
                value:
                - type: csp-violation
                  age: 10
                  url: https://chat.example.com/console/
                  body:
                    documentURL: https://chat.example.com/console/
                    blockedURL: https://cdn.example.com/x.js
                    effectiveDirective: script-src-elem
                    disposition: enforce
          application/csp-report:
            schema:
              $ref: '#/components/schemas/CspReportLegacy'
            examples:
              default:
                summary: Legacy report-uri
                value:
                  csp-report:
                    document-uri: https://chat.example.com/console/
                    blocked-uri: inline
                    effective-directive: script-src-elem
                    disposition: enforce
      responses:
        '204':
          description: Report accepted (also when it names nothing the server logs).
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me:
    get:
      operationId: getMe
      summary: Read the calling principal
      tags: [Me]
      x-authz-action: self.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Me'
              examples:
                default:
                  summary: Response
                  value:
                    principal:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      kind: user
                      handle: alex
                      display_name: Alex
                    is_owner: false
                    email: alex@example.com
                    locale: de
                    time_zone: Europe/Berlin
                    permission_version: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/profile:
    patch:
      operationId: updateMyProfile
      summary: Change own profile
      tags: [Me]
      x-authz-action: self.profile.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProfileUpdate'
            examples:
              default:
                summary: Request
                value:
                  display_name: Alex
                  locale: de
                  time_zone: Europe/Berlin
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    handle: sam
                    display_name: Sam
                    status: active
                    is_owner: false
                    locale: en
                    time_zone: Europe/Berlin
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/avatar:
    put:
      operationId: setMyAvatar
      summary: Upload own avatar
      tags: [Me]
      description: |-
        Only for people; nothing of the body is read before `authorize` allowed the action. Checks in this order (12 3.7): the first part is `file` (400 `validation_failed`); the declared type is JPEG, PNG or WebP (415 `unsupported_media_type`, with `reason` `file_type_denied` for a type in `file.denied_mime_types`); the part holds at most the smaller of `file.max_upload_bytes` and `file.upload_part_bytes` (413 `payload_too_large`) and at least one byte, and no further part follows (400 `validation_failed`); open sessions (429) and free space of the file system driver (503 `unavailable` with `reason` `storage_full`). The new file is linked at once (`avatar_file_id`) and replaces the previous avatar; it becomes deliverable later, after scan and metadata removal, and until then file downloads answer 409 `file_not_ready`.
      x-authz-action: self.profile.update
      x-rate-limit-class: rl.write
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ImageUpload'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    handle: sam
                    display_name: Sam
                    status: active
                    is_owner: false
                    locale: en
                    time_zone: Europe/Berlin
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteMyAvatar
      summary: Remove own avatar
      tags: [Me]
      x-authz-action: self.profile.update
      x-rate-limit-class: rl.write
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/credentials:
    get:
      operationId: getMyCredentials
      summary: List own identities and passkeys
      tags: [Me]
      x-authz-action: identity.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CredentialOverview'
              examples:
                default:
                  summary: Response
                  value:
                    identities:
                    - id: 0192f4a0-7b1c-7dd2-83b9-528d363b749b
                      provider_id: 0192f4a0-7b1c-7b3c-87cd-dea30414c03e
                      provider_kind: oidc
                      subject: alex
                      issuer: https://login.example.com
                      linked_at: '2026-10-10T09:30:00.000Z'
                    passkeys:
                    - id: 0192f4a0-7b1c-7ad4-8a99-f31c00005918
                      name: Laptop
                      backup_eligible: false
                      created_at: '2026-10-10T09:30:00.000Z'
                    local_password_set: false
                    totp_enrolled: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/identities/begin:
    post:
      operationId: beginIdentityLink
      summary: Start linking another identity
      tags: [Me]
      x-authz-action: identity.link
      x-rate-limit-class: rl.admin
      x-idempotency-exempt: auth_flow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentityLinkBegin'
            examples:
              default:
                summary: Request
                value:
                  provider_id: 0192f4a0-7b1c-7b3c-87cd-dea30414c03e
                  app_redirect_uri: com.example.teamchat:/link
                  client_challenge: E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OidcBeginResponse'
              examples:
                default:
                  summary: Response
                  value:
                    login_attempt_id: 0192f4a0-7b1c-74de-8f28-b1bfbb9fa235
                    authorization_url: https://login.example.com/authorize?client_id=chat&state=s1
                    expires_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/identities/complete:
    post:
      operationId: completeIdentityLink
      summary: Finish linking another identity
      tags: [Me]
      x-authz-action: identity.link
      x-rate-limit-class: rl.admin
      x-idempotency-exempt: auth_flow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentityLinkComplete'
            examples:
              default:
                summary: Request
                value:
                  login_code: lc_8f2d1c
                  client_verifier: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Identity'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7dd2-83b9-528d363b749b
                    provider_id: 0192f4a0-7b1c-7b3c-87cd-dea30414c03e
                    provider_kind: oidc
                    subject: alex
                    issuer: https://login.example.com
                    linked_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/passkeys/begin:
    post:
      operationId: beginPasskeyRegistration
      summary: Start registering a passkey
      tags: [Me]
      x-authz-action: passkey.register
      x-rate-limit-class: rl.admin
      x-idempotency-exempt: auth_flow
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebAuthnChallenge'
              examples:
                default:
                  summary: Response
                  value:
                    challenge_id: 0192f4a0-7b1c-747c-835d-13eca158d3c0
                    public_key: {}
                    expires_at: '2026-10-10T09:30:00.000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/passkeys/complete:
    post:
      operationId: completePasskeyRegistration
      summary: Finish registering a passkey
      tags: [Me]
      x-authz-action: passkey.register
      x-rate-limit-class: rl.admin
      x-idempotency-exempt: auth_flow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PasskeyRegisterComplete'
            examples:
              default:
                summary: Request
                value:
                  challenge_id: 0192f4a0-7b1c-747c-835d-13eca158d3c0
                  credential: {}
                  name: Laptop
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Passkey'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7ad4-8a99-f31c00005918
                    name: Laptop
                    backup_eligible: false
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/local-credential:
    put:
      operationId: setMyLocalCredential
      summary: Set local password and enroll TOTP
      tags: [Me]
      description: 'Needs a local identity (else `409 conflict`) and `auth.local_password_enabled` (else `403 feature_disabled`). Step-up is required once the password path is established (TOTP confirmed, 07 4.2 step 8). The TOTP URI in the response is shown once; the call is a PUT and stores no response.'
      x-authz-action: local_credential.set
      x-rate-limit-class: rl.admin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LocalCredentialSet'
            examples:
              default:
                summary: Request
                value:
                  password: example-password
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/LocalCredentialResult'
              examples:
                default:
                  summary: Response
                  value:
                    password_set: true
                    totp_enabled: false
                    totp_secret_uri: otpauth://totp/Team%20Chat:alex?secret=JBSWY3DPEHPK3PXP
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/mentions:
    get:
      operationId: listMyMentions
      summary: List own mentions
      tags: [Me]
      x-authz-action: mention.list
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MentionItemPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - message:
                        id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                        thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                        channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                        thread_seq: 42
                        author:
                          id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                          kind: user
                          handle: alex
                          display_name: Alex
                        client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                        origin: human
                        kind: text
                        content_md: '@support-agent the invoice export fails since this morning.'
                        content_format: 1
                        silent: false
                        attachments:
                        - position: 1
                          file:
                            id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                            size_bytes: 48213
                            mime_type: image/png
                            original_name: screenshot.png
                            uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                            purpose: attachment
                            upload_state: complete
                            scan_status: clean
                            transcript_status: none
                            thumbnails:
                            - variant: small
                              mime_type: image/jpeg
                              width: 320
                              height: 180
                            created_at: '2026-10-10T09:30:00.000Z'
                        mention_ids: [0192f4a0-7b1c-718f-86e7-9cb8b914b848]
                        reactions:
                        - emoji: 👍
                          count: 3
                        pinned: false
                        components:
                        - id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                          custom_id: approve-refund
                          type: button
                          definition: {}
                          position: 1
                          single_use: true
                          restricted_to: any
                          state: active
                          version: 3
                        version: 3
                        created_at: '2026-10-10T09:30:00.000Z'
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/templates:
    get:
      operationId: listMyTemplates
      summary: List own personal templates
      tags: [Me]
      description: Personal templates of the caller, without a channel context. Channel templates come from `listTemplates`.
      x-authz-action: template.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-777d-8541-d21a5b1c8fe4
                      scope: personal
                      name: Escalation
                      content_md: Please escalate this ticket to second level.
                      version: 3
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createPersonalTemplate
      summary: Create a personal template
      tags: [Me]
      x-authz-action: template.manage
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateCreate'
            examples:
              default:
                summary: Request
                value:
                  name: Escalation
                  content_md: Please escalate this ticket to second level.
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Template'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-777d-8541-d21a5b1c8fe4
                    scope: personal
                    name: Escalation
                    content_md: Please escalate this ticket to second level.
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users:
    get:
      operationId: listUsers
      summary: List people
      tags: [Users]
      x-authz-action: user.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: q
        in: query
        description: Prefix match on handle or display name.
        schema:
          type: string
      - name: status
        in: query
        description: Filter by status.
        schema:
          $ref: '#/components/schemas/UserStatus'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      handle: sam
                      display_name: Sam
                      status: active
                      is_owner: false
                      locale: en
                      time_zone: Europe/Berlin
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createUser
      summary: Create a person
      tags: [Users]
      description: |-
        With `local_account` the server creates the local identity (subject = handle) and a `first_login` enrollment code. The code is a one-time secret: a replay with the same `Idempotency-Key` answers `409 conflict` with `reason` `one_time_secret` (06, 7).
      x-authz-action: user.create
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserCreate'
            examples:
              default:
                summary: Request
                value:
                  handle: robin
                  display_name: Robin
                  email: robin@example.com
                  locale: de
                  time_zone: Europe/Berlin
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserCreated'
              examples:
                default:
                  summary: Response
                  value:
                    user:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      handle: sam
                      display_name: Sam
                      status: active
                      is_owner: false
                      locale: en
                      time_zone: Europe/Berlin
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users/{user_id}:
    get:
      operationId: getUser
      summary: Read a person
      tags: [Users]
      x-authz-action: user.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    handle: sam
                    display_name: Sam
                    status: active
                    is_owner: false
                    locale: en
                    time_zone: Europe/Berlin
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: updateUser
      summary: Change another person's profile
      tags: [Users]
      x-authz-action: user.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserUpdate'
            examples:
              default:
                summary: Request
                value: {}
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    handle: sam
                    display_name: Sam
                    status: active
                    is_owner: false
                    locale: en
                    time_zone: Europe/Berlin
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users/{user_id}/disable:
    post:
      operationId: disableUser
      summary: Disable a person
      tags: [Users]
      x-authz-action: user.disable
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Left the team
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    handle: sam
                    display_name: Sam
                    status: active
                    is_owner: false
                    locale: en
                    time_zone: Europe/Berlin
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users/{user_id}/enable:
    post:
      operationId: enableUser
      summary: Enable a person
      tags: [Users]
      x-authz-action: user.enable
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Left the team
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    handle: sam
                    display_name: Sam
                    status: active
                    is_owner: false
                    locale: en
                    time_zone: Europe/Berlin
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users/{user_id}/enrollment-codes:
    post:
      operationId: createEnrollmentCode
      summary: Issue an enrollment code
      tags: [Users]
      description: |-
        Issues a code for an active person: `first_login` for a person with a local identity and no device yet, `recovery` for a person with a local identity and a device, `identity_link` for a person without a local identity (08, 3.2). Issuing ends every other unused code of the person. The code is a one-time secret: a replay with the same `Idempotency-Key` answers `409 conflict` with `reason` `one_time_secret` instead of repeating it (06, 7).
      x-authz-action: user.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnrollmentCode'
              examples:
                default:
                  summary: Response
                  value:
                    code: en_4KX9-2MQA
                    purpose: first_login
                    expires_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users/{user_id}/credentials:
    get:
      operationId: getUserCredentials
      summary: List identities and passkeys of a person
      tags: [Users]
      x-authz-action: identity.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CredentialOverview'
              examples:
                default:
                  summary: Response
                  value:
                    identities:
                    - id: 0192f4a0-7b1c-7dd2-83b9-528d363b749b
                      provider_id: 0192f4a0-7b1c-7b3c-87cd-dea30414c03e
                      provider_kind: oidc
                      subject: alex
                      issuer: https://login.example.com
                      linked_at: '2026-10-10T09:30:00.000Z'
                    passkeys:
                    - id: 0192f4a0-7b1c-7ad4-8a99-f31c00005918
                      name: Laptop
                      backup_eligible: false
                      created_at: '2026-10-10T09:30:00.000Z'
                    local_password_set: false
                    totp_enrolled: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users/{user_id}/identities/{identity_id}:
    delete:
      operationId: unlinkIdentity
      summary: Unlink an identity
      tags: [Users]
      x-authz-action: identity.unlink
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      - $ref: '#/components/parameters/IdentityIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users/{user_id}/passkeys/{passkey_id}:
    delete:
      operationId: revokePasskey
      summary: Revoke a passkey
      tags: [Users]
      x-authz-action: passkey.revoke
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      - $ref: '#/components/parameters/PasskeyIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users/{user_id}/devices:
    get:
      operationId: listUserDevices
      summary: List devices of a person
      tags: [Devices]
      x-authz-action: device.list
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DevicePage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex iPhone
                      platform: ios
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users/{user_id}/sessions:
    get:
      operationId: listUserSessions
      summary: List sessions of a person
      tags: [Devices]
      x-authz-action: session.list
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: oidc
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /users/{user_id}/view-as:
    get:
      operationId: viewAsUser
      summary: See the instance as a person sees it
      tags: [Access]
      description: 'Owner only, with step-up and audit. Returns the spaces and channels the person sees, with level and the end of the view (`authz.view_as_max_duration`). The person''s direct messages stay invisible (SR6). In M1 the view lives only within this request (07, 9.2).'
      x-authz-action: access.view_as
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ViewAsSnapshot'
              examples:
                default:
                  summary: Response
                  value:
                    user:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      kind: user
                      handle: alex
                      display_name: Alex
                    expires_at: '2026-10-10T09:45:00.000Z'
                    spaces:
                    - space:
                        id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                        name: Support
                        sort_key: a0
                        version: 3
                        created_at: '2026-10-10T09:30:00.000Z'
                      level: read
                      capabilities: [approve_actions]
                    channels:
                    - channel:
                        id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                        kind: text
                        name: tickets
                        topic: Customer tickets
                        sort_key: a1
                        read_only: false
                        operator:
                          mode: inherit
                        version: 3
                        settings_version: 1
                        created_at: '2026-10-10T09:30:00.000Z'
                      level: read
                      capabilities: [approve_actions]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /owners:
    get:
      operationId: listOwners
      summary: List owners
      tags: [Users]
      x-authz-action: user.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OwnerList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - user:
                        id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        kind: user
                        handle: alex
                        display_name: Alex
                      granted_at: '2026-10-10T09:30:00.000Z'
                      reason: Requested by the space lead
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: addOwner
      summary: Make a person owner
      description: 'Optional `resolve_i1` runs the guided flow (05 rule 1.6, 10 section 11): the listed agents are authorized with `agent.billing_mode.set` and switched to `organization` first, in the same transaction as the change; I1 is checked before the commit, and either both parts commit or none.'
      tags: [Users]
      x-authz-action: owner.add
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OwnerAdd'
            examples:
              default:
                summary: Request
                value:
                  user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                  reason: Second owner as deputy
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Owner'
              examples:
                default:
                  summary: Response
                  value:
                    user:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      kind: user
                      handle: alex
                      display_name: Alex
                    granted_at: '2026-10-10T09:30:00.000Z'
                    reason: Requested by the space lead
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /owners/{user_id}/revoke:
    post:
      operationId: revokeOwner
      summary: Remove the owner role
      tags: [Users]
      description: |-
        Fails with `last_owner` when no other active owner remains and with `i1_violation` when I1 would
        break.
      x-authz-action: owner.remove
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/UserIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Left the team
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/devices:
    get:
      operationId: listMyDevices
      summary: List own devices
      tags: [Devices]
      x-authz-action: device.list
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DevicePage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex iPhone
                      platform: ios
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/devices/pairings:
    post:
      operationId: approvePairing
      summary: Approve a device pairing with its code
      tags: [Devices]
      description: |-
        Called from an active device of the person, with step-up. Creates the new device as `active` for the calling person; the new device collects its tokens with `completePairing` within `auth.pairing_code_ttl`, otherwise it is revoked. A code that matches no open pairing answers `404` and ends the step-up of the calling session, so every further attempt needs a new confirmation with biometrics.
      x-authz-action: device.approve
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PairingApprove'
            examples:
              default:
                summary: Request
                value:
                  pairing_code: 7KQ2-M9XD
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Device'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    name: Alex Laptop
                    platform: macos
                    state: active
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /devices/{device_id}:
    get:
      operationId: getDevice
      summary: Read a device
      tags: [Devices]
      description: A pending device polls this to learn when it was approved.
      x-authz-action: device.list
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/DeviceIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Device'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    name: Alex iPhone
                    platform: ios
                    state: active
                    app_version: 1.4.0
                    key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /devices/{device_id}/approve:
    post:
      operationId: approveDevice
      summary: Approve a pending device
      tags: [Devices]
      description: |-
        The approving person types the verification code shown on the pending device (08, 5.2). A wrong code revokes the pending device; the answer is then `200` with `state` `revoked`, because the request had an effect.
      x-authz-action: device.approve
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/DeviceIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DeviceApprove'
            examples:
              default:
                summary: Request
                value:
                  verification_code: '042517'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Device'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    name: Alex iPhone
                    platform: ios
                    state: active
                    app_version: 1.4.0
                    key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /devices/{device_id}/revoke:
    post:
      operationId: revokeDevice
      summary: Revoke a device
      tags: [Devices]
      description: Closes its gateway connections with 4012 and revokes its sessions.
      x-authz-action: device.revoke
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/DeviceIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Left the team
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Device'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    name: Alex iPhone
                    platform: ios
                    state: active
                    app_version: 1.4.0
                    key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/devices/current/push-registrations:
    post:
      operationId: registerPush
      summary: Register a push target for the current device
      tags: [Devices]
      description: |-
        Registers the push target of the caller's own active device in the caller's session and returns the preview token of the registration exactly once (13, 6.4 and 13). The row binds the person, the device and the session. A known `(provider, token)` is taken over: its row is deleted, whoever it belonged to, so the token stops reaching its former owner; the other active registrations of this device with the same provider and `app_id` end with `disabled_reason` `token_invalid` (one current token per app installation). Every new registration rotates the preview token. `unifiedpush` and `wns` give `400 validation_failed` with `errors[].code` `not_enabled`; `apns` or `fcm` without a configured adapter on this instance give `403 feature_disabled` with `reason` `push_provider_disabled`; a principal without a session (API tokens, agents, apps) gives `403 forbidden` with `reason` `step_up_not_possible`; a pending device gives `403 device_pending`. The preview token is a one-time secret: a replay with the same `Idempotency-Key` answers `409 conflict` with `reason` `one_time_secret` (06, 7), and the app registers again with a new key. A second concurrent registration of the same token can answer `409 conflict`.
      x-authz-action: push.register
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PushRegistrationCreate'
            examples:
              default:
                summary: Request
                value:
                  provider: fcm
                  token: EXAMPLE-device-token-a1b2
                  app_id: com.example.teamchat
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushRegistrationCreated'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-72f6-893c-a890548910a3
                    device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    provider: fcm
                    environment: production
                    app_id: com.example.teamchat
                    created_at: '2026-10-10T09:30:00.000Z'
                    preview_token: bothy_pvw_EXAMPLEpushpreviewtoken00000000000000000000_4Z1L0A
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /push-registrations/{registration_id}:
    delete:
      operationId: unregisterPush
      summary: Remove a push target
      tags: [Devices]
      description: |-
        Deletes a registration of the caller (13, 13). The token is free for a later registration and the preview token of the registration is worthless from then on. A registration of another person and an unknown one give the same `404`.
      x-authz-action: push.unregister
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/RegistrationIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /push/preview:
    get:
      operationId: getPushPreview
      summary: Load the preview of a notification
      tags: [Devices]
      description: |-
        Called by the notification extension with the preview token of the push registration (`Authorization: DPoP <token>`) and a DPoP proof of the device key whose `ath` is the hash of the preview token; no other operation accepts the token, and no other credential works here (13, 6.4; R-PN-07). Checks in this order: the parameter `kind`; exactly one `Authorization` value with the scheme `DPoP`; the form, kind `pvw` and checksum of the token without a database access; an active registration with the token; the session of the registration, whose device must be the device of the registration; the DPoP proof against the key of that session (`DPoP-Nonce` as on every DPoP request); then `message.read` on the message and `push.deliver` on its thread with the rights of the moment of the fetch. A missing, malformed or unknown token, a deactivated registration, a session that is unknown or belongs to another device and a wrong proof give `401`; while the registration is still active, a session, device or person that is no longer active (for example an expired session) gives `401 session_revoked`. `auth.logout`, `device.revoke` and `user.disable` deactivate the registration in their own transaction, so after them the token itself answers `401`. No right, a deleted or an unknown message give `404`, identical to each other. The answer follows `notify.preview_mode` of the person (13, 6.4 table): `full` with names, excerpt (text without Markdown, mentions as names, at most `notify.preview_max_chars`), `attachment_count` and `image_url` (a fetch ticket for the `small` thumbnail of the first image); `names` with the four names only; `none` only `kind`, `thread_id`, `message_id` and `badge`.
      x-authz-action: message.read
      x-rate-limit-class: rl.read
      security:
      - pushPreviewToken: []
      parameters:
      - name: nid
        in: query
        required: true
        description: Notification ID from the push.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: message_id
        in: query
        required: true
        description: Message of the notification.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: kind
        in: query
        required: true
        description: Kind of the notification, copied from the push payload (`k`, 13, 6.1 and 6.2). The server keeps no record of a hint, so the kind selects only the labels on the device; the answer echoes it. Only the kinds that point at a message are allowed; `security` and `admin` give `400 validation_failed` with `errors[].code` `invalid` at `#query/kind`, before the token is checked.
        schema:
          $ref: '#/components/schemas/NotificationKind'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            DPoP-Nonce:
              $ref: '#/components/headers/DPoP-Nonce'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushPreview'
              examples:
                default:
                  summary: Response
                  value:
                    kind: mention
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    badge: 3
                    space_name: Support
                    channel_name: tickets
                    thread_title: Invoice export fails
                    sender_display_name: Alex
                    excerpt: Sam can you check the export?
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/sessions:
    get:
      operationId: listMySessions
      summary: List own sessions
      tags: [Devices]
      x-authz-action: session.list
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: oidc
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/sessions/extensions:
    post:
      operationId: createExtensionSession
      summary: Create a child session for an app extension
      tags: [Devices]
      description: |-
        Creates a child session of the calling app session for the notification or share extension, with its own refresh family on the same device key (08, 7.4). An active child of the same component is replaced. The refresh token is shown once; the request carries no `Idempotency-Key` because a repeat only replaces the child.
      x-authz-action: session.extension.create
      x-rate-limit-class: rl.write
      x-idempotency-exempt: auth_flow
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ExtensionSessionCreate'
            examples:
              default:
                summary: Request
                value:
                  client_component: share_ext
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokenResponse'
              examples:
                default:
                  summary: Response
                  value:
                    access_token: eyJhbGciOiJFZERTQSJ9.e30.sig
                    token_type: DPoP
                    expires_in: 900
                    refresh_token: rt_q2Z0
                    refresh_expires_at: '2026-10-10T09:30:00.000Z'
                    session:
                      id: 0192f4a0-7b1c-75c3-8a09-bf22e12411b6
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      auth_method: oidc
                      client_component: share_ext
                      created_at: '2026-10-10T09:30:00.000Z'
                      expires_at: '2026-10-10T09:30:00.000Z'
                    device:
                      id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Alex iPhone
                      platform: ios
                      state: active
                      app_version: 1.4.0
                      key_thumbprint: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /sessions/{session_id}/revoke:
    post:
      operationId: revokeSession
      summary: End a session
      tags: [Devices]
      x-authz-action: session.revoke
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/SessionIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /principals/{principal_id}/tokens:
    get:
      operationId: listTokens
      summary: List API tokens of a principal
      tags: [Tokens]
      x-authz-action: token.list
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/PrincipalIdPath'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiTokenPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-703d-85ae-fdc884c55554
                      principal_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Adapter production
                      token_prefix: bothy_agt_EXAMPL
                      scopes: []
                      dpop_jkt: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      expires_at: '2027-10-10T00:00:00.000Z'
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createToken
      summary: Create an API token
      tags: [Tokens]
      description: |-
        Kind follows the principal (`agt` agent, `app` app, `pat` person, 08 section 12). A personal token only for oneself and only with the flag `tokens.personal_enabled`. A token for an app needs an active installation (else `409 conflict` with `reason` `app_not_installed`, also while the app is suspended); its scopes are the requested subset of the granted scopes of the installation, or all of them when `scopes` is absent or empty, and a scope the installation does not grant gives `400 validation_failed` at `/scopes/<i>`. An app request needs the scope of its action in the token and in the installation. The secret is shown once; a replay with the same `Idempotency-Key` answers `409 conflict` with `reason` `one_time_secret` (06, 7).
      x-authz-action: token.create
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/PrincipalIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ApiTokenCreate'
            examples:
              default:
                summary: Request
                value:
                  name: Adapter production
                  dpop_jkt: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                  expires_at: '2027-10-10T00:00:00.000Z'
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ApiTokenCreated'
              examples:
                default:
                  summary: Response
                  value:
                    token:
                      id: 0192f4a0-7b1c-703d-85ae-fdc884c55554
                      principal_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      name: Adapter production
                      token_prefix: bothy_agt_EXAMPL
                      scopes: []
                      dpop_jkt: 0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I
                      expires_at: '2027-10-10T00:00:00.000Z'
                      created_at: '2026-10-10T09:30:00.000Z'
                    secret: bothy_agt_EXAMPLEOnlyNotARealTokenForTheChecksumTests_3NymP3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /tokens/{token_id}:
    delete:
      operationId: revokeToken
      summary: Revoke an API token
      tags: [Tokens]
      x-authz-action: token.revoke
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/TokenIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /groups:
    get:
      operationId: listGroups
      summary: List groups
      tags: [Groups]
      x-authz-action: group.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: member_id
        in: query
        description: Only groups with this member.
        schema:
          $ref: '#/components/schemas/Uuid'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7abf-88a6-0fbb818f7405
                      name: Support
                      description: Support team
                      source: local
                      member_count: 1
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createGroup
      summary: Create a group
      tags: [Groups]
      x-authz-action: group.create
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupCreate'
            examples:
              default:
                summary: Request
                value:
                  name: Support
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Group'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7abf-88a6-0fbb818f7405
                    name: Support
                    description: Support team
                    source: local
                    member_count: 1
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /groups/{group_id}:
    get:
      operationId: getGroup
      summary: Read a group
      tags: [Groups]
      x-authz-action: group.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/GroupIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Group'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7abf-88a6-0fbb818f7405
                    name: Support
                    description: Support team
                    source: local
                    member_count: 1
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: updateGroup
      summary: Rename a group
      tags: [Groups]
      x-authz-action: group.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/GroupIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupUpdate'
            examples:
              default:
                summary: Request
                value:
                  name: Support
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Group'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7abf-88a6-0fbb818f7405
                    name: Support
                    description: Support team
                    source: local
                    member_count: 1
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteGroup
      summary: Delete a group and revoke its grants
      tags: [Groups]
      x-authz-action: group.delete
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/GroupIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /groups/{group_id}/members:
    get:
      operationId: listGroupMembers
      summary: List members
      tags: [Groups]
      x-authz-action: group.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/GroupIdPath'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupMemberPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - user:
                        id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        kind: user
                        handle: alex
                        display_name: Alex
                      added_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: addGroupMember
      summary: Add a member
      description: 'Optional `resolve_i1` runs the guided flow (05 rule 1.6, 10 section 11): the listed agents are authorized with `agent.billing_mode.set` and switched to `organization` first, in the same transaction as the change; I1 is checked before the commit, and either both parts commit or none.'
      tags: [Groups]
      x-authz-action: group.member.add
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/GroupIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupMemberAdd'
            examples:
              default:
                summary: Request
                value:
                  user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupMember'
              examples:
                default:
                  summary: Response
                  value:
                    user:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      kind: user
                      handle: alex
                      display_name: Alex
                    added_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /groups/{group_id}/members/{user_id}:
    delete:
      operationId: removeGroupMember
      summary: Remove a member
      tags: [Groups]
      x-authz-action: group.member.remove
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/GroupIdPath'
      - $ref: '#/components/parameters/UserIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /grants:
    get:
      operationId: listGrants
      summary: List grants of a target or subject
      tags: [Access]
      x-authz-action: grant.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: target_kind
        in: query
        description: Target kind.
        schema:
          $ref: '#/components/schemas/TargetKind'
      - name: target_id
        in: query
        description: Target ID.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: subject_kind
        in: query
        description: Subject kind.
        schema:
          $ref: '#/components/schemas/GrantSubjectKind'
      - name: subject_id
        in: query
        description: Subject ID.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: include_revoked
        in: query
        description: Include revoked and expired grants.
        schema:
          type: boolean
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GrantPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-727f-86bd-90fe7bdb3024
                      subject_kind: user
                      subject_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      target_kind: instance
                      level: read
                      capabilities: [approve_actions]
                      reason: Support team works in this space
                      granted_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      granted_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createGrant
      summary: Create a grant
      description: 'Optional `resolve_i1` runs the guided flow (05 rule 1.6, 10 section 11): the listed agents are authorized with `agent.billing_mode.set` and switched to `organization` first, in the same transaction as the change; I1 is checked before the commit, and either both parts commit or none.'
      tags: [Access]
      x-authz-action: grant.create
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GrantCreate'
            examples:
              default:
                summary: Request
                value:
                  subject_kind: user
                  subject_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                  target_kind: instance
                  level: read
                  reason: Support team works in this space
              guided:
                summary: Switch the operator to organization first (guided flow)
                value:
                  subject_kind: user
                  subject_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                  target_kind: channel
                  target_id: 0192f4a0-7b1c-7a3d-9f1e-2b4c6d8e0a12
                  level: trigger
                  reason: Sam works in the research notes
                  resolve_i1:
                    switch_to_organization: [0192f4a0-7b1c-7c55-8e21-3f5a7b9d1c34]
                    reason: Sam triggers the assistant too
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Grant'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-727f-86bd-90fe7bdb3024
                    subject_kind: user
                    subject_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    target_kind: instance
                    level: read
                    capabilities: [approve_actions]
                    reason: Support team works in this space
                    granted_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    granted_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /grants/preview:
    post:
      operationId: previewGrant
      summary: Show what a grant would change
      tags: [Access]
      description: |-
        Dry run of `createGrant` (action `access.preview`): affected principals, level changes and
        invariant violations. No step-up needed; the transaction is rolled back, nothing is stored.
        The caller needs read access to the target (else `404`) and `owner` or `manage` on it (else
        `403`); limits of delegation give `200` with `allowed: false` and `denial_reason`. `text`
        is in the caller's language (`de` or `en`, else `de`) with dates in the caller's time zone
        (07, 9.3). With `resolve_i1` the preview is a dry run of the whole guided flow: every listed
        agent needs `access.preview` with the target action `agent.billing_mode.set` (owner, no
        step-up; an unknown or invisible agent gives `404`), the agents are switched to
        `organization` before the grant, and `text` names each agent the request would switch.
      x-authz-action: access.preview
      x-rate-limit-class: rl.admin
      x-idempotency-exempt: read_only
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GrantCreate'
            examples:
              default:
                summary: Request
                value:
                  subject_kind: user
                  subject_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                  target_kind: instance
                  level: read
                  reason: Support team works in this space
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GrantPreview'
              examples:
                default:
                  summary: Response
                  value:
                    allowed: true
                    effects:
                    - principal:
                        id: 0192f4a0-7b1c-733c-835c-6d67630008cd
                        kind: user
                        handle: sam
                        display_name: Sam
                      target_kind: channel
                      target_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      before_level: none
                      after_level: trigger
                      capabilities_added: []
                    invariant_violations: []
                    hints:
                    - code: read_horizon
                      agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      principal_id: 0192f4a0-7b1c-733c-835c-6d67630008cd
                      channel_id: 0192f4a0-7b1c-7c2a-8e1d-41a6b2c3d4e5
                    text: "Sam gets the level \"Trigger\" in channel Marketing/Kampagnen until 2026-12-31.\nSam can then: read and write messages, create threads, trigger agent Texter.\nUpload files: no. Confirm approval cards: no.\nNote: Texter also reads Marketing/Intern. Sam may not read Marketing/Intern but can learn its contents through Texter."
                    text_locale: en
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /grants/{grant_id}:
    get:
      operationId: getGrant
      summary: Read a grant
      tags: [Access]
      x-authz-action: grant.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/GrantIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Grant'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-727f-86bd-90fe7bdb3024
                    subject_kind: user
                    subject_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    target_kind: instance
                    level: read
                    capabilities: [approve_actions]
                    reason: Support team works in this space
                    granted_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    granted_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /grants/{grant_id}/revoke:
    post:
      operationId: revokeGrant
      summary: Revoke a grant
      tags: [Access]
      x-authz-action: grant.revoke
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/GrantIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Left the team
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Grant'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-727f-86bd-90fe7bdb3024
                    subject_kind: user
                    subject_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    target_kind: instance
                    level: read
                    capabilities: [approve_actions]
                    reason: Support team works in this space
                    granted_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    granted_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /spaces/{space_id}/access:
    get:
      operationId: explainSpaceAccess
      summary: Who has access to a space
      tags: [Access]
      description: 'Every principal with access and the chain of sources (direct, group, inherited, operator, via agent, blocked by a guard rule). An entry only through an agent has level `none`; disabled principals are marked `inactive` (07, 9.1).'
      x-authz-action: access.explain
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/SpaceIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessExplanation'
              examples:
                default:
                  summary: Response
                  value:
                    target_kind: space
                    target_id: 0192f4a0-7b1c-7fd5-831e-d4370e087874
                    entries:
                    - principal:
                        id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        kind: user
                        handle: alex
                        display_name: Alex
                      level: read
                      capabilities: [approve_actions]
                      sources:
                      - kind: owner
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/access:
    get:
      operationId: explainChannelAccess
      summary: Who has access to a channel
      tags: [Access]
      description: 'Every principal with access and the chain of sources (direct, group, inherited, operator, direct-message participant, via agent, blocked by a guard rule). An entry only through an agent has level `none`; disabled principals are marked `inactive` (07, 9.1).'
      x-authz-action: access.explain
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessExplanation'
              examples:
                default:
                  summary: Response
                  value:
                    target_kind: space
                    target_id: 0192f4a0-7b1c-7fd5-831e-d4370e087874
                    entries:
                    - principal:
                        id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        kind: user
                        handle: alex
                        display_name: Alex
                      level: read
                      capabilities: [approve_actions]
                      sources:
                      - kind: owner
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /access/hints:
    get:
      operationId: listPermissionHints
      summary: List the permission hints
      tags: [Access]
      description: 'The list "Permission hints" of 07 section 9.5, computed from the permission state that `authorize` uses: `h1_read_horizon` for every agent, person of its trigger set and channel of its read horizon the person cannot read (07 section 6), `grant_to_disabled` for grants whose subject is disabled, `trigger_blocked` for `trigger` grants whose triggering SR2 or SR3 blocks on every operated channel they reach, `agent_personal` for agents in mode `personal` with their trigger set. Ordered by kind in this order, then by agent, person and channel, by grant ID or by agent ID. Without `space_id` the instance (owners); with it the space (owner or `manage`), limited to its agents, channels and grants. A space the caller cannot read gives `404`, readable without `manage` `403`. Hints never name content; `labels` carries the names of the principals, groups, spaces and channels of the page.'
      x-authz-action: access.hints.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: space_id
        in: query
        description: Only the hints of this space; without it the whole instance (owners only).
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: kind
        in: query
        description: Only hints of this kind.
        schema:
          $ref: '#/components/schemas/PermissionHintKind'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PermissionHintPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - kind: h1_read_horizon
                      agent_id: 0192f4a0-7b1c-7a11-8c2e-1d4e5f6a7b8c
                      person_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bd
                      space_id: 0192f4a0-7b1c-7fd5-831e-d4370e087874
                      channel_id: 0192f4a0-7b1c-7c33-9e4f-2a3b4c5d6e7f
                    labels:
                      principals:
                      - id: 0192f4a0-7b1c-7a11-8c2e-1d4e5f6a7b8c
                        kind: agent
                        handle: texter
                        display_name: Texter
                      - id: 0192f4a0-7b1c-7f89-8d6b-6960453241bd
                        kind: user
                        handle: sam
                        display_name: Sam
                      groups: []
                      spaces:
                      - id: 0192f4a0-7b1c-7fd5-831e-d4370e087874
                        name: Marketing
                      channels:
                      - id: 0192f4a0-7b1c-7c33-9e4f-2a3b4c5d6e7f
                        space_id: 0192f4a0-7b1c-7fd5-831e-d4370e087874
                        name: Intern
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /access/expiring:
    get:
      operationId: listExpiringAccess
      summary: List the access that expires soon
      tags: [Access]
      description: 'The list "Expiring soon" of 07 section 9.4: grants that are not revoked and memberships that are not removed (of groups that are not deleted) whose `expires_at` lies within `authz.expiry_reminder_lead` from now, and for owners the API tokens of agents and apps that are not revoked and expire within the same lead (never personal tokens, never a token value), ordered by expiry, then kind, then ID. "Extend" is a revocation plus a new grant with a new reason (07 section 2.5), a token is renewed by creating a new one and revoking the old one. Without `space_id` the instance (owners); with it the space (owner or `manage`): only grants on the space and its channels and memberships of groups that hold such a grant, no tokens. A space the caller cannot read gives `404`, readable without `manage` `403`.'
      x-authz-action: access.hints.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: space_id
        in: query
        description: Only the access within this space; without it the whole instance (owners only).
        schema:
          $ref: '#/components/schemas/Uuid'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ExpiringAccessPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - kind: membership
                      expires_at: '2026-10-12T09:30:00.000Z'
                      membership_id: 0192f4a0-7b1c-7b22-8d3e-4f5a6b7c8d9e
                      group_id: 0192f4a0-7b1c-7e44-9a5b-6c7d8e9f0a1b
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bd
                    labels:
                      principals:
                      - id: 0192f4a0-7b1c-7f89-8d6b-6960453241bd
                        kind: user
                        handle: sam
                        display_name: Sam
                      groups:
                      - id: 0192f4a0-7b1c-7e44-9a5b-6c7d8e9f0a1b
                        name: Support
                      spaces: []
                      channels: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /instance:
    get:
      operationId: getInstance
      summary: Instance settings for clients
      tags: [Admin]
      x-authz-action: instance.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InstanceInfo'
              examples:
                default:
                  summary: Response
                  value:
                    instance_id: 0192f4a0-7b1c-7db7-89b9-2fe4c48dcb3e
                    branding:
                      product_name: Team Chat
                      accent_color: loch
                      version: 3
                    locale_default: de
                    spec_version: 1.0.0
                    features:
                    - key: dm.enabled
                      enabled: true
                    limits:
                      message_max_content_bytes: 262144
                      message_max_attachments: 10
                      file_max_upload_bytes: 104857600
                      thread_title_max_chars: 200
                      voice_max_duration_ms: 900000
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /instance/settings:
    get:
      operationId: listInstanceSettings
      summary: List all instance settings
      tags: [Admin]
      x-authz-action: instance.settings.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InstanceSettingList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - key: locale.default
                      value: true
                      version: 3
                      updated_at: '2026-10-10T09:30:00.000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /instance/settings/{key}:
    put:
      operationId: putInstanceSetting
      summary: Set an instance setting
      tags: [Admin]
      x-authz-action: instance.settings.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/KeyPath'
      - $ref: '#/components/parameters/IfMatchOptional'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InstanceSettingPut'
            examples:
              default:
                summary: Request
                value:
                  value: true
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InstanceSetting'
              examples:
                default:
                  summary: Response
                  value:
                    key: locale.default
                    value: true
                    version: 3
                    updated_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /instance/branding:
    put:
      operationId: updateBranding
      summary: Change branding
      tags: [Admin]
      x-authz-action: instance.branding.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BrandingUpdate'
            examples:
              default:
                summary: Request
                value:
                  product_name: Team Chat
                  accent_color: loch
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Branding'
              examples:
                default:
                  summary: Response
                  value:
                    product_name: Team Chat
                    accent_color: loch
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /instance/agent-billing-mode:
    put:
      operationId: setAllAgentsBillingMode
      summary: Switch the backend policy of all agents
      tags: [Agents]
      x-authz-action: agent.billing_mode.set_all
      x-rate-limit-class: rl.admin
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BillingModeSetAll'
            examples:
              default:
                summary: Request
                value:
                  billing_mode: organization
                  reason: Switch all agents to the company key
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BillingModeSetAllResult'
              examples:
                default:
                  summary: Response
                  value:
                    changed_agent_ids: [0192f4a0-7b1c-7d65-8e06-d83d1842d15e]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /feature-flags:
    get:
      operationId: listFeatureFlags
      summary: List feature flags
      tags: [Admin]
      x-authz-action: feature_flag.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeatureFlagList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - key: voice.enabled
                      kind: option
                      description: Voice messages with transcription
                      default: false
                      module: files
                      orphaned: false
                      enabled: true
                      rollout: {}
                      version: 3
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /feature-flags/{key}:
    put:
      operationId: putFeatureFlag
      summary: Set a feature flag
      tags: [Admin]
      x-authz-action: feature_flag.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/KeyPath'
      - $ref: '#/components/parameters/IfMatchOptional'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FeatureFlagPut'
            examples:
              default:
                summary: Request
                value:
                  enabled: true
                  reason: Pilot with the support team
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FeatureFlag'
              examples:
                default:
                  summary: Response
                  value:
                    key: voice.enabled
                    kind: option
                    description: Voice messages with transcription
                    default: false
                    module: files
                    orphaned: false
                    enabled: true
                    rollout: {}
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /signing-keys/rotate:
    post:
      operationId: rotateSigningKey
      summary: Rotate a server signing key
      tags: [Admin]
      x-authz-action: signing_key.rotate
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SigningKeyRotate'
            examples:
              default:
                summary: Request
                value:
                  purpose: access_token
                  mode: scheduled
                  reason: Scheduled rotation
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SigningKeyRotation'
              examples:
                default:
                  summary: Response
                  value:
                    purpose: access_token
                    new_kid: inv-2026-11
                    previous_kid: inv-2026-10
                    previous_not_after: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /identity-providers:
    get:
      operationId: listIdentityProviders
      summary: List identity providers
      tags: [Admin]
      x-authz-action: identity_provider.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentityProviderList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7e26-89e1-d4e246452fb8
                      kind: oidc
                      name: Company SSO
                      config: {}
                      enabled: true
                      allow_provisioning: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createIdentityProvider
      summary: Add an identity provider
      tags: [Admin]
      description: 'Adds an identity provider (08, 2.6). A `trusted_header` provider that would be enabled (the default) while the public listener is not bound to a loopback address is refused with `409 conflict`, `reason: trusted_header_requires_loopback` and a `hint` naming `server.listen_addr` (08, 2.4 rule 1).'
      x-authz-action: identity_provider.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentityProviderCreate'
            examples:
              default:
                summary: Request
                value:
                  kind: oidc
                  name: Company SSO
                  config: {}
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentityProvider'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7e26-89e1-d4e246452fb8
                    kind: oidc
                    name: Company SSO
                    config: {}
                    enabled: true
                    allow_provisioning: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /identity-providers/{provider_id}:
    get:
      operationId: getIdentityProvider
      summary: Read an identity provider
      tags: [Admin]
      x-authz-action: identity_provider.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ProviderIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentityProvider'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7e26-89e1-d4e246452fb8
                    kind: oidc
                    name: Company SSO
                    config: {}
                    enabled: true
                    allow_provisioning: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: updateIdentityProvider
      summary: Change an identity provider
      tags: [Admin]
      description: 'Changes an identity provider; absent members stay. Setting `enabled` to true on a `trusted_header` provider while the public listener is not bound to a loopback address is refused with `409 conflict`, `reason: trusted_header_requires_loopback` and a `hint` naming `server.listen_addr` (08, 2.4 rule 1). Disabling a provider ends the sessions of its identities (08, 3.3).'
      x-authz-action: identity_provider.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ProviderIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IdentityProviderUpdate'
            examples:
              default:
                summary: Request
                value: {}
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IdentityProvider'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7e26-89e1-d4e246452fb8
                    kind: oidc
                    name: Company SSO
                    config: {}
                    enabled: true
                    allow_provisioning: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteIdentityProvider
      summary: Remove an identity provider
      tags: [Admin]
      description: Fails with `conflict` while identities are linked; disable it instead.
      x-authz-action: identity_provider.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ProviderIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /spaces:
    get:
      operationId: listSpaces
      summary: List visible spaces
      tags: [Spaces]
      x-authz-action: space.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpaceViewList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - space:
                        id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                        name: Support
                        sort_key: a0
                        version: 3
                        created_at: '2026-10-10T09:30:00.000Z'
                      level: read
                      capabilities: [approve_actions]
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createSpace
      summary: Create a space
      tags: [Spaces]
      x-authz-action: space.create
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpaceCreate'
            examples:
              default:
                summary: Request
                value:
                  name: Research
                  icon: flask
                  color: heather
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Space'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    name: Support
                    icon: headset
                    color: moss
                    sort_key: a0
                    version: 3
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /spaces/{space_id}:
    get:
      operationId: getSpace
      summary: Read a space
      tags: [Spaces]
      x-authz-action: space.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/SpaceIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SpaceView'
              examples:
                default:
                  summary: Response
                  value:
                    space:
                      id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                      name: Support
                      icon: headset
                      color: moss
                      sort_key: a0
                      version: 3
                      created_at: '2026-10-10T09:30:00.000Z'
                    level: read
                    capabilities: [approve_actions]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: updateSpace
      summary: Change name, icon or color
      tags: [Spaces]
      x-authz-action: space.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/SpaceIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpaceUpdate'
            examples:
              default:
                summary: Request
                value:
                  name: Research
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Space'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    name: Support
                    icon: headset
                    color: moss
                    sort_key: a0
                    version: 3
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /spaces/{space_id}/move:
    post:
      operationId: moveSpace
      summary: Change the global position of a space
      tags: [Spaces]
      x-authz-action: space.reorder
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/SpaceIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MovePosition'
            examples:
              default:
                summary: Request
                value: {}
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Space'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    name: Support
                    icon: headset
                    color: moss
                    sort_key: a0
                    version: 3
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /spaces/{space_id}/archive:
    post:
      operationId: archiveSpace
      summary: Archive a space
      tags: [Spaces]
      description: 'Final; there is no unarchive. Every channel of the space becomes read-only for everyone, owners included; reading stays for those who could read before. Archiving is a permission change with `access.changed`; archived spaces leave `listSpaces` (07, 4.5).'
      x-authz-action: space.archive
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/SpaceIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Left the team
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Space'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    name: Support
                    icon: headset
                    color: moss
                    sort_key: a0
                    version: 3
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /spaces/{space_id}/operator:
    put:
      operationId: setSpaceOperator
      summary: Set the default operator of a space
      description: 'Optional `resolve_i1` runs the guided flow (05 rule 1.6, 10 section 11): the listed agents are authorized with `agent.billing_mode.set` and switched to `organization` first, in the same transaction as the change; I1 is checked before the commit, and either both parts commit or none.'
      tags: [Spaces]
      x-authz-action: space.operator.set
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/SpaceIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OperatorSet'
            examples:
              default:
                summary: Request
                value:
                  mode: inherit
                  reason: Support agent takes over
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Space'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    name: Support
                    icon: headset
                    color: moss
                    sort_key: a0
                    version: 3
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/sidebar:
    get:
      operationId: getMySidebar
      summary: Read own sidebar state
      tags: [Preferences]
      x-authz-action: settings.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SidebarStateList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                      collapsed: false
                      sort_key: a0V
                      version: 3
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/sidebar/spaces/{space_id}:
    put:
      operationId: putMySidebarSpace
      summary: Collapse or reorder a space for oneself
      tags: [Preferences]
      x-authz-action: sidebar.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/SpaceIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SidebarStatePut'
            examples:
              default:
                summary: Request
                value:
                  collapsed: false
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SidebarState'
              examples:
                default:
                  summary: Response
                  value:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    collapsed: false
                    sort_key: a0V
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /spaces/{space_id}/channels:
    get:
      operationId: listSpaceChannels
      summary: List visible channels of a space
      tags: [Channels]
      x-authz-action: channel.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/SpaceIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelViewList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - channel:
                        id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                        kind: text
                        name: tickets
                        topic: Customer tickets
                        sort_key: a1
                        read_only: false
                        operator:
                          mode: inherit
                        version: 3
                        settings_version: 1
                        created_at: '2026-10-10T09:30:00.000Z'
                      level: read
                      capabilities: [approve_actions]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createChannel
      summary: Create a channel
      tags: [Channels]
      x-authz-action: channel.create
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/SpaceIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelCreate'
            examples:
              default:
                summary: Request
                value:
                  kind: text
                  name: tickets
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: text
                    name: tickets
                    topic: Customer tickets
                    sort_key: a1
                    read_only: false
                    operator:
                      mode: inherit
                    version: 3
                    settings_version: 1
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}:
    get:
      operationId: getChannel
      summary: Read a channel
      tags: [Channels]
      x-authz-action: channel.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelView'
              examples:
                default:
                  summary: Response
                  value:
                    channel:
                      id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: text
                      name: tickets
                      topic: Customer tickets
                      sort_key: a1
                      read_only: false
                      operator:
                        mode: inherit
                      version: 3
                      settings_version: 1
                      created_at: '2026-10-10T09:30:00.000Z'
                    level: read
                    capabilities: [approve_actions]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: updateChannel
      summary: Change name or topic
      tags: [Channels]
      x-authz-action: channel.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelUpdate'
            examples:
              default:
                summary: Request
                value:
                  name: tickets
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: text
                    name: tickets
                    topic: Customer tickets
                    sort_key: a1
                    read_only: false
                    operator:
                      mode: inherit
                    version: 3
                    settings_version: 1
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/move:
    post:
      operationId: moveChannel
      summary: Change the position of a channel in its space
      tags: [Channels]
      x-authz-action: channel.reorder
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MovePosition'
            examples:
              default:
                summary: Request
                value: {}
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: text
                    name: tickets
                    topic: Customer tickets
                    sort_key: a1
                    read_only: false
                    operator:
                      mode: inherit
                    version: 3
                    settings_version: 1
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/archive:
    post:
      operationId: archiveChannel
      summary: Archive a channel
      tags: [Channels]
      description: 'Final; there is no unarchive. The channel becomes read-only for everyone, owners included (`403 channel_read_only` with `reason: channel_archived`); reading stays for those who could read before. Archiving is a permission change with `access.changed`; archived channels leave `listSpaceChannels` (07, 4.5).'
      x-authz-action: channel.archive
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Left the team
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: text
                    name: tickets
                    topic: Customer tickets
                    sort_key: a1
                    read_only: false
                    operator:
                      mode: inherit
                    version: 3
                    settings_version: 1
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/behavior:
    get:
      operationId: getChannelBehavior
      summary: Read prompt, skills and mention rules
      tags: [Channels]
      description: The ETag is `settings_version`.
      x-authz-action: channel.behavior.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelBehavior'
              examples:
                default:
                  summary: Response
                  value:
                    require_mention: false
                    thread_require_mention: false
                    free_response: false
                    auto_thread: false
                    allow_mentioned_agents: false
                    history_backfill_limit: 20
                    prompt: Answer support tickets politely. Ask before closing a ticket.
                    skills: [support-triage]
                    settings_version: 1
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    put:
      operationId: putChannelBehavior
      summary: Change prompt, skills and mention rules
      tags: [Channels]
      x-authz-action: channel.behavior.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelBehaviorPut'
            examples:
              default:
                summary: Request
                value:
                  require_mention: false
                  thread_require_mention: false
                  free_response: false
                  auto_thread: false
                  allow_mentioned_agents: false
                  history_backfill_limit: 20
                  prompt: Answer support tickets politely.
                  skills: [support-triage]
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelBehavior'
              examples:
                default:
                  summary: Response
                  value:
                    require_mention: false
                    thread_require_mention: false
                    free_response: false
                    auto_thread: false
                    allow_mentioned_agents: false
                    history_backfill_limit: 20
                    prompt: Answer support tickets politely. Ask before closing a ticket.
                    skills: [support-triage]
                    settings_version: 1
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/operator:
    put:
      operationId: setChannelOperator
      summary: Set or inherit the operator of a channel
      description: 'Optional `resolve_i1` runs the guided flow (05 rule 1.6, 10 section 11): the listed agents are authorized with `agent.billing_mode.set` and switched to `organization` first, in the same transaction as the change; I1 is checked before the commit, and either both parts commit or none.'
      tags: [Channels]
      x-authz-action: channel.operator.set
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OperatorSet'
            examples:
              default:
                summary: Request
                value:
                  mode: inherit
                  reason: Support agent takes over
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: text
                    name: tickets
                    topic: Customer tickets
                    sort_key: a1
                    read_only: false
                    operator:
                      mode: inherit
                    version: 3
                    settings_version: 1
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/read-only:
    put:
      operationId: setChannelReadOnly
      summary: Switch the read-only state of a channel
      tags: [Channels]
      description: |-
        Write-protected for every principal including owners; only a running import writes (05, rule 1.8). Switching on cancels active runs in the channel.
        The imported channel of a source that is neither `completed` nor `failed` is controlled by its ownership list only: `409 conflict` with `reason` `import_lead_controls` (use `setImportLead`).
      x-authz-action: channel.read_only.set
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelReadOnlySet'
            examples:
              default:
                summary: Request
                value:
                  read_only: true
                  reason: Channel is mirrored from the old system
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: text
                    name: tickets
                    topic: Customer tickets
                    sort_key: a1
                    read_only: true
                    operator:
                      mode: inherit
                    version: 4
                    settings_version: 1
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/project-binding:
    put:
      operationId: putProjectBinding
      summary: Bind a forum to a project
      tags: [Channels]
      x-authz-action: channel.project.bind
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IfMatchOptional'
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/ProjectBindingPut'}
            examples:
              default:
                summary: Request
                value:
                  workspace_ref: website
                  repository_url: https://git.example.com/team/website
                  default_branch: main
                  reason: Website project moves to this forum
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id: {$ref: '#/components/headers/Correlation-Id'}
            RateLimit: {$ref: '#/components/headers/RateLimit'}
            RateLimit-Policy: {$ref: '#/components/headers/RateLimit-Policy'}
            ETag: {$ref: '#/components/headers/ETag'}
          content:
            application/json:
              schema: {$ref: '#/components/schemas/ProjectBinding'}
              examples:
                default:
                  summary: Response
                  value:
                    workspace_ref: website
                    repository_url: https://git.example.com/team/website
                    default_branch: main
                    runner_config: {}
                    ready: false
                    version: 3
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
        '412': {$ref: '#/components/responses/PreconditionFailed'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        default: {$ref: '#/components/responses/Error'}
    delete:
      operationId: deleteProjectBinding
      summary: Remove the project binding
      tags: [Channels]
      x-authz-action: channel.project.bind
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id: {$ref: '#/components/headers/Correlation-Id'}
            RateLimit: {$ref: '#/components/headers/RateLimit'}
            RateLimit-Policy: {$ref: '#/components/headers/RateLimit-Policy'}
        '400': {$ref: '#/components/responses/BadRequest'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '409': {$ref: '#/components/responses/Conflict'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
        default: {$ref: '#/components/responses/Error'}

  /channels/{channel_id}/tags:
    get:
      operationId: listForumTags
      summary: List forum tags
      tags: [Channels]
      x-authz-action: channel.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ForumTagList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7f24-8f76-a5de0ed16c6f
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      name: bug
                      color: rowan
                      sort_key: a0
                      restricted: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createForumTag
      summary: Create a forum tag
      tags: [Channels]
      x-authz-action: forum_tag.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForumTagCreate'
            examples:
              default:
                summary: Request
                value:
                  name: bug
                  color: rowan
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ForumTag'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7f24-8f76-a5de0ed16c6f
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    name: bug
                    color: rowan
                    sort_key: a0
                    restricted: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/tags/{tag_id}:
    patch:
      operationId: updateForumTag
      summary: Change a forum tag
      tags: [Channels]
      x-authz-action: forum_tag.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/TagIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ForumTagUpdate'
            examples:
              default:
                summary: Request
                value:
                  name: bug
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ForumTag'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7f24-8f76-a5de0ed16c6f
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    name: bug
                    color: rowan
                    sort_key: a0
                    restricted: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteForumTag
      summary: Delete a forum tag
      tags: [Channels]
      x-authz-action: forum_tag.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/TagIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/tags/{tag_id}/move:
    post:
      operationId: moveForumTag
      summary: Change the position of a forum tag
      tags: [Channels]
      description: Sets `sort_key` relative to sibling tags of the same forum; bumps the channel version and writes `channel.updated` like every tag change (03 `forum_tags`).
      x-authz-action: forum_tag.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/TagIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MovePosition'
            examples:
              default:
                summary: Request
                value: {}
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ForumTag'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7f24-8f76-a5de0ed16c6f
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    name: bug
                    color: rowan
                    sort_key: a0
                    restricted: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/templates:
    get:
      operationId: listTemplates
      summary: List own and channel templates
      tags: [Channels]
      x-authz-action: template.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-777d-8541-d21a5b1c8fe4
                      scope: personal
                      name: Escalation
                      content_md: Please escalate this ticket to second level.
                      version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createChannelTemplate
      summary: Create a channel template
      tags: [Channels]
      x-authz-action: template.manage
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateCreate'
            examples:
              default:
                summary: Request
                value:
                  name: Escalation
                  content_md: Please escalate this ticket to second level.
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Template'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-777d-8541-d21a5b1c8fe4
                    scope: personal
                    name: Escalation
                    content_md: Please escalate this ticket to second level.
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /templates/{template_id}:
    patch:
      operationId: updateTemplate
      summary: Change a template
      tags: [Channels]
      x-authz-action: template.manage
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/TemplateIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplateUpdate'
            examples:
              default:
                summary: Request
                value: {}
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Template'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-777d-8541-d21a5b1c8fe4
                    scope: personal
                    name: Escalation
                    content_md: Please escalate this ticket to second level.
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteTemplate
      summary: Delete a template
      tags: [Channels]
      x-authz-action: template.manage
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/TemplateIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/export:
    get:
      operationId: exportChannel
      summary: Export the history of a channel
      tags: [Privacy]
      description: |-
        Streams one `Message` per line in `thread_seq` order per thread. Requires step-up.

        `from` must be strictly before `to`, else `400 validation_failed` at `#query/to` with code
        `invalid`; the range has no upper bound, so clients split long ranges. Each line carries the
        full `content_md` without `content_truncated` and without `my_reactions`; deleted messages
        appear as tombstones at their `thread_seq`. Threads come ascending by id, the messages of a
        thread ascending by `thread_seq`. Read rights are checked again before every batch, so a
        revocation ends the stream. An export cut after the first byte ends without a complete
        response (no final chunk); a range without messages gives `200` with an empty body.
      x-authz-action: export.channel
      x-rate-limit-class: rl.export
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - name: from
        in: query
        description: Start of the range (inclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: to
        in: query
        description: End of the range (exclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      responses:
        '200':
          description: OK. One JSON object per line (`application/x-ndjson`).
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/x-ndjson:
              schema:
                $ref: '#/components/schemas/Message'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/dms:
    get:
      operationId: listMyDms
      summary: List own direct message channels
      tags: [Channels]
      x-authz-action: channel.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ChannelViewList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - channel:
                        id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                        kind: text
                        name: tickets
                        topic: Customer tickets
                        sort_key: a1
                        read_only: false
                        operator:
                          mode: inherit
                        version: 3
                        settings_version: 1
                        created_at: '2026-10-10T09:30:00.000Z'
                      level: read
                      capabilities: [approve_actions]
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /dms:
    post:
      operationId: createDm
      summary: Open a direct message channel
      tags: [Channels]
      description: |-
        Returns the existing channel with 200 when the same participants already have one. 403
        `feature_disabled` when `dm.enabled` is off.
      x-authz-action: dm.create
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DmCreate'
            examples:
              default:
                summary: Request
                value:
                  participant_ids: [0192f4a0-7b1c-7324-80b6-deddeed437e3]
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Channel'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: text
                    name: tickets
                    topic: Customer tickets
                    sort_key: a1
                    read_only: false
                    operator:
                      mode: inherit
                    version: 3
                    settings_version: 1
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/leave:
    post:
      operationId: leaveDm
      summary: Leave a direct message channel
      tags: [Channels]
      x-authz-action: dm.leave
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/incoming-webhooks:
    get:
      operationId: listIncomingWebhooks
      summary: List incoming webhooks of a channel
      tags: [Webhooks]
      x-authz-action: webhook_in.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IncomingWebhookList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-784b-80fa-070d5a412eea
                      app_id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      name: Monitoring alerts
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createIncomingWebhook
      summary: Create an incoming webhook
      tags: [Webhooks]
      x-authz-action: webhook_in.create
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IncomingWebhookCreate'
            examples:
              default:
                summary: Request
                value:
                  app_id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                  name: Monitoring alerts
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IncomingWebhookCreated'
              examples:
                default:
                  summary: Response
                  value:
                    webhook:
                      id: 0192f4a0-7b1c-784b-80fa-070d5a412eea
                      app_id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      name: Monitoring alerts
                      created_at: '2026-10-10T09:30:00.000Z'
                    url: https://chat.example.com/api/v1/hooks/0192f4a0-7b1c-784b-80fa-070d5a412eea/bothy_whi_EXAMPLE000000000000000000000000000000000000_2WwD3e
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /incoming-webhooks/{webhook_id}:
    delete:
      operationId: revokeIncomingWebhook
      summary: Revoke an incoming webhook
      tags: [Webhooks]
      x-authz-action: webhook_in.revoke
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/WebhookIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /hooks/{webhook_id}:
    post:
      operationId: postIncomingWebhookWithBearer
      summary: Post a message through an incoming webhook with the token in Authorization
      tags: [Webhooks]
      description: |-
        The token of the incoming webhook is sent as `Authorization: Bearer <token>`; the path ends with the webhook ID. Use this variant where the sender can set a header, so the token never appears in a URL. Of the root security only `bearerAuth` applies here, with the token of the incoming webhook; any other scheme or credential counts as a wrong token (`404`).

        The format is recognised per request from the body: a body with at least one Alertmanager member
        (`version`, `groupKey`, `truncatedAlerts`, `status`, `receiver`, `groupLabels`, `commonLabels`,
        `commonAnnotations`, `externalURL`, `alerts`) is the unchanged webhook payload of Prometheus
        Alertmanager and gives one message per notification; it must carry no native member (`400
        validation_failed`, `format_mixed`). Every other body is `native`: Markdown in `content_md` with
        optional `title`, `thread_id` and `client_msg_id`. A format outside `webhook_in.formats_enabled`
        gives `400 validation_failed` with `format_disabled`.

        Order of checks (11 8.4): `rl.webhook_in` of the webhook (`429`), the token (`404`), the body up to
        `webhook_in.max_body_bytes` (`413`), the format (`400`), `authorize` of the app (`404`), then the
        message (`201`). An unknown or revoked webhook, a wrong or malformed token, a suspended or
        uninstalled app and every refusal of the app's rights give the same `404`. A failed token check
        counts against `rl.public_auth` of the client IP.

        The message has `origin = app` with the app as author, resolves no mentions and never triggers an
        agent. `native` with `client_msg_id` is idempotent: a repeat answers `200` with the original and
        `Idempotent-Replay: true`, other content with the same `client_msg_id` `422 idempotency_key_reuse`.
        Senders inside the stack reach the same path on the internal listener.
      x-authz-action: webhook_in.post
      x-rate-limit-class: rl.webhook_in
      x-idempotency-exempt: client_id
      parameters:
      - $ref: '#/components/parameters/WebhookIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IncomingWebhookPost'
            examples:
              native:
                value:
                  content_md: Disk usage above threshold on build host.
                  title: Disk alert
                  client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
              alertmanager:
                value:
                  version: '4'
                  groupKey: '{}:{alertname="DiskFull"}'
                  truncatedAlerts: 0
                  status: firing
                  receiver: bothy-ops
                  groupLabels:
                    alertname: DiskFull
                  commonLabels:
                    alertname: DiskFull
                    severity: warning
                  commonAnnotations:
                    summary: Disk usage above threshold.
                  externalURL: https://alertmanager.example.com
                  alerts:
                  - status: firing
                    labels:
                      alertname: DiskFull
                      instance: build.example.com
                      severity: warning
                    annotations:
                      summary: Disk usage above threshold.
                    startsAt: '2026-10-10T09:30:00.000Z'
                    endsAt: '0001-01-01T00:00:00Z'
                    generatorURL: https://prometheus.example.com/graph
                    fingerprint: 4a1f0c2b9d3e5f60
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IncomingWebhookResult'
              examples:
                default:
                  value:
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
        '200':
          description: 'Replay: the original created by an earlier `native` request with the same `client_msg_id`.'
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            Idempotent-Replay:
              $ref: '#/components/headers/Idempotent-Replay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IncomingWebhookResult'
              examples:
                default:
                  value:
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /hooks/{webhook_id}/{token}:
    post:
      operationId: postIncomingWebhook
      summary: Post a message through an incoming webhook
      tags: [Webhooks]
      description: |-
        The token is the last path segment; `Authorization` is ignored. Servers write the path only as its pattern `/api/v1/hooks/{webhook_id}/{token}` in logs, traces and `Problem.instance`, and reverse proxies must redact it (16 4.3).

        The format is recognised per request from the body: a body with at least one Alertmanager member
        (`version`, `groupKey`, `truncatedAlerts`, `status`, `receiver`, `groupLabels`, `commonLabels`,
        `commonAnnotations`, `externalURL`, `alerts`) is the unchanged webhook payload of Prometheus
        Alertmanager and gives one message per notification; it must carry no native member (`400
        validation_failed`, `format_mixed`). Every other body is `native`: Markdown in `content_md` with
        optional `title`, `thread_id` and `client_msg_id`. A format outside `webhook_in.formats_enabled`
        gives `400 validation_failed` with `format_disabled`.

        Order of checks (11 8.4): `rl.webhook_in` of the webhook (`429`), the token (`404`), the body up to
        `webhook_in.max_body_bytes` (`413`), the format (`400`), `authorize` of the app (`404`), then the
        message (`201`). An unknown or revoked webhook, a wrong or malformed token, a suspended or
        uninstalled app and every refusal of the app's rights give the same `404`. A failed token check
        counts against `rl.public_auth` of the client IP.

        The message has `origin = app` with the app as author, resolves no mentions and never triggers an
        agent. `native` with `client_msg_id` is idempotent: a repeat answers `200` with the original and
        `Idempotent-Replay: true`, other content with the same `client_msg_id` `422 idempotency_key_reuse`.
        Senders inside the stack reach the same path on the internal listener.
      x-authz-action: webhook_in.post
      x-rate-limit-class: rl.webhook_in
      x-idempotency-exempt: client_id
      parameters:
      - $ref: '#/components/parameters/WebhookIdPath'
      - $ref: '#/components/parameters/TokenPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IncomingWebhookPost'
            examples:
              native:
                value:
                  content_md: Disk usage above threshold on build host.
                  title: Disk alert
                  client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
              alertmanager:
                value:
                  version: '4'
                  groupKey: '{}:{alertname="DiskFull"}'
                  truncatedAlerts: 0
                  status: firing
                  receiver: bothy-ops
                  groupLabels:
                    alertname: DiskFull
                  commonLabels:
                    alertname: DiskFull
                    severity: warning
                  commonAnnotations:
                    summary: Disk usage above threshold.
                  externalURL: https://alertmanager.example.com
                  alerts:
                  - status: firing
                    labels:
                      alertname: DiskFull
                      instance: build.example.com
                      severity: warning
                    annotations:
                      summary: Disk usage above threshold.
                    startsAt: '2026-10-10T09:30:00.000Z'
                    endsAt: '0001-01-01T00:00:00Z'
                    generatorURL: https://prometheus.example.com/graph
                    fingerprint: 4a1f0c2b9d3e5f60
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IncomingWebhookResult'
              examples:
                default:
                  summary: Response
                  value:
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
        '200':
          description: 'Replay: the original created by an earlier `native` request with the same `client_msg_id`.'
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            Idempotent-Replay:
              $ref: '#/components/headers/Idempotent-Replay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IncomingWebhookResult'
              examples:
                default:
                  value:
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
        '400':
          $ref: '#/components/responses/BadRequest'
        '404':
          $ref: '#/components/responses/NotFound'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /channels/{channel_id}/threads:
    get:
      operationId: listChannelThreads
      summary: List threads or forum posts of a channel
      tags: [Threads]
      x-authz-action: thread.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: state
        in: query
        description: Filter by state.
        schema:
          $ref: '#/components/schemas/ThreadState'
      - name: tag_id
        in: query
        description: Only threads with this tag.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: sort
        in: query
        description: Sort order, newest first.
        schema:
          $ref: '#/components/schemas/ForumSort'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: thread
                      title: Invoice export fails
                      title_source: auto
                      state: open
                      last_message_seq: 5301
                      last_activity_at: '2026-10-10T09:30:00.000Z'
                      message_count: 1
                      created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3
                      tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createThread
      summary: Create a forum post or a thread from a message
      tags: [Threads]
      description: A `first_message` uses its `client_msg_id` for idempotency; without it, send `Idempotency-Key`. A `first_message` with `kind = voice` follows the rules of `createMessage` (pointers with the prefix `/first_message`); a voice file whose upload or scan has not ended answers `409 file_not_ready`, a blocked or failed file `409 file_blocked`, and neither thread nor message is stored.
      x-authz-action: thread.create
      x-rate-limit-class: rl.message
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadCreate'
            examples:
              default:
                summary: Request
                value:
                  title: Invoice export fails
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadCreated'
              examples:
                default:
                  summary: Response
                  value:
                    thread:
                      id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: thread
                      title: Invoice export fails
                      title_source: auto
                      state: open
                      last_message_seq: 5301
                      last_activity_at: '2026-10-10T09:30:00.000Z'
                      message_count: 1
                      created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3
                      tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
        '200':
          description: 'Replay: the thread created by an earlier request with the same `first_message.client_msg_id` or `Idempotency-Key`.'
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            Idempotent-Replay:
              $ref: '#/components/headers/Idempotent-Replay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadCreated'
              examples:
                default:
                  summary: Response
                  value:
                    thread:
                      id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: thread
                      title: Invoice export fails
                      title_source: auto
                      state: open
                      last_message_seq: 5301
                      last_activity_at: '2026-10-10T09:30:00.000Z'
                      message_count: 1
                      created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3
                      tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}:
    get:
      operationId: getThread
      summary: Read a thread
      tags: [Threads]
      x-authz-action: thread.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Thread'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: thread
                    title: Invoice export fails
                    title_source: auto
                    state: open
                    last_message_seq: 5301
                    last_activity_at: '2026-10-10T09:30:00.000Z'
                    message_count: 1
                    created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
                    tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/title:
    put:
      operationId: setThreadTitle
      summary: Set the title
      tags: [Threads]
      description: Every principal except the thread's operator agent sets `title_source = manual`. The operator agent may only replace an `auto` title, which stays `auto`; on a `manual` title it gets `409 title_manual`.
      x-authz-action: thread.title.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadTitlePut'
            examples:
              default:
                summary: Request
                value:
                  title: Invoice export fails for March
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Thread'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: thread
                    title: Invoice export fails
                    title_source: auto
                    state: open
                    last_message_seq: 5301
                    last_activity_at: '2026-10-10T09:30:00.000Z'
                    message_count: 1
                    created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
                    tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/close:
    post:
      operationId: closeThread
      summary: Mark a thread as done
      tags: [Threads]
      x-authz-action: thread.close
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Thread'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: thread
                    title: Invoice export fails
                    title_source: auto
                    state: open
                    last_message_seq: 5301
                    last_activity_at: '2026-10-10T09:30:00.000Z'
                    message_count: 1
                    created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
                    tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/reopen:
    post:
      operationId: reopenThread
      summary: Reopen a thread
      tags: [Threads]
      x-authz-action: thread.reopen
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Thread'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: thread
                    title: Invoice export fails
                    title_source: auto
                    state: open
                    last_message_seq: 5301
                    last_activity_at: '2026-10-10T09:30:00.000Z'
                    message_count: 1
                    created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
                    tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/move:
    post:
      operationId: moveThread
      summary: Move a thread to another channel
      tags: [Threads]
      description: Only to channels with the same effective operator; otherwise `operator_mismatch`.
      x-authz-action: thread.move
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadMove'
            examples:
              default:
                summary: Request
                value:
                  target_channel_id: 0192f4a0-7b1c-77d1-8cf4-a695a7985d2f
                  reason: Belongs to billing
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Thread'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-77d1-8cf4-a695a7985d2f
                    kind: thread
                    title: Invoice export fails
                    title_source: auto
                    state: open
                    last_message_seq: 5301
                    last_activity_at: '2026-10-10T09:30:00.000Z'
                    message_count: 1
                    created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
                    moved_at: '2026-10-10T09:30:00.000Z'
                    tag_ids: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/tags:
    put:
      operationId: setThreadTags
      summary: Set forum tags of a thread
      tags: [Threads]
      x-authz-action: thread.tags.set
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - $ref: '#/components/parameters/IfMatchOptional'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadTagsPut'
            examples:
              default:
                summary: Request
                value:
                  tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Thread'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    kind: thread
                    title: Invoice export fails
                    title_source: auto
                    state: open
                    last_message_seq: 5301
                    last_activity_at: '2026-10-10T09:30:00.000Z'
                    message_count: 1
                    created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
                    tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/agent-sessions:
    get:
      operationId: listAgentSessions
      summary: List current and earlier agent sessions
      tags: [Threads]
      x-authz-action: agent_session.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentSessionList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-741b-8617-885056753dc0
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      external_session_ref: sess-7f3a
                      is_current: true
                      started: true
                      model: model-large
                      cost:
                        amount_micros: 1250000
                        currency: USD
                        is_estimate: true
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/agent-sessions/reset:
    post:
      operationId: resetAgentSession
      summary: Start a new agent session in the thread
      description: Ends the current session of the thread with `end_reason = reset` and returns the ended row; the next run gets `session_mode = new` (09, 9.2). A run in progress keeps its session. Without a current session 409 `conflict` with `reason` `no_current_session`.
      tags: [Threads]
      x-authz-action: agent_session.reset
      x-rate-limit-class: rl.trigger
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentSession'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-741b-8617-885056753dc0
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    external_session_ref: sess-7f3a
                    is_current: false
                    started: true
                    model: model-large
                    cost:
                      amount_micros: 1250000
                      currency: USD
                      is_estimate: true
                    created_at: '2026-10-10T09:30:00.000Z'
                    ended_at: '2026-10-10T10:05:00.000Z'
                    end_reason: reset
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/agent-sessions/bind:
    post:
      operationId: bindAgentSession
      summary: Bind an existing external session
      description: Compare-and-swap over the current session of the thread (09, 9.4); nothing changes on a 409. `reason` `session_changed` when `expected_current_session_id` does not name the current session, `session_bound_elsewhere` when the reference is current in another thread without `take_over`. With `take_over` the permission is checked in that thread too. The next run gets `session_mode = bind`.
      tags: [Threads]
      x-authz-action: agent_session.bind
      x-rate-limit-class: rl.trigger
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentSessionBind'
            examples:
              default:
                summary: Request
                value:
                  external_session_ref: sess-7f3a
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentSession'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-741b-8617-885056753dc0
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    external_session_ref: sess-7f3a
                    is_current: true
                    started: true
                    model: model-large
                    cost:
                      amount_micros: 1250000
                      currency: USD
                      is_estimate: true
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/agent-sessions/report:
    post:
      operationId: reportAgentSession
      summary: Report the external session reference and model
      description: 'The adapter reports the session of its own active run in the thread (09, 9.1 and 9.5). `If-Match` is the compare-and-swap over `agent_sessions.version`: absent means the run has no session row yet, and the report creates it; a row that exists needs its version, otherwise 412 `version_conflict` with `current`. 409 `conflict` with `reason` `session_bound_elsewhere` (the reference is current in another thread), `session_changed` (the thread got another current session) or `session_not_bound` (`bind_failed` without a started session row).'
      tags: [Agent runtime]
      x-authz-action: agent_session.report
      x-rate-limit-class: rl.agent_runtime
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchOptional'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentSessionReport'
            examples:
              default:
                summary: Request
                value:
                  external_session_ref: sess-7f3a
                  model: model-large
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentSession'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-741b-8617-885056753dc0
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    external_session_ref: sess-7f3a
                    is_current: true
                    started: true
                    model: model-large
                    cost:
                      amount_micros: 1250000
                      currency: USD
                      is_estimate: true
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/runs:
    get:
      operationId: listThreadRuns
      summary: List runs of a thread
      tags: [Runs]
      x-authz-action: run.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunSummaryPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-767f-8292-c1b14d7d40df
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      triggered_by: 0192f4a0-7b1c-7db7-8afc-99f48568ad99
                      state: queued
                      state_reason: user_cancelled
                      version: 3
                      billing_mode_expected: personal
                      status_line: Reading the export logs
                      summary: Found the cause and proposed a fix
                      cost:
                        amount_micros: 1250000
                        currency: USD
                        is_estimate: true
                      step_count: 1
                      queued_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/costs:
    get:
      operationId: getThreadCosts
      summary: Costs of a thread per run
      description: 'Valid cost entries of the thread (entries no correction supersedes), whatever channel they were booked in, per run (by `run_id` ascending, unpaginated) and summed per billing mode and currency without conversion. `tokens` are always present; amounts of runs in mode `personal` (their `costs` and their totals) appear only when the server key `billing.show_personal_estimates` is on (10, 12.2). Never a person: costs stand per run (20, 9). Without read on the thread the same 404 as for an unknown thread. A thread without entries answers empty lists.'
      tags: [Stats]
      x-authz-action: cost.read.thread
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CostBreakdown'
              examples:
                default:
                  summary: Response
                  value:
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    totals:
                    - billing_mode: organization
                      amount_micros: 1250000
                      currency: USD
                      is_estimate: true
                    runs:
                    - run_id: 0192f4a0-7b1c-70cd-8013-27d7ac09a77f
                      billing_mode: organization
                      agent_session_id: 0192f4a0-7b1c-7000-8a00-000000000501
                      tokens:
                        tokens_in: 120000
                        tokens_out: 8000
                        tokens_cache_read: 40000
                        tokens_cache_write: 10000
                        tokens_cache_write_1h: 0
                      costs:
                      - amount_micros: 1250000
                        currency: USD
                        is_estimate: true
                      unpriced: false
                    - run_id: 0192f4a0-7b1c-70cd-8013-27d7ac09a780
                      billing_mode: personal
                      tokens:
                        tokens_in: 3000
                        tokens_out: 500
                        tokens_cache_read: 0
                        tokens_cache_write: 0
                        tokens_cache_write_1h: 0
                      unpriced: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/pins:
    get:
      operationId: listPins
      summary: List pinned messages
      tags: [Messages]
      x-authz-action: thread.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PinList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      pinned_by: 0192f4a0-7b1c-72de-8fae-dfbd3e36ff9a
                      pinned_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/read-state:
    put:
      operationId: putReadState
      summary: Advance the read position
      tags: [Preferences]
      x-authz-action: read_state.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReadStatePut'
            examples:
              default:
                summary: Request
                value:
                  last_read_seq: 42
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadState'
              examples:
                default:
                  summary: Response
                  value:
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    last_read_seq: 41
                    mention_count: 1
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/read-state/mark-unread:
    post:
      operationId: markThreadUnread
      summary: Move the read position back
      tags: [Preferences]
      x-authz-action: read_state.mark_unread
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReadStatePut'
            examples:
              default:
                summary: Request
                value:
                  last_read_seq: 42
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadState'
              examples:
                default:
                  summary: Response
                  value:
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    last_read_seq: 41
                    mention_count: 1
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/draft:
    put:
      operationId: putThreadDraft
      summary: Save the draft of a thread
      tags: [Preferences]
      x-authz-action: draft.write
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftPut'
            examples:
              default:
                summary: Request
                value:
                  content_md: Draft of a reply
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DraftPutResult'
              examples:
                default:
                  summary: Response
                  value:
                    draft:
                      id: 0192f4a0-7b1c-7ad4-8297-1c4f06dd683c
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      content_md: Draft of a reply
                      file_ids: [0192f4a0-7b1c-746e-8bc6-de981880ca4c]
                      version: 3
                    overwrote_newer: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteThreadDraft
      summary: Delete the draft of a thread
      tags: [Preferences]
      x-authz-action: draft.write
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/draft:
    put:
      operationId: putChannelDraft
      summary: Save the draft of a new forum post
      tags: [Preferences]
      x-authz-action: draft.write
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/DraftPut'
            examples:
              default:
                summary: Request
                value:
                  content_md: Draft of a reply
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DraftPutResult'
              examples:
                default:
                  summary: Response
                  value:
                    draft:
                      id: 0192f4a0-7b1c-7ad4-8297-1c4f06dd683c
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      content_md: Draft of a reply
                      file_ids: [0192f4a0-7b1c-746e-8bc6-de981880ca4c]
                      version: 3
                    overwrote_newer: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteChannelDraft
      summary: Delete the draft of a new forum post
      tags: [Preferences]
      x-authz-action: draft.write
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /threads/{thread_id}/messages:
    get:
      operationId: listThreadMessages
      summary: Read thread history
      tags: [Messages]
      description: |-
        At most one of `before_seq`, `after_seq`, `around_seq`. Without any, returns the newest page.
        Gaps in `thread_seq` mean missing data, tombstones fill deleted positions.
      x-authz-action: thread.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      - name: before_seq
        in: query
        description: Messages with `thread_seq` below this value.
        schema:
          type: integer
          format: int64
      - name: after_seq
        in: query
        description: Messages with `thread_seq` above this value.
        schema:
          type: integer
          format: int64
      - name: around_seq
        in: query
        description: Messages centered on this value.
        schema:
          type: integer
          format: int64
      - name: limit
        in: query
        description: Page size.
        schema:
          $ref: '#/components/schemas/PageLimit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessagePage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      thread_seq: 42
                      author:
                        id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        kind: user
                        handle: alex
                        display_name: Alex
                      client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                      origin: human
                      kind: text
                      content_md: '@support-agent the invoice export fails since this morning.'
                      content_format: 1
                      silent: false
                      attachments:
                      - position: 1
                        file:
                          id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                          size_bytes: 48213
                          mime_type: image/png
                          original_name: screenshot.png
                          uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                          purpose: attachment
                          upload_state: complete
                          scan_status: clean
                          transcript_status: none
                          thumbnails:
                          - variant: small
                            mime_type: image/jpeg
                            width: 320
                            height: 180
                          created_at: '2026-10-10T09:30:00.000Z'
                      mention_ids: [0192f4a0-7b1c-718f-86e7-9cb8b914b848]
                      reactions:
                      - emoji: 👍
                        count: 3
                      pinned: false
                      components:
                      - id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                        custom_id: approve-refund
                        type: button
                        definition: {}
                        position: 1
                        single_use: true
                        restricted_to: any
                        state: active
                        version: 3
                      version: 3
                      created_at: '2026-10-10T09:30:00.000Z'
                    has_more_before: false
                    has_more_after: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createMessage
      summary: Send a message
      tags: [Messages]
      description: |-
        Idempotent by `(author, client_msg_id)`: a repeat with the same body returns the original with
        200 and `Idempotent-Replay: true`; with a different body 422 `idempotency_key_reuse`. Sending
        into a closed thread reopens it. Agent invocation and follow-ups are evaluated after storing;
        denial sets `trigger_state = failed` with `trigger_state_reason`. When the message triggers an
        agent while one of its attachments is not final (scan or check still running), the agent
        invocation waits until all attachments are final or until `file.invocation_hold_max` has
        passed; `trigger_state` stays `received` meanwhile. A voice message (`kind = voice`, 12 8.1)
        needs one ready audio attachment: a file whose upload or scan has not ended answers `409
        file_not_ready`, a blocked or failed file `409 file_blocked`, and nothing is stored. A voice
        message that triggers an agent without a transcription path gets `trigger_state = failed`
        with `voice_unsupported` (09, 13); with the server transcript (`voice.transcription_mode =
        service`) the invocation waits until the transcript is `done` or `failed`, at most
        `voice.transcript_hold_max`.
      x-authz-action: message.create
      x-rate-limit-class: rl.message
      x-idempotency-exempt: client_id
      parameters:
      - $ref: '#/components/parameters/ThreadIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageCreate'
            examples:
              default:
                summary: Request
                value:
                  client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                  content_md: '@support-agent the invoice export fails since this morning.'
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_seq: 42
                    author:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      kind: user
                      handle: alex
                      display_name: Alex
                    client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                    origin: human
                    kind: text
                    content_md: '@support-agent the invoice export fails since this morning.'
                    content_format: 1
                    silent: false
                    attachments:
                    - position: 1
                      file:
                        id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                        size_bytes: 48213
                        mime_type: image/png
                        original_name: screenshot.png
                        uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        purpose: attachment
                        upload_state: complete
                        scan_status: clean
                        transcript_status: none
                        thumbnails:
                        - variant: small
                          mime_type: image/jpeg
                          width: 320
                          height: 180
                        created_at: '2026-10-10T09:30:00.000Z'
                    mention_ids: [0192f4a0-7b1c-718f-86e7-9cb8b914b848]
                    reactions:
                    - emoji: 👍
                      count: 3
                    pinned: false
                    components:
                    - id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                      custom_id: approve-refund
                      type: button
                      definition: {}
                      position: 1
                      single_use: true
                      restricted_to: any
                      state: active
                      version: 3
                    version: 3
                    created_at: '2026-10-10T09:30:00.000Z'
        '200':
          description: 'Replay: the original created by an earlier request with the same `client_msg_id`.'
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            Idempotent-Replay:
              $ref: '#/components/headers/Idempotent-Replay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_seq: 42
                    author:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      kind: user
                      handle: alex
                      display_name: Alex
                    client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                    origin: human
                    kind: text
                    content_md: '@support-agent the invoice export fails since this morning.'
                    content_format: 1
                    silent: false
                    attachments:
                    - position: 1
                      file:
                        id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                        size_bytes: 48213
                        mime_type: image/png
                        original_name: screenshot.png
                        uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        purpose: attachment
                        upload_state: complete
                        scan_status: clean
                        transcript_status: none
                        thumbnails:
                        - variant: small
                          mime_type: image/jpeg
                          width: 320
                          height: 180
                        created_at: '2026-10-10T09:30:00.000Z'
                    mention_ids: [0192f4a0-7b1c-718f-86e7-9cb8b914b848]
                    reactions:
                    - emoji: 👍
                      count: 3
                    pinned: false
                    components:
                    - id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                      custom_id: approve-refund
                      type: button
                      definition: {}
                      position: 1
                      single_use: true
                      restricted_to: any
                      state: active
                      version: 3
                    version: 3
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /messages/{message_id}:
    get:
      operationId: getMessage
      summary: Read a message
      tags: [Messages]
      x-authz-action: message.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/MessageIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_seq: 42
                    author:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      kind: user
                      handle: alex
                      display_name: Alex
                    client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                    origin: human
                    kind: text
                    content_md: '@support-agent the invoice export fails since this morning.'
                    content_format: 1
                    silent: false
                    attachments:
                    - position: 1
                      file:
                        id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                        size_bytes: 48213
                        mime_type: image/png
                        original_name: screenshot.png
                        uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        purpose: attachment
                        upload_state: complete
                        scan_status: clean
                        transcript_status: none
                        thumbnails:
                        - variant: small
                          mime_type: image/jpeg
                          width: 320
                          height: 180
                        created_at: '2026-10-10T09:30:00.000Z'
                    mention_ids: [0192f4a0-7b1c-718f-86e7-9cb8b914b848]
                    reactions:
                    - emoji: 👍
                      count: 3
                    pinned: false
                    components:
                    - id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                      custom_id: approve-refund
                      type: button
                      definition: {}
                      position: 1
                      single_use: true
                      restricted_to: any
                      state: active
                      version: 3
                    version: 3
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: updateMessage
      summary: Edit own message
      tags: [Messages]
      description: '`If-Match` carries the expected version. A deleted message gives 410 `message_deleted`.'
      x-authz-action: message.update
      x-rate-limit-class: rl.message
      parameters:
      - $ref: '#/components/parameters/MessageIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageUpdate'
            examples:
              default:
                summary: Request
                value:
                  content_md: '@support-agent the invoice export fails since 08:00.'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_seq: 42
                    author:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      kind: user
                      handle: alex
                      display_name: Alex
                    client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                    origin: human
                    kind: text
                    content_md: '@support-agent the invoice export fails since this morning.'
                    content_format: 1
                    silent: false
                    attachments:
                    - position: 1
                      file:
                        id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                        size_bytes: 48213
                        mime_type: image/png
                        original_name: screenshot.png
                        uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        purpose: attachment
                        upload_state: complete
                        scan_status: clean
                        transcript_status: none
                        thumbnails:
                        - variant: small
                          mime_type: image/jpeg
                          width: 320
                          height: 180
                        created_at: '2026-10-10T09:30:00.000Z'
                    mention_ids: [0192f4a0-7b1c-718f-86e7-9cb8b914b848]
                    reactions:
                    - emoji: 👍
                      count: 3
                    pinned: false
                    components:
                    - id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                      custom_id: approve-refund
                      type: button
                      definition: {}
                      position: 1
                      single_use: true
                      restricted_to: any
                      state: active
                      version: 3
                    version: 3
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteMessage
      summary: Delete a message
      tags: [Messages]
      description: |-
        Idempotent; an already deleted message also gives 204. Moderation of other people's messages
        needs `manage`.
      x-authz-action: message.delete
      x-rate-limit-class: rl.message
      parameters:
      - $ref: '#/components/parameters/MessageIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /messages/{message_id}/revisions:
    get:
      operationId: listMessageRevisions
      summary: Read earlier versions
      tags: [Messages]
      x-authz-action: message.revisions.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/MessageIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageRevisionList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - revision: 1
                      content_md: '@support-agent the invoice export fails.'
                      edited_by: 0192f4a0-7b1c-72e5-8bdc-9774dd9ce26f
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /messages/{message_id}/reactions:
    get:
      operationId: listReactions
      summary: List who reacted
      tags: [Messages]
      x-authz-action: message.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/MessageIdPath'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: emoji
        in: query
        description: Only this emoji.
        schema:
          type: string
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReactionPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - emoji: 👍
                      principal:
                        id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        kind: user
                        handle: alex
                        display_name: Alex
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /messages/{message_id}/reactions/{emoji}:
    put:
      operationId: addReaction
      summary: Add own reaction
      tags: [Messages]
      x-authz-action: reaction.add
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/MessageIdPath'
      - $ref: '#/components/parameters/EmojiPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: removeReaction
      summary: Remove own reaction
      tags: [Messages]
      x-authz-action: reaction.remove
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/MessageIdPath'
      - $ref: '#/components/parameters/EmojiPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /messages/{message_id}/pin:
    put:
      operationId: pinMessage
      summary: Pin a message
      tags: [Messages]
      x-authz-action: pin.create
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/MessageIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: unpinMessage
      summary: Unpin a message
      tags: [Messages]
      x-authz-action: pin.delete
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/MessageIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /components/{component_id}/interactions:
    post:
      operationId: createInteraction
      summary: Use a button, select or form
      tags: [Interactions]
      description: |-
        Single-use components accept exactly one interaction; later ones get 409 `component_used`.
        Idempotent by `(principal, client_interaction_id)`.
      x-authz-action: interaction.create
      x-rate-limit-class: rl.interaction
      x-idempotency-exempt: client_id
      parameters:
      - $ref: '#/components/parameters/ComponentIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InteractionCreate'
            examples:
              default:
                summary: Request
                value:
                  client_interaction_id: 0192f4a0-7b1c-704d-8f46-22a420803f92
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Interaction'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-77f5-8fe4-151103a7b8b5
                    component_id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                    custom_id: approve-refund
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    principal_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    client_interaction_id: 0192f4a0-7b1c-704d-8f46-22a420803f92
                    values: {}
                    state: received
                    created_at: '2026-10-10T09:30:00.000Z'
        '200':
          description: 'Replay: the original created by an earlier request with the same `client_interaction_id`.'
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            Idempotent-Replay:
              $ref: '#/components/headers/Idempotent-Replay'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Interaction'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-77f5-8fe4-151103a7b8b5
                    component_id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                    custom_id: approve-refund
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    principal_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    client_interaction_id: 0192f4a0-7b1c-704d-8f46-22a420803f92
                    values: {}
                    state: received
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /components/{component_id}:
    patch:
      operationId: updateComponent
      summary: Disable or change an own component
      tags: [Interactions]
      x-authz-action: component.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/ComponentIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ComponentUpdate'
            examples:
              default:
                summary: Request
                value: {}
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Component'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                    custom_id: approve-refund
                    type: button
                    definition: {}
                    position: 1
                    single_use: true
                    restricted_to: any
                    state: active
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /interactions/{interaction_id}/respond:
    post:
      operationId: respondToInteraction
      summary: Acknowledge or answer an interaction
      tags: [Agent runtime]
      x-authz-action: interaction.respond
      x-rate-limit-class: rl.agent_runtime
      parameters:
      - $ref: '#/components/parameters/InteractionIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InteractionRespond'
            examples:
              default:
                summary: Request
                value:
                  state: acknowledged
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Interaction'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-77f5-8fe4-151103a7b8b5
                    component_id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                    custom_id: approve-refund
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    principal_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    client_interaction_id: 0192f4a0-7b1c-704d-8f46-22a420803f92
                    values: {}
                    state: received
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/uploads:
    post:
      operationId: createUpload
      summary: Start an upload
      tags: [Files]
      description: |-
        Checks in this order (12, 3.1): size against `file.max_upload_bytes` (413 `payload_too_large`), quota `file.quota_bytes_per_principal` (409 `conflict` with `reason` `quota_exceeded`), open sessions (429), free space of the file system driver (503 `unavailable` with `reason` `storage_full`). A denied type answers 415 `unsupported_media_type` with `reason` `file_type_denied`.
      x-authz-action: file.upload
      x-rate-limit-class: rl.upload
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadCreate'
            examples:
              default:
                summary: Request
                value:
                  name: screenshot.png
                  size_bytes: 48213
                  sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
                  declared_mime_type: image/png
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadSession'
              examples:
                default:
                  summary: Response
                  value:
                    file_id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                    mode: direct
                    part_size_bytes: 1
                    parts:
                    - part_number: 1
                      url: /api/v1/uploads/0192f4a0-7b1c-7000-8a00-000000000501/parts/1
                      headers:
                      - name: Content-Type
                        value: application/octet-stream
                    expires_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '415':
          $ref: '#/components/responses/UnsupportedMediaType'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /uploads/{file_id}/parts/{part_number}:
    put:
      operationId: uploadPart
      summary: Upload one part in direct mode
      tags: [Files]
      description: |-
        Parts can be sent in parallel and in any order (12, 3.2). The same part number with the same `Content-Digest` answers 200 without new work; with another digest 409 `conflict` with `reason` `part_conflict`. Wrong length or digest: 400 `validation_failed` with `errors[].code` `part_digest_mismatch`, nothing is stored. An expired session answers 410 `gone`.
      x-authz-action: file.upload
      x-rate-limit-class: rl.upload
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      - $ref: '#/components/parameters/PartNumberPath'
      - name: Content-Digest
        in: header
        required: true
        description: 'RFC 9530 digest of the part with the member `sha-256`, e.g. `sha-256=:<base64>:`; other members are ignored.'
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadPartResult'
              examples:
                default:
                  summary: Response
                  value:
                    part_number: 1
                    etag: '"3858f62230ac3c915f300c664312c11f"'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          $ref: '#/components/responses/Gone'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /uploads/{file_id}/complete:
    post:
      operationId: completeUpload
      summary: Finish an upload
      tags: [Files]
      description: |-
        Closes the session and queues the job `files.scan`, which checks size and SHA-256, detects the MIME type and runs the malware scan. The file is
        downloadable after `scan_status` is `clean` or `skipped`. `parts` is checked against the confirmed parts: a duplicate or out-of-range number gives 400, another `etag` 409 `conflict` with `reason` `part_conflict`. A completed session answers 200 with the file again. Missing parts answer 409 `conflict` with `reason` `upload_incomplete`; an expired session answers 410 `gone` (12, 3.4 and 3.6).
      x-authz-action: file.upload
      x-rate-limit-class: rl.upload
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadComplete'
            examples:
              default:
                summary: Request
                value:
                  parts:
                  - part_number: 1
                    etag: '"3858f62230ac3c915f300c664312c11f"'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/File'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                    sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
                    size_bytes: 48213
                    mime_type: image/png
                    original_name: screenshot.png
                    uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    purpose: attachment
                    upload_state: complete
                    scan_status: clean
                    transcript_status: none
                    thumbnails:
                    - variant: small
                      mime_type: image/jpeg
                      width: 320
                      height: 180
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          $ref: '#/components/responses/Gone'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /uploads/{file_id}:
    get:
      operationId: getUpload
      summary: Read the state of an own upload
      tags: [Files]
      description: |-
        Confirmed parts of an own upload, so a client can resume after a network loss (12, 3.2). `upload_state` is `pending` for an open session without a confirmed part, `uploading` with at least one, `complete` once completed. An aborted or expired session answers 410 `gone`.
      x-authz-action: file.upload
      x-rate-limit-class: rl.upload
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadState'
              examples:
                default:
                  summary: Response
                  value:
                    file_id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                    upload_state: uploading
                    part_size_bytes: 8388608
                    completed_parts:
                    - part_number: 1
                      etag: '"9b2cf535f27731c974343645a3985328"'
                    expires_at: '2026-10-10T09:30:00.000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: abortUpload
      summary: Abort an upload
      tags: [Files]
      description: |-
        Ends an open own session: its parts are deleted, the file becomes `upload_state` `failed` and `file.updated` goes to the uploader. An aborted or expired session answers 204 again; a completed one 409 `invalid_state_transition`.
      x-authz-action: file.delete
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /files/{file_id}:
    get:
      operationId: getFile
      summary: Read file metadata
      tags: [Files]
      description: The header `ETag` is the version of the transcript of the file, derived from its status, source, language and text (03 R-DM-48, 12 8.2); `getTranscript` sends the same value, and `submitTranscript` expects it in `If-Match`.
      x-authz-action: file.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/File'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                    sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
                    size_bytes: 48213
                    mime_type: image/png
                    original_name: screenshot.png
                    uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    purpose: attachment
                    upload_state: complete
                    scan_status: clean
                    transcript_status: none
                    thumbnails:
                    - variant: small
                      mime_type: image/jpeg
                      width: 320
                      height: 180
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteFile
      summary: Delete an unattached file
      description: Deletes an own file that is not attached. A file the caller already deleted answers `204` as well; an unknown, foreign or purged file answers `404 not_found`. An attached file answers `409 conflict` with `reason` `file_attached` (delete the message instead); an avatar, logo or export file answers `409 conflict` with `reason` `purpose_not_deletable`. The objects and the row of the file are removed within `privacy.object_delete_delay`.
      tags: [Files]
      x-authz-action: file.delete
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /files/{file_id}/content:
    get:
      operationId: downloadFile
      summary: Download a file
      tags: [Files]
      description: |-
        Answers 200 with the bytes (supports `Range`); there are no presigned GET URLs (ADR 0048).
        Media players without DPoP use a ticket (`createFileTicket`) in the query parameter `ticket`
        instead of `Authorization`. 409 `file_not_ready` while the scan is pending, 409 `file_blocked`
        for blocked or failed files; without read access always 404 (12, 9.3). Exactly one byte range
        gives 206, several ranges are ignored (200 with the whole file), a range outside the file
        gives 416. Types outside the inline list of 12 9.3 (HTML, SVG, PDF and every other type) are sent as
        `application/octet-stream` with `Content-Disposition: attachment`, `nosniff` and the sandbox
        CSP. An `Authorization` header wins over a `ticket` parameter. Pass-through via the server
        only (ADR 0048).
      x-authz-action: file.read
      x-rate-limit-class: rl.read
      security:
      - dpopAuth: []
      - bearerAuth: []
      - fileTicket: []
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      - $ref: '#/components/parameters/FileTicketQuery'
      - $ref: '#/components/parameters/Range'
      - $ref: '#/components/parameters/If-Range'
      responses:
        '200':
          description: The whole file (12, 9.3; ADR 0048).
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            Content-Disposition:
              $ref: '#/components/headers/Content-Disposition'
            Content-Security-Policy:
              $ref: '#/components/headers/File-Content-Security-Policy'
            Cross-Origin-Resource-Policy:
              $ref: '#/components/headers/Cross-Origin-Resource-Policy'
            Cache-Control:
              $ref: '#/components/headers/File-Cache-Control'
            ETag:
              $ref: '#/components/headers/File-ETag'
            Repr-Digest:
              $ref: '#/components/headers/Repr-Digest'
            Accept-Ranges:
              $ref: '#/components/headers/Accept-Ranges'
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
            image/gif:
              schema:
                type: string
                format: binary
            audio/mp4:
              schema:
                type: string
                format: binary
            audio/mpeg:
              schema:
                type: string
                format: binary
            video/mp4:
              schema:
                type: string
                format: binary
        '206':
          description: Exactly one byte range of the file (12, 9.3; ADR 0048).
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            Content-Disposition:
              $ref: '#/components/headers/Content-Disposition'
            Content-Security-Policy:
              $ref: '#/components/headers/File-Content-Security-Policy'
            Cross-Origin-Resource-Policy:
              $ref: '#/components/headers/Cross-Origin-Resource-Policy'
            Cache-Control:
              $ref: '#/components/headers/File-Cache-Control'
            ETag:
              $ref: '#/components/headers/File-ETag'
            Repr-Digest:
              $ref: '#/components/headers/Repr-Digest'
            Accept-Ranges:
              $ref: '#/components/headers/Accept-Ranges'
            Content-Range:
              $ref: '#/components/headers/Content-Range'
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
            image/jpeg:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
            image/gif:
              schema:
                type: string
                format: binary
            audio/mp4:
              schema:
                type: string
                format: binary
            audio/mpeg:
              schema:
                type: string
                format: binary
            video/mp4:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/FileConflict'
        '416':
          $ref: '#/components/responses/RangeNotSatisfiable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /files/{file_id}/thumbnails/{variant}:
    get:
      operationId: downloadThumbnail
      summary: Download a thumbnail
      tags: [Files]
      description: |-
        Accepts a fetch ticket in the query parameter `ticket` like `downloadFile` (push preview
        `image_url`, ADR 0048). 409 `file_not_ready` or `file_blocked` as for `downloadFile`; a variant
        the file does not have gives 404. Thumbnails are always inline with their own type; range,
        headers and caching as for `downloadFile` (12, 9.3).
      x-authz-action: file.read
      x-rate-limit-class: rl.read
      security:
      - dpopAuth: []
      - bearerAuth: []
      - fileTicket: []
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      - $ref: '#/components/parameters/VariantPath'
      - $ref: '#/components/parameters/FileTicketQuery'
      - $ref: '#/components/parameters/Range'
      - $ref: '#/components/parameters/If-Range'
      responses:
        '200':
          description: The whole thumbnail (12, 9.3; ADR 0048).
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            Content-Disposition:
              $ref: '#/components/headers/Content-Disposition'
            Content-Security-Policy:
              $ref: '#/components/headers/File-Content-Security-Policy'
            Cross-Origin-Resource-Policy:
              $ref: '#/components/headers/Cross-Origin-Resource-Policy'
            Cache-Control:
              $ref: '#/components/headers/File-Cache-Control'
            ETag:
              $ref: '#/components/headers/File-ETag'
            Repr-Digest:
              $ref: '#/components/headers/Repr-Digest'
            Accept-Ranges:
              $ref: '#/components/headers/Accept-Ranges'
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
        '206':
          description: Exactly one byte range of the thumbnail (12, 9.3; ADR 0048).
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            Content-Disposition:
              $ref: '#/components/headers/Content-Disposition'
            Content-Security-Policy:
              $ref: '#/components/headers/File-Content-Security-Policy'
            Cross-Origin-Resource-Policy:
              $ref: '#/components/headers/Cross-Origin-Resource-Policy'
            Cache-Control:
              $ref: '#/components/headers/File-Cache-Control'
            ETag:
              $ref: '#/components/headers/File-ETag'
            Repr-Digest:
              $ref: '#/components/headers/Repr-Digest'
            Accept-Ranges:
              $ref: '#/components/headers/Accept-Ranges'
            Content-Range:
              $ref: '#/components/headers/Content-Range'
          content:
            image/jpeg:
              schema:
                type: string
                format: binary
            image/png:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/FileConflict'
        '416':
          $ref: '#/components/responses/RangeNotSatisfiable'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /files/{file_id}/tickets:
    post:
      operationId: createFileTicket
      summary: Create a short-lived fetch ticket
      tags: [Files]
      description: |-
        For media players and "open with" that cannot send a DPoP proof. Every fetch with the ticket
        checks signature, expiry, session and `file.read` again (12, 9.2). A ticket binds to the login
        session of the caller: a principal without a session (agent, app, API token) gets 403
        `forbidden` with `reason` `step_up_not_possible`. No ticket is issued for a file that is not
        deliverable (409 `file_not_ready` or `file_blocked`), and a variant the file does not have
        gives 404. A repeated call mints a new ticket.
      x-authz-action: file.read
      x-rate-limit-class: rl.read
      x-idempotency-exempt: read_only
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FileTicketCreate'
            examples:
              default:
                summary: Request
                value:
                  variant: original
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileTicket'
              examples:
                default:
                  summary: Response
                  value:
                    url: '/api/v1/files/0192f4a0-7b1c-75c7-88f6-199cc394b56a/content?ticket=c2lnbmVk'
                    expires_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/FileConflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /files/{file_id}/transcript:
    get:
      operationId: getTranscript
      summary: Read the transcript of a voice message
      tags: [Files]
      description: 409 `file_not_ready` or `file_blocked` as for `downloadFile`; without read access always 404 (12, 9.3). Normal authentication only, never a fetch ticket. `text`, `language` and `source` are present only with `status` `done`. The header `ETag` is the derived version of the transcript, the same value as in `getFile` and the `If-Match` of `submitTranscript`.
      x-authz-action: file.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transcript'
              examples:
                default:
                  summary: Response
                  value:
                    file_id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                    status: done
                    text: Please call the customer back tomorrow morning.
                    language: en
                    source: agent
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/FileConflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    put:
      operationId: submitTranscript
      summary: Store a transcript created by the agent
      tags: [Agent runtime]
      description: Mode `agent` of `voice.transcription_mode` only (12, 8.2). Only the operator agent of the thread whose message the voice file is attached to, with read access, may store the transcript; a file the caller cannot read, an unattached file of another uploader and an unknown id answer the same 404, and a visible caller who is not that operator gets 403 `forbidden`. Another mode, or a file that is not a voice format, gives 409 `conflict` with `reason` `transcription_unavailable`; a file that is not ready gives 409 `file_not_ready` or `file_blocked`. `If-Match` carries the transcript `ETag` of `getFile` or `getTranscript` (derived from status, source, language and text, 03 R-DM-48); without it or with `*` 428 `precondition_required`, with another value 412 `version_conflict` with the current `Transcript` in `current` and its `ETag`. `text` may be empty, must not contain U+0000 (`invalid`) and is at most `message.max_content_bytes` UTF-8 bytes (`too_long`). The answer has `status` `done` and `source` `agent` and the new `ETag`; a later submit with that value replaces the transcript. No audit entry.
      x-authz-action: file.transcript.submit
      x-rate-limit-class: rl.agent_runtime
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TranscriptSubmit'
            examples:
              default:
                summary: Request
                value:
                  text: Please call the customer back tomorrow morning.
                  language: en
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transcript'
              examples:
                default:
                  summary: Response
                  value:
                    file_id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                    status: done
                    text: Please call the customer back tomorrow morning.
                    language: en
                    source: agent
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /files/{file_id}/transcribe:
    post:
      operationId: requestTranscription
      summary: Request a transcript
      tags: [Files]
      description: 403 `feature_disabled` when `voice.enabled` is off. Starts the server transcript in mode `service` of `voice.transcription_mode` (12, 8.2). An unknown or invisible file answers 404; another mode, or a file that is not a voice format, gives 409 `conflict` with `reason` `transcription_unavailable`; a file that is not ready gives 409 `file_not_ready` or `file_blocked`. A transcript in status `none` or `failed` goes to `pending` and a new job starts; in status `pending` or `done` nothing changes. The answer is the current `Transcript`; the result arrives through `file.updated`.
      x-authz-action: file.transcribe
      x-rate-limit-class: rl.upload
      parameters:
      - $ref: '#/components/parameters/FileIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Accepted; processing continues asynchronously.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Transcript'
              examples:
                default:
                  summary: Response
                  value:
                    file_id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                    status: pending
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /search:
    get:
      operationId: search
      summary: Full-text search
      tags: [Search]
      description: |-
        Results are filtered by `message.read`, `thread.read` and `file.read` inside the query; counts
        are not returned. Page size `search.page_size_default`, at most `search.page_size_max` (14, 4.7).
      x-authz-action: search.query
      x-rate-limit-class: rl.search
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: q
        in: query
        description: 'Query in web search syntax: words, "phrases", `or`, `-word`.'
        schema:
          type: string
          minLength: 1
        required: true
      - name: type
        in: query
        description: Result type; default `messages` (14, 11).
        schema:
          type: string
          enum: [messages, threads, files]
          default: messages
      - name: sort
        in: query
        description: Order of the hits (14, 4.5).
        schema:
          type: string
          enum: [relevance, newest]
          default: relevance
      - name: space_id
        in: query
        description: Only channels of these spaces, intersected with the visible ones.
        schema:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        style: form
        explode: true
      - name: channel_id
        in: query
        description: Only these channels, intersected with the visible ones.
        schema:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        style: form
        explode: true
      - name: thread_id
        in: query
        description: Restrict to a thread.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: author_id
        in: query
        description: Author of the message or uploader of the file.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: agent_id
        in: query
        description: Threads operated by this agent.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: after
        in: query
        description: Created at or after.
        schema:
          $ref: '#/components/schemas/Timestamp'
      - name: before
        in: query
        description: Created before.
        schema:
          $ref: '#/components/schemas/Timestamp'
      - name: has_file
        in: query
        description: Only messages with at least one attachment.
        schema:
          type: boolean
      - name: thread_state
        in: query
        description: Only open or closed threads.
        schema:
          $ref: '#/components/schemas/ThreadState'
      - name: tag_id
        in: query
        description: Forum tags of the thread.
        schema:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        style: form
        explode: true
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SearchHitPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - kind: message
                      score: 1.5
                      thread_title: Invoice export fails
                      channel_name: tickets
                      space_name: Support
                      segments:
                      - text: 'the '
                        match: false
                      - text: invoice
                        match: true
                      - text: ' export fails'
                        match: false
                    candidates_capped: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /search/thread-titles:
    get:
      operationId: searchThreadTitles
      summary: Prefix search over thread titles
      tags: [Search]
      description: |-
        For the quick switcher (14, 4.9): words shorter than `search.prefix_min_chars` are dropped; same access filter as `/search`, newest activity first. Page size `search.page_size_default`, at most `search.page_size_max`.
      x-authz-action: search.query
      x-rate-limit-class: rl.search
      parameters:
      - $ref: '#/components/parameters/Limit'
      - name: q
        in: query
        required: true
        description: Typed prefix text.
        schema:
          type: string
          minLength: 1
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ThreadTitleHitList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      title: Invoice export fails
                      last_activity_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /sync/snapshot:
    get:
      operationId: getSnapshot
      summary: Load a snapshot
      tags: [Sync]
      x-authz-action: sync.snapshot.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Snapshot'
              examples:
                default:
                  summary: Response
                  value:
                    head_seq: 5300
                    sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                    permission_version: 418
                    principal:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      kind: user
                      handle: alex
                      display_name: Alex
                    is_owner: false
                    spaces:
                    - space:
                        id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                        name: Support
                        sort_key: a0
                        version: 3
                        created_at: '2026-10-10T09:30:00.000Z'
                      level: read
                      capabilities: [approve_actions]
                    channels:
                    - channel:
                        id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                        kind: text
                        name: tickets
                        topic: Customer tickets
                        sort_key: a1
                        read_only: false
                        operator:
                          mode: inherit
                        version: 3
                        settings_version: 1
                        created_at: '2026-10-10T09:30:00.000Z'
                      level: read
                      capabilities: [approve_actions]
                    sidebar:
                    - space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                      collapsed: false
                      sort_key: a0V
                      version: 3
                    unread_threads:
                    - thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      unread_count: 1
                      mention_count: 1
                      last_message_seq: 5301
                      last_read_seq: 5301
                    active_runs:
                    - id: 0192f4a0-7b1c-767f-8292-c1b14d7d40df
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      triggered_by: 0192f4a0-7b1c-7db7-8afc-99f48568ad99
                      state: queued
                      state_reason: user_cancelled
                      version: 3
                      billing_mode_expected: personal
                      status_line: Reading the export logs
                      summary: Found the cause and proposed a fix
                      cost:
                        amount_micros: 1250000
                        currency: USD
                        is_estimate: true
                      step_count: 1
                      queued_at: '2026-10-10T09:30:00.000Z'
                    agents:
                    - agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      state: unknown
                    settings:
                    - key: composer.enter_sends
                      value: true
                      version: 3
                    drafts:
                    - id: 0192f4a0-7b1c-7f2d-8106-461e390824dc
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      version: 3
                    features:
                    - key: dm.enabled
                      enabled: false
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /events:
    get:
      operationId: listEvents
      summary: Catch up on events after a sequence
      tags: [Sync]
      description: Same filtering as the gateway. 410 `resync_required` with `reason` when catch-up is impossible.
      x-authz-action: events.catch_up
      x-rate-limit-class: rl.read
      parameters:
      - name: after
        in: query
        description: Last processed `seq`.
        schema:
          type: integer
          format: int64
          minimum: 0
        required: true
      - name: epoch
        in: query
        description: Sync epoch stored with the cursor (04 section 7.5). A different epoch answers `410 resync_required` with `reason = epoch_changed`. Absent only before the first catch-up; then `cursor_ahead` is the second net (R-SY-04).
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: limit
        in: query
        description: At most `sync.catchup_page_size`.
        schema:
          type: integer
          minimum: 1
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventPage'
              examples:
                default:
                  summary: Response
                  value:
                    events:
                    - id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                      schema_version: 1
                      occurred_at: '2026-10-10T09:30:00.000Z'
                      scope: {}
                      resource:
                        kind: message
                      type: message.created
                      payload:
                        message:
                          id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                          thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                          channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                          thread_seq: 42
                          author:
                            id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                            kind: user
                            handle: alex
                            display_name: Alex
                          client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                          origin: human
                          kind: text
                          content_md: '@support-agent the invoice export fails since this morning.'
                          content_format: 1
                          silent: false
                          attachments:
                          - position: 1
                            file:
                              id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                              size_bytes: 48213
                              mime_type: image/png
                              original_name: screenshot.png
                              uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                              purpose: attachment
                              upload_state: complete
                              scan_status: clean
                              transcript_status: none
                              thumbnails: []
                              created_at: '2026-10-10T09:30:00.000Z'
                          mention_ids: [0192f4a0-7b1c-718f-86e7-9cb8b914b848]
                          reactions:
                          - emoji: 👍
                            count: 3
                          pinned: false
                          components:
                          - id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                            custom_id: approve-refund
                            type: button
                            definition: {}
                            position: 1
                            single_use: true
                            restricted_to: any
                            state: active
                            version: 3
                          version: 3
                          created_at: '2026-10-10T09:30:00.000Z'
                    next_cursor: 1
                    head_seq: 5301
                    sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/read-states:
    get:
      operationId: listMyReadStates
      summary: List own read positions
      tags: [Preferences]
      x-authz-action: settings.read
      x-rate-limit-class: rl.read
      parameters:
      - name: channel_id
        in: query
        description: Only threads of this channel.
        schema:
          $ref: '#/components/schemas/Uuid'
        required: true
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ReadStateList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      last_read_seq: 41
                      mention_count: 1
                      version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/drafts:
    get:
      operationId: listMyDrafts
      summary: List own drafts
      tags: [Preferences]
      x-authz-action: draft.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DraftList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7ad4-8297-1c4f06dd683c
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      content_md: Draft of a reply
                      file_ids: [0192f4a0-7b1c-746e-8bc6-de981880ca4c]
                      version: 3
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/settings:
    get:
      operationId: listMySettings
      summary: List own settings
      tags: [Preferences]
      x-authz-action: settings.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserSettingList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - key: composer.enter_sends
                      value: true
                      version: 3
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/settings/{key}:
    put:
      operationId: putMySetting
      summary: Set an own setting
      tags: [Preferences]
      x-authz-action: settings.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/KeyPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserSettingPut'
            examples:
              default:
                summary: Request
                value:
                  value: true
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserSetting'
              examples:
                default:
                  summary: Response
                  value:
                    key: composer.enter_sends
                    value: true
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteMySetting
      summary: Reset an own setting to default
      tags: [Preferences]
      x-authz-action: settings.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/KeyPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/notification-preferences:
    get:
      operationId: listMyNotificationPreferences
      summary: List notification levels
      description: >-
        The caller's own levels, ordered by target kind, then target id. Only targets the caller can still
        read are listed; rows on targets the caller lost stay stored and appear again when the right returns.
      tags: [Preferences]
      x-authz-action: settings.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationPreferenceList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - target_kind: space
                      target_id: 0192f4a0-7b1c-7fd5-831e-d4370e087874
                      level: all
                      version: 3
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/notification-preferences/{target_kind}/{target_id}:
    put:
      operationId: putNotificationPreference
      summary: Set the notification level of a target
      description: >-
        Sets the caller's level on a space, channel or thread the caller can read, the main thread of a text
        channel included. An unknown target, an id of another kind and a target without read right give the
        same 404. An archived target (or a channel in an archived space) takes no new preference: 409
        conflict with reason target_archived. muted_until is stored as given (whole microseconds), also when
        it has passed; a passed value has no effect. A write that changes nothing answers 200 without event
        and without a new version.
      tags: [Preferences]
      x-authz-action: notification_pref.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/TargetKindPath'
      - $ref: '#/components/parameters/TargetIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NotificationPreferencePut'
            examples:
              default:
                summary: Request
                value:
                  level: all
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/NotificationPreference'
              examples:
                default:
                  summary: Response
                  value:
                    target_kind: space
                    target_id: 0192f4a0-7b1c-7fd5-831e-d4370e087874
                    level: all
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteNotificationPreference
      summary: Inherit the notification level again
      description: >-
        Removes the caller's level on the target, so it inherits again. Answers 204 also when no row is
        stored (then without event); deleting stays possible on an archived target. A later put starts again
        at version 1.
      tags: [Preferences]
      x-authz-action: notification_pref.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/TargetKindPath'
      - $ref: '#/components/parameters/TargetIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/quiet-hours:
    get:
      operationId: listMyQuietHours
      summary: List quiet hours
      tags: [Preferences]
      x-authz-action: settings.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuietHoursList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7c3e-88ef-5d4efd8f4f24
                      weekdays: 31
                      start_local: '22:00'
                      end_local: 07:00
                      time_zone: Europe/Berlin
                      enabled: true
                      version: 3
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createQuietHoursRule
      summary: Add a quiet hours rule
      description: >-
        Adds a rule. time_zone must be an IANA zone name (400 validation_failed with errors[].code
        unknown_time_zone); absent means the person's time zone. Every write sends the complete list in
        quiet_hours.updated; a write that would make that list larger than sync.event_payload_max_bytes is
        refused with 400 validation_failed and errors[].code limit_reached.
      tags: [Preferences]
      x-authz-action: quiet_hours.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuietHoursRuleInput'
            examples:
              default:
                summary: Request
                value:
                  weekdays: 31
                  start_local: '22:00'
                  end_local: 07:00
                  time_zone: Europe/Berlin
                  enabled: true
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuietHoursRule'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7c3e-88ef-5d4efd8f4f24
                    weekdays: 31
                    start_local: '22:00'
                    end_local: 07:00
                    time_zone: Europe/Berlin
                    enabled: true
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /me/quiet-hours/{rule_id}:
    put:
      operationId: putQuietHoursRule
      summary: Change a quiet hours rule
      description: >-
        Replaces the whole rule; an absent time_zone clears it (the rule then follows the person's time
        zone). Equal values answer 200 without event and without a new version. time_zone and the list size
        are checked as for createQuietHoursRule (unknown_time_zone, limit_reached).
      tags: [Preferences]
      x-authz-action: quiet_hours.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/RuleIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuietHoursRuleInput'
            examples:
              default:
                summary: Request
                value:
                  weekdays: 31
                  start_local: '22:00'
                  end_local: 07:00
                  time_zone: Europe/Berlin
                  enabled: true
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuietHoursRule'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7c3e-88ef-5d4efd8f4f24
                    weekdays: 31
                    start_local: '22:00'
                    end_local: 07:00
                    time_zone: Europe/Berlin
                    enabled: true
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteQuietHoursRule
      summary: Delete a quiet hours rule
      tags: [Preferences]
      x-authz-action: quiet_hours.update
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/RuleIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /client-metrics:
    post:
      operationId: submitClientMetrics
      summary: Submit anonymous client measurements
      tags: [Stats]
      description: |-
        Only when `privacy.client_metrics` is on; otherwise `403 feature_disabled`. At most once per `client.metrics.flush_interval` (15, section 17).
      x-authz-action: client_metrics.submit
      x-rate-limit-class: rl.write
      x-idempotency-exempt: telemetry
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClientMetricsBatch'
            examples:
              default:
                summary: Request
                value:
                  platform: ios
                  app_version: 1.4.0
                  density: touch
                  metrics:
                  - name: client.thread_open.duration
                    kind: histogram
                    buckets:
                    - le: 0.1
                      count: 12
                    - le: 0.25
                      count: 3
                  - name: client.reconnects
                    kind: counter
                    close_code: 4009
                    value: 1
      responses:
        '202':
          description: Accepted.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents:
    get:
      operationId: listAgents
      summary: List visible agents
      tags: [Agents]
      x-authz-action: agent.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      handle: support-agent
                      display_name: Support Agent
                      status: active
                      adapter_kind: hermes
                      tool_class: chat
                      description: Answers support tickets
                      billing_mode: personal
                      presence:
                        state: unknown
                      operator_config: {}
                      version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createAgent
      summary: Create an agent
      tags: [Agents]
      x-authz-action: agent.create
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentCreate'
            examples:
              default:
                summary: Request
                value:
                  handle: support-agent
                  display_name: Support Agent
                  adapter_kind: hermes
                  description: Answers support tickets
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    handle: support-agent
                    display_name: Support Agent
                    status: active
                    adapter_kind: hermes
                    tool_class: chat
                    description: Answers support tickets
                    billing_mode: personal
                    presence:
                      state: unknown
                      runtime_version: 2.3.1
                    operator_config: {}
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/{agent_id}:
    get:
      operationId: getAgent
      summary: Read an agent with reported state
      tags: [Agents]
      x-authz-action: agent.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/AgentIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    handle: support-agent
                    display_name: Support Agent
                    status: active
                    adapter_kind: hermes
                    tool_class: chat
                    description: Answers support tickets
                    billing_mode: personal
                    presence:
                      state: unknown
                      runtime_version: 2.3.1
                    operator_config: {}
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: updateAgent
      summary: Change an agent
      tags: [Agents]
      x-authz-action: agent.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AgentIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentUpdate'
            examples:
              default:
                summary: Request
                value:
                  display_name: Support Agent
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    handle: support-agent
                    display_name: Support Agent
                    status: active
                    adapter_kind: hermes
                    tool_class: chat
                    description: Answers support tickets
                    billing_mode: personal
                    presence:
                      state: unknown
                      runtime_version: 2.3.1
                    operator_config: {}
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/{agent_id}/disable:
    post:
      operationId: disableAgent
      summary: Disable an agent
      tags: [Agents]
      x-authz-action: agent.disable
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AgentIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Left the team
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    handle: support-agent
                    display_name: Support Agent
                    status: active
                    adapter_kind: hermes
                    tool_class: chat
                    description: Answers support tickets
                    billing_mode: personal
                    presence:
                      state: unknown
                      runtime_version: 2.3.1
                    operator_config: {}
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/{agent_id}/enable:
    post:
      operationId: enableAgent
      summary: Enable an agent
      tags: [Agents]
      x-authz-action: agent.enable
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AgentIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Left the team
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    handle: support-agent
                    display_name: Support Agent
                    status: active
                    adapter_kind: hermes
                    tool_class: chat
                    description: Answers support tickets
                    billing_mode: personal
                    presence:
                      state: unknown
                      runtime_version: 2.3.1
                    operator_config: {}
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/{agent_id}/billing-mode:
    put:
      operationId: setAgentBillingMode
      summary: Switch the backend policy of an agent
      tags: [Agents]
      description: |-
        Switching to `personal` requires `credential_holder_id` and fails with `i1_violation` if anyone
        else could trigger the agent.
      x-authz-action: agent.billing_mode.set
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AgentIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BillingModeSet'
            examples:
              default:
                summary: Request
                value:
                  billing_mode: personal
                  reason: Team uses the agent now
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    handle: support-agent
                    display_name: Support Agent
                    status: active
                    adapter_kind: hermes
                    tool_class: chat
                    description: Answers support tickets
                    billing_mode: personal
                    presence:
                      state: unknown
                      runtime_version: 2.3.1
                    operator_config: {}
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/{agent_id}/credential-holder:
    put:
      operationId: setCredentialHolder
      summary: Change the credential holder
      tags: [Agents]
      x-authz-action: agent.credential_holder.set
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AgentIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CredentialHolderSet'
            examples:
              default:
                summary: Request
                value:
                  credential_holder_id: 0192f4a0-7b1c-7ae9-8d74-7494cd730bc3
                  reason: Handover to deputy
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    handle: support-agent
                    display_name: Support Agent
                    status: active
                    adapter_kind: hermes
                    tool_class: chat
                    description: Answers support tickets
                    billing_mode: personal
                    presence:
                      state: unknown
                      runtime_version: 2.3.1
                    operator_config: {}
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/{agent_id}/tool-class:
    put:
      operationId: setAgentToolClass
      summary: Set the tool class of an agent
      tags: [Agents]
      description: |-
        Target tool class (`chat` < `scoped` < `host`, 07 2.11). `authorize` uses the higher of target and reported class; a guest run needs a matching tool class (SR2).
      x-authz-action: agent.tool_class.set
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AgentIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ToolClassSet'
            examples:
              default:
                summary: Request
                value:
                  tool_class: scoped
                  reason: Agent now works in a project checkout
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Agent'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    handle: support-agent
                    display_name: Support Agent
                    status: active
                    adapter_kind: hermes
                    tool_class: scoped
                    description: Answers support tickets
                    billing_mode: organization
                    presence:
                      state: unknown
                    operator_config: {}
                    version: 4
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/{agent_id}/webhooks:
    get:
      operationId: listAgentWebhookSubscriptions
      summary: List webhook subscriptions of an agent
      tags: [Webhooks]
      description: |-
        Webhook as transport for adapters without a running process (04, 15.6).
      x-authz-action: webhook_out.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/AgentIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7b8e-8d3f-2a9c4e6f0b15
                      agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      url: https://hooks.example.com/agent
                      event_types: [agent.invocation]
                      state: active
                      consecutive_failures: 0
                      signature_schemes: [v1a]
                      signing_key_kid: whk-2026-10
                      created_at: '2026-10-10T09:30:00.000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createAgentWebhookSubscription
      summary: Subscribe an agent to webhooks
      tags: [Webhooks]
      description: |-
        Delivery is strictly in `seq` order per subscription; `2xx` acknowledges up to that `seq`. Every delivery carries a freshly issued invocation token.
      x-authz-action: webhook_out.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AgentIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionCreate'
            examples:
              default:
                summary: Request
                value:
                  url: https://hooks.example.com/agent
                  event_types: [agent.invocation, agent.followup, run.cancel_requested]
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionCreated'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/me/heartbeat:
    post:
      operationId: sendHeartbeat
      summary: Report state and actual billing mode
      description: |-
        The adapter reports the whole state of its runtime (`agent_heartbeat.v1`, 09 section 4.8); the member `agent` of the gateway op `heartbeat` has the same effect. Only an agent may call it (`agent-self`); the body carries no agent id. The last heartbeat wins: an absent optional member means "not reported", except `project_readiness`, which is applied only when present and changed since the last list this server process applied (`ApplyProjectReadiness` of M2-03, in its own transaction).

        The server validates every member and the closed keys of `details` itself, also the patterns of names, versions and codes; a violation answers `400 validation_failed` with every field error; `rate_limit` carries only `state` and an optional `resets_at` (M6-14). A change of `reported_billing_mode` or `reported_tool_class`, also from or to "not reported", is stored at once and audited with action `agent.heartbeat`; the other fields are stored at most once per `agent.heartbeat_persist_interval`. The presence becomes `offline` once `agent.presence_timeout` passes without a heartbeat. The server compares the reported with the configured mode at each heartbeat and returns the warnings of `AgentRuntimeStatus.warnings`; the reported mode is evidence for display, warning, audit and alarm and never a permission (BR-05). `interval_ms` is `sync.heartbeat_interval`.
      tags: [Agent runtime]
      x-authz-action: agent.heartbeat
      x-rate-limit-class: rl.agent_runtime
      x-idempotency-exempt: agent_runtime
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Heartbeat'
            examples:
              default:
                summary: Request
                value:
                  state: online
                  runtime_version: 2.3.1
                  reported_billing_mode: personal
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HeartbeatAck'
              examples:
                default:
                  summary: Response
                  value:
                    presence:
                      state: online
                      reported_billing_mode: personal
                      runtime_version: 2.3.1
                      last_heartbeat_at: '2026-10-20T09:00:00.000Z'
                      billing_mode_matches: true
                    interval_ms: 30000
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/me/consumers/{consumer}:
    get:
      operationId: getConsumerState
      summary: Read the cursor of a consumer
      tags: [Agent runtime]
      x-authz-action: agent.events.consume
      x-rate-limit-class: rl.agent_runtime
      parameters:
      - $ref: '#/components/parameters/ConsumerPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsumerState'
              examples:
                default:
                  summary: Response
                  value:
                    consumer: default
                    acked_seq: 5301
                    head_seq: 5301
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/me/consumers/{consumer}/events:
    get:
      operationId: pollAgentEvents
      summary: Fetch events for a consumer without WebSocket
      tags: [Agent runtime]
      description: |-
        Delivers from `acked_seq + 1`. Invocation events carry a fresh `invocation_token`.
        Unacknowledged events are delivered again after `agent.ack_timeout`.
      x-authz-action: agent.events.consume
      x-rate-limit-class: rl.agent_runtime
      parameters:
      - $ref: '#/components/parameters/ConsumerPath'
      - name: limit
        in: query
        description: At most `agent.max_inflight_events`.
        schema:
          type: integer
          minimum: 1
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EventPage'
              examples:
                default:
                  summary: Response
                  value:
                    events:
                    - id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                      schema_version: 1
                      occurred_at: '2026-10-10T09:30:00.000Z'
                      scope: {}
                      resource:
                        kind: message
                      type: message.created
                      payload:
                        message:
                          id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                          thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                          channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                          thread_seq: 42
                          author:
                            id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                            kind: user
                            handle: alex
                            display_name: Alex
                          client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                          origin: human
                          kind: text
                          content_md: '@support-agent the invoice export fails since this morning.'
                          content_format: 1
                          silent: false
                          attachments:
                          - position: 1
                            file:
                              id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                              size_bytes: 48213
                              mime_type: image/png
                              original_name: screenshot.png
                              uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                              purpose: attachment
                              upload_state: complete
                              scan_status: clean
                              transcript_status: none
                              thumbnails: []
                              created_at: '2026-10-10T09:30:00.000Z'
                          mention_ids: [0192f4a0-7b1c-718f-86e7-9cb8b914b848]
                          reactions:
                          - emoji: 👍
                            count: 3
                          pinned: false
                          components:
                          - id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                            custom_id: approve-refund
                            type: button
                            definition: {}
                            position: 1
                            single_use: true
                            restricted_to: any
                            state: active
                            version: 3
                          version: 3
                          created_at: '2026-10-10T09:30:00.000Z'
                    next_cursor: 1
                    head_seq: 5301
                    sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/me/consumers/{consumer}/ack:
    post:
      operationId: ackAgentEvents
      summary: Acknowledge events up to a sequence
      tags: [Agent runtime]
      x-authz-action: agent.events.consume
      x-rate-limit-class: rl.agent_runtime
      x-idempotency-exempt: agent_runtime
      parameters:
      - $ref: '#/components/parameters/ConsumerPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConsumerAck'
            examples:
              default:
                summary: Request
                value:
                  seq: 5301
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConsumerState'
              examples:
                default:
                  summary: Response
                  value:
                    consumer: default
                    acked_seq: 5301
                    head_seq: 5301
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/me/channels:
    get:
      operationId: listServedChannels
      summary: List channels the agent serves
      tags: [Agent runtime]
      x-authz-action: agent.config.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentChannelConfigList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      behavior:
                        require_mention: false
                        thread_require_mention: false
                        free_response: false
                        auto_thread: false
                        allow_mentioned_agents: false
                        history_backfill_limit: 20
                        prompt: Answer support tickets politely. Ask before closing a ticket.
                        skills: [support-triage]
                        settings_version: 1
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/me/channels/{channel_id}/config:
    get:
      operationId: getServedChannelConfig
      summary: Read prompt, skills and project binding of a served channel
      tags: [Agent runtime]
      x-authz-action: agent.config.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentChannelConfig'
              examples:
                default:
                  summary: Response
                  value:
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    behavior:
                      require_mention: false
                      thread_require_mention: false
                      free_response: false
                      auto_thread: false
                      allow_mentioned_agents: false
                      history_backfill_limit: 20
                      prompt: Answer support tickets politely. Ask before closing a ticket.
                      skills: [support-triage]
                      settings_version: 1
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /agents/me/triggerers:
    get:
      operationId: getAgentTriggerers
      summary: List who may trigger this agent
      tags: [Agent runtime]
      description: |-
        Principals with effective `trigger` on a channel the agent operates, with `permission_version`. The adapter keeps it as allow list and reloads on `access.changed` (09, 15).
      x-authz-action: agent.triggerers.read
      x-rate-limit-class: rl.agent_runtime
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AgentTriggerers'
              examples:
                default:
                  summary: Response
                  value:
                    principal_ids: [0192f4a0-7b1c-7f89-8d6b-6960453241bc]
                    permission_version: 418
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /runs:
    get:
      operationId: listActiveRuns
      summary: Running now
      tags: [Runs]
      description: Runs in `queued`, `running` or `waiting_input`, filtered by `run.read`.
      x-authz-action: run.overview.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: agent_id
        in: query
        description: Only runs of this agent.
        schema:
          $ref: '#/components/schemas/Uuid'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunSummaryPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-767f-8292-c1b14d7d40df
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      triggered_by: 0192f4a0-7b1c-7db7-8afc-99f48568ad99
                      state: queued
                      state_reason: user_cancelled
                      version: 3
                      billing_mode_expected: personal
                      status_line: Reading the export logs
                      summary: Found the cause and proposed a fix
                      cost:
                        amount_micros: 1250000
                        currency: USD
                        is_estimate: true
                      step_count: 1
                      queued_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /runs/{run_id}:
    get:
      operationId: getRun
      summary: Read the run document
      tags: [Runs]
      x-authz-action: run.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/RunIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Run'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-70cd-8013-27d7ac09a77f
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    triggered_by: 0192f4a0-7b1c-7db7-8afc-99f48568ad99
                    state: queued
                    version: 3
                    billing_mode_expected: personal
                    model: model-large
                    status_line: Reading the export logs
                    metrics:
                      context_pct: 37.5
                    cost:
                      amount_micros: 1250000
                      currency: USD
                      is_estimate: true
                    steps:
                      0192f4a0-7b1c-7986-8350-06ee441b527a:
                        id: 0192f4a0-7b1c-70f2-84b9-bdac58ce716b
                        ordinal: 1
                        kind: tool_call
                        name: read_file
                        title: Reading export.log
                        status: pending
                        started_at: '2026-10-10T09:30:00.000Z'
                    followups:
                      0192f4a0-7b1c-77d2-8ef5-9f8e2e1db96b:
                        message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                        state: queued
                    queued_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: patchRun
      summary: Apply a live-box patch
      tags: [Agent runtime]
      description: |-
        Same as gateway op `run.update` (one use case). Only the run's own agent patches, only while
        the run is `queued`, `running` or `waiting_input`. A path or op outside the allowed set of
        09 section 5.3 (for example `/state`) gives 400 `validation_failed` with `errors[].code`
        `patch_path_forbidden` and the `pointer` of the op. An ended run gives 409 `conflict` with
        `reason` `run_not_active`, checked before the version. A wrong `base_version` gives 409
        `base_version_conflict` with the current document in `current`, exactly as `getRun`
        answers it. Set semantics: `add` and `replace` set a member also when it is absent,
        `remove` of an absent member is a no-op, and one stored patch raises the version by one.
        A follow-up state moves forward only (`queued` to `delivered` or `absorbed`, `delivered` to
        `absorbed`); a backward change gives 409 `invalid_state_transition`.
      x-authz-action: run.update
      x-rate-limit-class: rl.agent_runtime
      x-idempotency-exempt: agent_runtime
      parameters:
      - $ref: '#/components/parameters/RunIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunPatch'
            examples:
              default:
                summary: Request
                value:
                  base_version: 7
                  ops:
                  - op: replace
                    path: /steps/0192f4a0-7b1c-7000-8a00-0000000000a1/status
                    value: completed
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunPatchResult'
              examples:
                default:
                  summary: Response
                  value:
                    version: 8
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /runs/{run_id}/steps:
    get:
      operationId: listRunSteps
      summary: List steps in order
      tags: [Runs]
      x-authz-action: run.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/RunIdPath'
      - name: cursor
        in: query
        description: Cursor from the previous page.
        schema:
          $ref: '#/components/schemas/Cursor'
      - name: limit
        in: query
        description: Page size.
        schema:
          $ref: '#/components/schemas/PageLimit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunStepPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-70f2-84b9-bdac58ce716b
                      ordinal: 1
                      kind: tool_call
                      name: read_file
                      title: Reading export.log
                      status: pending
                      started_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /runs/{run_id}/context:
    get:
      operationId: getRunContext
      summary: Load the context of an own active run
      tags: [Runs]
      description: |-
        Agent-self, only for own active runs in `queued`, `running` or `waiting_input` (09, 4.6): the
        adapter reads it before `startRun`. An unknown run, a run the agent cannot see and an own run
        that is no longer active answer the same 404; another agent gets 403 `forbidden` with reason
        `not_self`, a person or an app reason `subject_kind_not_allowed`. The server computes `role` and `trust` of every
        element at the time of the request from the current rights; `trigger_message` is never cut
        and never counted, the other parts are cut per element at `agent.context_message_max_bytes`
        and in sum at `agent.context_max_bytes` from old to new.
      x-authz-action: run.context.read
      x-rate-limit-class: rl.agent_runtime
      parameters:
      - $ref: '#/components/parameters/RunIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunContext'
              examples:
                default:
                  summary: Response
                  value:
                    run_id: 0192f4a0-7b1c-7c4b-8f0e-4d2a6b1c9e30
                    context_floor_seq: 0
                    trigger_message:
                      message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                      thread_seq: 42
                      created_at: '2026-10-10T09:30:00.000Z'
                      author:
                        id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        kind: user
                        handle: alex
                        display_name: Alex
                      role: user
                      origin: human
                      kind: text
                      channel_id: 0192f4a0-7b1c-7a1e-9c2d-3b4a5c6d7e8f
                      content_md: '@support-agent the invoice export fails since this morning.'
                      attachments: []
                      trust: trigger
                    thread_context: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /runs/{run_id}/start:
    post:
      operationId: startRun
      summary: Start a queued run
      tags: [Agent runtime]
      description: |-
        Compare-and-swap `queued -> running` after checking the invocation token. Repeating the call
        with the same token after success returns 200 with `Idempotent-Replay: true` while the run is
        `running`; any other token gets 409 `already_started`. Once the run waits for input, the start
        token is consumed and gets 403 `invocation_token_invalid` with `reason: superseded`. A different `billing_mode_used` fails the run with
        `billing_mode_mismatch`; a `tool_class_used` above the `tool_class` of the token fails it with
        409 `tool_class_mismatch`. A token error gives 403 `invocation_token_invalid` with `reason`
        `expired` (renew with `renewInvocationToken`, then start again), `superseded` (a newer
        delivery replaced it) or `invalid` (09 section 4.5).
      x-authz-action: run.start
      x-rate-limit-class: rl.agent_runtime
      x-idempotency-exempt: agent_runtime
      parameters:
      - $ref: '#/components/parameters/RunIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunStart'
            examples:
              default:
                summary: Request
                value:
                  invocation_token: eyJhbGciOiJFZERTQSIsImtpZCI6Imludi0yMDI2LTEwIn0.e30.sig
                  billing_mode_used: personal
                  tool_class_used: scoped
                  external_session_ref: sess-7f3a
                  model: model-large
      responses:
        '200':
          description: 'OK. A repeated start with the same token while the run is `running` answers the current run with `Idempotent-Replay: true` and changes nothing (06 section 7 point 5).'
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            Idempotent-Replay:
              $ref: '#/components/headers/Idempotent-Replay'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Run'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-70cd-8013-27d7ac09a77f
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    triggered_by: 0192f4a0-7b1c-7db7-8afc-99f48568ad99
                    state: running
                    version: 3
                    billing_mode_expected: personal
                    model: model-large
                    status_line: Reading the export logs
                    metrics:
                      context_pct: 37.5
                    cost:
                      amount_micros: 1250000
                      currency: USD
                      is_estimate: true
                    steps:
                      0192f4a0-7b1c-7986-8350-06ee441b527a:
                        id: 0192f4a0-7b1c-70f2-84b9-bdac58ce716b
                        ordinal: 1
                        kind: tool_call
                        name: read_file
                        title: Reading export.log
                        status: pending
                        started_at: '2026-10-10T09:30:00.000Z'
                    followups:
                      0192f4a0-7b1c-77d2-8ef5-9f8e2e1db96b:
                        message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                        state: queued
                    queued_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /runs/{run_id}/resume:
    post:
      operationId: resumeRun
      summary: Resume a run that waited for input
      tags: [Agent runtime]
      description: '`waiting_input -> running` with the token of the interaction or follow-up delivery. The consumed start token (its `jti` equals `runs.started_jti`) gets 403 `invocation_token_invalid` with `reason: superseded` (09 section 4.5).'
      x-authz-action: run.start
      x-rate-limit-class: rl.agent_runtime
      x-idempotency-exempt: agent_runtime
      parameters:
      - $ref: '#/components/parameters/RunIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunStart'
            examples:
              default:
                summary: Request
                value:
                  invocation_token: eyJhbGciOiJFZERTQSIsImtpZCI6Imludi0yMDI2LTEwIn0.e30.sig
                  billing_mode_used: personal
                  tool_class_used: scoped
                  external_session_ref: sess-7f3a
                  model: model-large
      responses:
        '200':
          description: 'OK. A repeated start with the same token while the run is `running` answers the current run with `Idempotent-Replay: true` and changes nothing (06 section 7 point 5).'
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            Idempotent-Replay:
              $ref: '#/components/headers/Idempotent-Replay'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Run'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-70cd-8013-27d7ac09a77f
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    triggered_by: 0192f4a0-7b1c-7db7-8afc-99f48568ad99
                    state: running
                    version: 3
                    billing_mode_expected: personal
                    model: model-large
                    status_line: Reading the export logs
                    metrics:
                      context_pct: 37.5
                    cost:
                      amount_micros: 1250000
                      currency: USD
                      is_estimate: true
                    steps:
                      0192f4a0-7b1c-7986-8350-06ee441b527a:
                        id: 0192f4a0-7b1c-70f2-84b9-bdac58ce716b
                        ordinal: 1
                        kind: tool_call
                        name: read_file
                        title: Reading export.log
                        status: pending
                        started_at: '2026-10-10T09:30:00.000Z'
                    followups:
                      0192f4a0-7b1c-77d2-8ef5-9f8e2e1db96b:
                        message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                        state: queued
                    queued_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /runs/{run_id}/invocation-token:
    post:
      operationId: renewInvocationToken
      summary: Get a fresh invocation token for an own waiting run
      tags: [Agent runtime]
      description: |-
        For a run in `queued` or `waiting_input` whose invocation token expired before the start
        (09 section 4.5.1). The body carries the current token; its expiry is not checked, but its
        signature, audience, run and `jti` are: the `jti` equals the current one (`runs.invocation_jti`)
        or the one the last renewal replaced (`runs.renewed_from_jti`, retry after a lost answer), and
        it is not consumed by a start (`runs.started_jti` is NULL or differs, SQL `started_jti IS
        DISTINCT FROM jti`). A retry with the replaced token issues a
        fresh token again; the run still starts at most once. The server repeats every check it runs
        before a delivery (`agent.invoke` for the triggering person with current permissions, I2, SR2,
        SR3); on failure the run becomes `failed` and the answer is 403 `agent_invocation_denied`. A
        replaced or consumed token gets 403 `invocation_token_invalid` with `reason: superseded`; a
        run that already started gets 409 `already_started`.
      x-authz-action: run.token.renew
      x-rate-limit-class: rl.agent_runtime
      x-idempotency-exempt: agent_runtime
      parameters:
      - $ref: '#/components/parameters/RunIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InvocationTokenRenew'
            examples:
              default:
                summary: Request
                value:
                  invocation_token: eyJhbGciOiJFZERTQSIsImtpZCI6Imludi0yMDI2LTEwIn0.e30.sig
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InvocationTokenGrant'
              examples:
                default:
                  summary: Response
                  value:
                    invocation_token: eyJhbGciOiJFZERTQSIsImtpZCI6Imludi0yMDI2LTEwIn0.e30.signature
                    expires_at: '2026-10-10T09:31:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /runs/{run_id}/await-input:
    post:
      operationId: awaitRunInput
      summary: Wait for input from a person
      tags: [Agent runtime]
      x-authz-action: run.finish
      x-rate-limit-class: rl.agent_runtime
      x-idempotency-exempt: agent_runtime
      parameters:
      - $ref: '#/components/parameters/RunIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunAwaitInput'
            examples:
              default:
                summary: Request
                value:
                  status_line: Waiting for approval
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Run'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-70cd-8013-27d7ac09a77f
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    triggered_by: 0192f4a0-7b1c-7db7-8afc-99f48568ad99
                    state: queued
                    version: 3
                    billing_mode_expected: personal
                    model: model-large
                    status_line: Reading the export logs
                    metrics:
                      context_pct: 37.5
                    cost:
                      amount_micros: 1250000
                      currency: USD
                      is_estimate: true
                    steps:
                      0192f4a0-7b1c-7986-8350-06ee441b527a:
                        id: 0192f4a0-7b1c-70f2-84b9-bdac58ce716b
                        ordinal: 1
                        kind: tool_call
                        name: read_file
                        title: Reading export.log
                        status: pending
                        started_at: '2026-10-10T09:30:00.000Z'
                    followups:
                      0192f4a0-7b1c-77d2-8ef5-9f8e2e1db96b:
                        message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                        state: queued
                    queued_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /runs/{run_id}/finish:
    post:
      operationId: finishRun
      summary: End a run
      tags: [Agent runtime]
      description: Repeating the same final state is idempotent and returns 200.
      x-authz-action: run.finish
      x-rate-limit-class: rl.agent_runtime
      x-idempotency-exempt: agent_runtime
      parameters:
      - $ref: '#/components/parameters/RunIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunFinish'
            examples:
              default:
                summary: Request
                value:
                  state: completed
                  reason: adapter_error
                  summary: Found the cause and proposed a fix
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Run'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-70cd-8013-27d7ac09a77f
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    triggered_by: 0192f4a0-7b1c-7db7-8afc-99f48568ad99
                    state: queued
                    version: 3
                    billing_mode_expected: personal
                    model: model-large
                    status_line: Reading the export logs
                    metrics:
                      context_pct: 37.5
                    cost:
                      amount_micros: 1250000
                      currency: USD
                      is_estimate: true
                    steps:
                      0192f4a0-7b1c-7986-8350-06ee441b527a:
                        id: 0192f4a0-7b1c-70f2-84b9-bdac58ce716b
                        ordinal: 1
                        kind: tool_call
                        name: read_file
                        title: Reading export.log
                        status: pending
                        started_at: '2026-10-10T09:30:00.000Z'
                    followups:
                      0192f4a0-7b1c-77d2-8ef5-9f8e2e1db96b:
                        message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                        state: queued
                    queued_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /runs/{run_id}/cancel:
    post:
      operationId: cancelRun
      summary: Cancel a run
      tags: [Runs]
      description: |-
        Writes `run.cancel_requested` for the agent. Without confirmation within `agent.cancel_grace`
        the server cancels the run itself.
      x-authz-action: run.cancel
      x-rate-limit-class: rl.trigger
      parameters:
      - $ref: '#/components/parameters/RunIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunCancel'
            examples:
              default:
                summary: Request
                value:
                  reason: Wrong ticket
      responses:
        '202':
          description: Accepted; processing continues asynchronously.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunSummary'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-767f-8292-c1b14d7d40df
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    triggered_by: 0192f4a0-7b1c-7db7-8afc-99f48568ad99
                    state: queued
                    state_reason: user_cancelled
                    version: 3
                    billing_mode_expected: personal
                    status_line: Reading the export logs
                    summary: Found the cause and proposed a fix
                    cost:
                      amount_micros: 1250000
                      currency: USD
                      is_estimate: true
                    step_count: 1
                    queued_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/commands:
    get:
      operationId: listChannelCommands
      summary: List commands usable in a channel
      tags: [Commands]
      description: |-
        Lists the registered commands offered in the channel: those of the channel's effective operator first, then those
        of guest agents (only with `allow_mentioned_agents`) and of apps with `write` on the channel, an active installation
        and the scope `commands:write`, each group ordered by lower-case name, owner and ID. Per owner and name only the most specific scope appears (`channel` before `space`
        before `instance`). The built-in commands are not part of the list; the client shows them from its own list. A dm
        channel lists nothing.
      x-authz-action: command.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommandList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7beb-80fc-f336f4fe79aa
                      owner_principal_id: 0192f4a0-7b1c-7176-8de0-ddf186d11091
                      name: escalate
                      description: Escalate the ticket to second level
                      scope_kind: instance
                      options:
                      - name: ticket
                        description: Ticket number
                        type: string
                        required: true
                      required_level: write
                      control: false
                      version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /commands:
    post:
      operationId: registerCommand
      summary: Register a command
      tags: [Commands]
      description: |-
        Registers a command of the calling agent or app. A built-in name answers `409 conflict` with `reason:
        command_name_reserved`; a name the owner already uses on the same scope target answers `409 conflict` without a
        reason.
      x-authz-action: command.register
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommandRegister'
            examples:
              default:
                summary: Request
                value:
                  name: escalate
                  description: Escalate the ticket to second level
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Command'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7beb-80fc-f336f4fe79aa
                    owner_principal_id: 0192f4a0-7b1c-7176-8de0-ddf186d11091
                    name: escalate
                    description: Escalate the ticket to second level
                    scope_kind: instance
                    options:
                    - name: ticket
                      description: Ticket number
                      type: string
                      required: true
                    required_level: write
                    control: false
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /commands/{command_id}:
    get:
      operationId: getCommand
      summary: Read a command
      tags: [Commands]
      x-authz-action: command.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/CommandIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Command'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7beb-80fc-f336f4fe79aa
                    owner_principal_id: 0192f4a0-7b1c-7176-8de0-ddf186d11091
                    name: escalate
                    description: Escalate the ticket to second level
                    scope_kind: instance
                    options:
                    - name: ticket
                      description: Ticket number
                      type: string
                      required: true
                    required_level: write
                    control: false
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    put:
      operationId: updateCommand
      summary: Replace a command definition
      tags: [Commands]
      description: |-
        Replaces the definition of a command of the caller. A built-in name answers `409 conflict` with `reason:
        command_name_reserved`; a name the owner already uses on the same scope target answers `409 conflict` without a
        reason. An identical definition answers the command unchanged.
      x-authz-action: command.register
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/CommandIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommandRegister'
            examples:
              default:
                summary: Request
                value:
                  name: escalate
                  description: Escalate the ticket to second level
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Command'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7beb-80fc-f336f4fe79aa
                    owner_principal_id: 0192f4a0-7b1c-7176-8de0-ddf186d11091
                    name: escalate
                    description: Escalate the ticket to second level
                    scope_kind: instance
                    options:
                    - name: ticket
                      description: Ticket number
                      type: string
                      required: true
                    required_level: write
                    control: false
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: removeCommand
      summary: Remove a command
      tags: [Commands]
      x-authz-action: command.remove
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/CommandIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/commands/{command_id}/autocomplete:
    post:
      operationId: autocompleteCommand
      summary: Get suggestions for an option
      tags: [Commands]
      description: |-
        Forwards the query to the command owner and waits up to `command.autocomplete_timeout`; returns
        no choices when the owner does not answer in time. Returns an empty list at once when an agent
        owner, or an app owner without `delivery.interactions_url`, has no live gateway connection, when
        the installation of the owning app stops being active after the check, and for an agent command in a forum channel without
        `thread_id` after the check (R-AZ-26). An app whose manifest declares `delivery.interactions_url`
        gets the query as a signed `POST` to that address and has `app.interaction_timeout` to answer,
        never beyond `command.autocomplete_timeout`: `200` with valid choices returns them, `202` keeps
        the request open for `answerAutocomplete`, any other answer returns an empty list at once (11, 9).
        A command of an agent is checked like `agent.invoke` without a run: a refusal is
        `403 agent_invocation_denied` with `reason` (for example `level_insufficient`,
        `agent_not_reachable`, `billing_mode_personal_not_holder`, `tool_class_owner_only`,
        `project_not_ready`), a read-only or archived channel `403 channel_read_only`. A command of an app
        is checked by its `required_level` and the reachability of the app (active installation,
        `commands:write`, `write` on the channel), never by the `agent.invoke` guard; a missing level is
        `403 agent_invocation_denied` with `reason`, a read-only or archived channel
        `403 channel_read_only`. An unknown, removed or not covered command, an app command whose app is
        not reachable, an unknown channel or a thread of another channel is `404`.
        The query names an option with `autocomplete` (`400 validation_failed` with `unknown_option`,
        `autocomplete_not_enabled` or `duplicate`; `too_long` when the request does not fit
        `sync.event_payload_max_bytes`).
      x-authz-action: command.autocomplete
      x-rate-limit-class: rl.trigger
      x-idempotency-exempt: read_only
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/CommandIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutocompleteQuery'
            examples:
              default:
                summary: Request
                value:
                  option: ticket
                  value: '47'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AutocompleteResult'
              examples:
                default:
                  summary: Response
                  value:
                    choices:
                    - name: High priority
                      value: high
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /channels/{channel_id}/commands/{command_id}/invoke:
    post:
      operationId: invokeCommand
      summary: Run a command
      tags: [Commands]
      x-authz-action: command.invoke
      x-rate-limit-class: rl.trigger
      parameters:
      - $ref: '#/components/parameters/ChannelIdPath'
      - $ref: '#/components/parameters/CommandIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommandInvoke'
            examples:
              default:
                summary: Request
                value: {}
      responses:
        '202':
          description: Accepted; processing continues asynchronously.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CommandInvocation'
              examples:
                default:
                  summary: Response
                  value:
                    invocation_id: 0192f4a0-7b1c-712e-8ccb-17c85f8d11c9
                    command_id: 0192f4a0-7b1c-7beb-80fc-f336f4fe79aa
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    accepted: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /command-autocompletions/{request_id}/answer:
    post:
      operationId: answerAutocomplete
      summary: Answer an autocomplete request
      tags: [Agent runtime]
      description: |-
        The owner of the command answers an open request of `command.autocomplete_requested`; the
        first valid answer goes back to the person. `404` for an unknown or forgotten request,
        `410 gone` after the wait ended, `204` without effect for an answer after the first one.
        `400 validation_failed` for more than `command.autocomplete_max_choices` choices
        (`too_many_items`), an empty name or value (`too_short`) or a name or value that is no display
        text (`invalid_utf8`, `control_character`, `bidi_control`); the request stays open.
      x-authz-action: command.autocomplete.respond
      x-rate-limit-class: rl.agent_runtime
      x-idempotency-exempt: agent_runtime
      parameters:
      - $ref: '#/components/parameters/RequestIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AutocompleteAnswer'
            examples:
              default:
                summary: Request
                value:
                  choices:
                  - name: High priority
                    value: high
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /apps:
    get:
      operationId: listApps
      summary: List installed apps
      tags: [Apps]
      x-authz-action: app.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AppPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                      slug: ticket-sync
                      display_name: Ticket Sync
                      manifest:
                        schema_version: 1
                        slug: ticket-sync
                        name: Ticket Sync
                        description: Mirrors tickets of a help desk into a forum.
                        publisher: Example Ltd
                        version: 1.2.0
                        homepage_url: https://apps.example.com/ticket-sync
                        min_api_version: '0.2'
                        scopes:
                          required: ['messages:write']
                          optional: ['messages:read', 'events:subscribe']
                        events: [message.created]
                        delivery:
                          mode: webhook
                          events_url: https://apps.example.com/ticket-sync/events
                        data_egress: Messages of the granted channels go to apps.example.com.
                      manifest_version: 1.2.0
                      installation:
                        id: 0192f4a0-7b1c-78dc-8da2-d6c386222e8a
                        granted_scopes: ['events:subscribe', 'messages:read', 'messages:write']
                        event_subscriptions: [message.created]
                        state: active
                        installed_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: installApp
      summary: Install an app from its manifest
      tags: [Apps]
      description: |-
        Owner only with a fresh step-up. The manifest is checked against the schema (unknown members give `400 validation_failed` with `unknown_member`) and the check rules of 11, 3.2 (`400 validation_failed` with the pointers under `/manifest`; `400 webhook_host_not_allowed` when every failure is a delivery host outside `webhook.allowed_hosts`). `granted_scopes` holds every required and any optional scope of the manifest; `event_subscriptions` absent means none, each needs `events:subscribe` and the scope of the event. One transaction creates the app principal (handle = `slug`, display name = `name`), the app and its installation and raises the rights version; nothing is stored on a refusal. No token and no grant is created: grants come per target from `createGrant`, tokens from `createToken`. The same `slug` again is `409 conflict` (use `upgradeAppManifest`); a slug whose apps are all uninstalled reinstalls with the same principal.
      x-authz-action: app.install
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppInstall'
            examples:
              default:
                summary: Request
                value:
                  manifest:
                    schema_version: 1
                    slug: ticket-sync
                    name: Ticket Sync
                    description: Mirrors tickets of a help desk into a forum.
                    publisher: Example Ltd
                    version: 1.2.0
                    homepage_url: https://apps.example.com/ticket-sync
                    min_api_version: '0.2'
                    scopes:
                      required: ['messages:write']
                      optional: ['messages:read', 'events:subscribe']
                    events: [message.created]
                    delivery:
                      mode: webhook
                      events_url: https://apps.example.com/ticket-sync/events
                    data_egress: Messages of the granted channels go to apps.example.com.
                  granted_scopes: ['events:subscribe', 'messages:read', 'messages:write']
                  event_subscriptions: [message.created]
                  reason: Sync tickets with the help desk
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/App'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                    slug: ticket-sync
                    display_name: Ticket Sync
                    manifest:
                      schema_version: 1
                      slug: ticket-sync
                      name: Ticket Sync
                      description: Mirrors tickets of a help desk into a forum.
                      publisher: Example Ltd
                      version: 1.2.0
                      homepage_url: https://apps.example.com/ticket-sync
                      min_api_version: '0.2'
                      scopes:
                        required: ['messages:write']
                        optional: ['messages:read', 'events:subscribe']
                      events: [message.created]
                      delivery:
                        mode: webhook
                        events_url: https://apps.example.com/ticket-sync/events
                      data_egress: Messages of the granted channels go to apps.example.com.
                    manifest_version: 1.2.0
                    installation:
                      id: 0192f4a0-7b1c-78dc-8da2-d6c386222e8a
                      granted_scopes: ['events:subscribe', 'messages:read', 'messages:write']
                      event_subscriptions: [message.created]
                      state: active
                      installed_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /app-manifests/validate:
    post:
      operationId: validateManifest
      summary: Validate a manifest without installing
      tags: [Apps]
      description: |-
        Owner only. Schema violations give `400 validation_failed`; the check rules of 11, 3.2 give `200` with `valid` false and every rule error, pointers relative to the manifest root. Nothing is stored and no audit entry is written.
      x-authz-action: app.read
      x-rate-limit-class: rl.read
      x-idempotency-exempt: read_only
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppManifest'
            examples:
              default:
                summary: Request
                value:
                  schema_version: 1
                  slug: ticket-sync
                  name: Ticket Sync
                  description: Mirrors tickets of a help desk into a forum.
                  publisher: Example Ltd
                  version: 1.2.0
                  homepage_url: https://apps.example.com/ticket-sync
                  min_api_version: '0.2'
                  scopes:
                    required: ['messages:write']
                    optional: ['messages:read', 'events:subscribe']
                  events: [message.created]
                  delivery:
                    mode: webhook
                    events_url: https://apps.example.com/ticket-sync/events
                  data_egress: Messages of the granted channels go to apps.example.com.
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ManifestValidation'
              examples:
                default:
                  summary: Response
                  value:
                    valid: false
                    errors:
                    - pointer: /delivery/events_url
                      code: webhook_host_not_allowed
                      detail: The host is not in webhook.allowed_hosts (R-DM-17, 11 section 3.2 rule 3).
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /apps/{app_id}:
    get:
      operationId: getApp
      summary: Read an app
      tags: [Apps]
      x-authz-action: app.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/AppIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/App'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                    slug: ticket-sync
                    display_name: Ticket Sync
                    manifest:
                      schema_version: 1
                      slug: ticket-sync
                      name: Ticket Sync
                      description: Mirrors tickets of a help desk into a forum.
                      publisher: Example Ltd
                      version: 1.2.0
                      homepage_url: https://apps.example.com/ticket-sync
                      min_api_version: '0.2'
                      scopes:
                        required: ['messages:write']
                        optional: ['messages:read', 'events:subscribe']
                      events: [message.created]
                      delivery:
                        mode: webhook
                        events_url: https://apps.example.com/ticket-sync/events
                      data_egress: Messages of the granted channels go to apps.example.com.
                    manifest_version: 1.2.0
                    installation:
                      id: 0192f4a0-7b1c-78dc-8da2-d6c386222e8a
                      granted_scopes: ['events:subscribe', 'messages:read', 'messages:write']
                      event_subscriptions: [message.created]
                      state: active
                      installed_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /apps/{app_id}/manifest:
    put:
      operationId: upgradeAppManifest
      summary: Install a new manifest version
      tags: [Apps]
      description: |-
        Owner only with a fresh step-up (`app.install`). The new manifest has the same `slug` and passes the rules of `installApp`; changed content needs another `version` (`400` at `/manifest/version`). The upgrade never adds a scope, an event or a grant: `granted_scopes` and `event_subscriptions` are subsets of the current ones (a new scope gives `400` at `/granted_scopes/<i>`; consent with `setAppScopes`), and `event_subscriptions` absent keeps the current ones the new manifest and scopes still allow. What the new manifest drops acts at once. New delivery hosts are confirmed by the upgrade itself and must be in `webhook.allowed_hosts`. An identical manifest with identical lists answers `200` without effect; an uninstalled app is `409 conflict`.
      x-authz-action: app.install
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AppIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppInstall'
            examples:
              default:
                summary: Request
                value:
                  manifest:
                    schema_version: 1
                    slug: ticket-sync
                    name: Ticket Sync
                    description: Mirrors tickets of a help desk into a forum.
                    publisher: Example Ltd
                    version: 1.2.0
                    homepage_url: https://apps.example.com/ticket-sync
                    min_api_version: '0.2'
                    scopes:
                      required: ['messages:write']
                      optional: ['messages:read', 'events:subscribe']
                    events: [message.created]
                    delivery:
                      mode: webhook
                      events_url: https://apps.example.com/ticket-sync/events
                    data_egress: Messages of the granted channels go to apps.example.com.
                  granted_scopes: ['events:subscribe', 'messages:read', 'messages:write']
                  event_subscriptions: [message.created]
                  reason: Sync tickets with the help desk
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/App'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                    slug: ticket-sync
                    display_name: Ticket Sync
                    manifest:
                      schema_version: 1
                      slug: ticket-sync
                      name: Ticket Sync
                      description: Mirrors tickets of a help desk into a forum.
                      publisher: Example Ltd
                      version: 1.2.0
                      homepage_url: https://apps.example.com/ticket-sync
                      min_api_version: '0.2'
                      scopes:
                        required: ['messages:write']
                        optional: ['messages:read', 'events:subscribe']
                      events: [message.created]
                      delivery:
                        mode: webhook
                        events_url: https://apps.example.com/ticket-sync/events
                      data_egress: Messages of the granted channels go to apps.example.com.
                    manifest_version: 1.2.0
                    installation:
                      id: 0192f4a0-7b1c-78dc-8da2-d6c386222e8a
                      granted_scopes: ['events:subscribe', 'messages:read', 'messages:write']
                      event_subscriptions: [message.created]
                      state: active
                      installed_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /apps/{app_id}/scopes:
    put:
      operationId: setAppScopes
      summary: Change granted scopes and subscriptions
      tags: [Apps]
      description: |-
        Owner only with a fresh step-up; the consent for new scopes and events. `granted_scopes` is a subset of the scopes of the manifest; `event_subscriptions` absent keeps the current ones the new scopes still allow, present it replaces them (each needs `events:subscribe` and the scope of the event). A removed scope acts on the next request of every token; a newly granted scope needs a new token, because a token keeps the scopes it was created with. Equal lists answer `200` without effect; an uninstalled app is `409 conflict`.
      x-authz-action: app.scopes.update
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AppIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppScopesPut'
            examples:
              default:
                summary: Request
                value:
                  granted_scopes: ['events:subscribe', 'messages:read', 'messages:write']
                  event_subscriptions: [message.created]
                  reason: Mirror replies into the help desk
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/App'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                    slug: ticket-sync
                    display_name: Ticket Sync
                    manifest:
                      schema_version: 1
                      slug: ticket-sync
                      name: Ticket Sync
                      description: Mirrors tickets of a help desk into a forum.
                      publisher: Example Ltd
                      version: 1.2.0
                      homepage_url: https://apps.example.com/ticket-sync
                      min_api_version: '0.2'
                      scopes:
                        required: ['messages:write']
                        optional: ['messages:read', 'events:subscribe']
                      events: [message.created]
                      delivery:
                        mode: webhook
                        events_url: https://apps.example.com/ticket-sync/events
                      data_egress: Messages of the granted channels go to apps.example.com.
                    manifest_version: 1.2.0
                    installation:
                      id: 0192f4a0-7b1c-78dc-8da2-d6c386222e8a
                      granted_scopes: ['events:subscribe', 'messages:read', 'messages:write']
                      event_subscriptions: [message.created]
                      state: active
                      installed_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /apps/{app_id}/suspend:
    post:
      operationId: suspendApp
      summary: Suspend an app
      tags: [Apps]
      description: |-
        Emergency stop without losing grants: tokens blocked, deliveries paused, commands hidden, components disabled (11, 6).
      x-authz-action: app.suspend
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AppIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /apps/{app_id}/resume:
    post:
      operationId: resumeApp
      summary: Resume a suspended app
      tags: [Apps]
      x-authz-action: app.resume
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AppIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /apps/{app_id}/uninstall:
    post:
      operationId: uninstallApp
      summary: Uninstall an app
      tags: [Apps]
      x-authz-action: app.uninstall
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AppIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Left the team
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /apps/{app_id}/webhooks:
    get:
      operationId: listWebhookSubscriptions
      summary: List outgoing webhook subscriptions
      tags: [Webhooks]
      x-authz-action: webhook_out.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/AppIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7e12-8162-c8db90be34b1
                      app_id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                      url: https://hooks.example.com/chat
                      event_types: [message.created]
                      state: active
                      consecutive_failures: 1
                      signature_schemes: [v1a]
                      signing_key_kid: whk-2026-10
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createWebhookSubscription
      summary: Subscribe a URL to events
      tags: [Webhooks]
      description: The host and port must be in `webhook.allowed_hosts`; otherwise 400 `webhook_host_not_allowed`. With `enable_hmac` the response carries the `v1` secret once; it is a one-time secret, so a replay with the same `Idempotency-Key` answers 409 `conflict` with `reason` `one_time_secret`.
      x-authz-action: webhook_out.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/AppIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionCreate'
            examples:
              default:
                summary: Request
                value:
                  url: https://hooks.example.com/chat
                  event_types: [message.created]
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionCreated'
              examples:
                default:
                  summary: Response
                  value:
                    subscription:
                      id: 0192f4a0-7b1c-7e12-8162-c8db90be34b1
                      app_id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                      url: https://hooks.example.com/chat
                      event_types: [message.created]
                      state: active
                      consecutive_failures: 1
                      signature_schemes: [v1a]
                      signing_key_kid: whk-2026-10
                      created_at: '2026-10-10T09:30:00.000Z'
                    hmac_secret: whsec_EXAMPLE
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /webhooks/{subscription_id}:
    patch:
      operationId: updateWebhookSubscription
      summary: Change, pause or rotate a subscription
      tags: [Webhooks]
      description: A new host must be in `webhook.allowed_hosts`. `rotate_hmac_secret` returns the new `v1` secret once; it is a one-time secret, so a replay with the same `Idempotency-Key` answers 409 `conflict` with `reason` `one_time_secret`.
      x-authz-action: webhook_out.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/SubscriptionIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookSubscriptionUpdate'
            examples:
              default:
                summary: Request
                value: {}
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookSubscriptionCreated'
              examples:
                default:
                  summary: Response
                  value:
                    subscription:
                      id: 0192f4a0-7b1c-7e12-8162-c8db90be34b1
                      app_id: 0192f4a0-7b1c-7895-82f8-463a64ff0fbe
                      url: https://hooks.example.com/chat
                      event_types: [message.created]
                      state: active
                      consecutive_failures: 1
                      signature_schemes: [v1a]
                      signing_key_kid: whk-2026-10
                      created_at: '2026-10-10T09:30:00.000Z'
                    hmac_secret: whsec_EXAMPLE
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteWebhookSubscription
      summary: Delete a subscription
      tags: [Webhooks]
      x-authz-action: webhook_out.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/SubscriptionIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /webhooks/{subscription_id}/deliveries:
    get:
      operationId: listWebhookDeliveries
      summary: List delivery attempts
      tags: [Webhooks]
      x-authz-action: webhook_out.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/SubscriptionIdPath'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: status
        in: query
        description: Filter by status.
        schema:
          $ref: '#/components/schemas/WebhookDeliveryStatus'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDeliveryPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-73de-870f-6ce6fa448f1e
                      subscription_id: 0192f4a0-7b1c-756c-88a8-bf72a9fa67f3
                      event_id: 0192f4a0-7b1c-757c-87c8-267db4b2c89f
                      event_seq: 5301
                      event_type: message.created
                      attempt: 1
                      status: pending
                      last_error: timeout
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /webhook-deliveries/{delivery_id}/redeliver:
    post:
      operationId: redeliverWebhook
      summary: Deliver an event again
      tags: [Webhooks]
      x-authz-action: webhook_out.redeliver
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/DeliveryIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Accepted; processing continues asynchronously.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDelivery'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-73de-870f-6ce6fa448f1e
                    subscription_id: 0192f4a0-7b1c-756c-88a8-bf72a9fa67f3
                    event_id: 0192f4a0-7b1c-757c-87c8-267db4b2c89f
                    event_seq: 5301
                    event_type: message.created
                    attempt: 1
                    status: pending
                    last_error: timeout
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /audit/events:
    get:
      operationId: listAuditEvents
      summary: Query the audit log
      tags: [Audit]
      x-authz-action: audit.read
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: actor_id
        in: query
        description: Actor principal.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: action
        in: query
        description: Action ID.
        schema:
          type: string
      - name: decision
        in: query
        description: Decision.
        schema:
          $ref: '#/components/schemas/AuditDecision'
      - name: resource_kind
        in: query
        description: Resource kind.
        schema:
          type: string
      - name: resource_id
        in: query
        description: Resource ID.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: from
        in: query
        description: Occurred at or after.
        schema:
          $ref: '#/components/schemas/Timestamp'
      - name: to
        in: query
        description: Occurred before.
        schema:
          $ref: '#/components/schemas/Timestamp'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditEventPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - seq: 5301
                      id: 0192f4a0-7b1c-7cb3-8665-8fa2cdd01d12
                      occurred_at: '2026-10-10T09:30:00.000Z'
                      action: grant.create
                      decision: allow
                      resource_kind: grant
                      details: {}
                      prev_hash: text
                      hash: text
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /audit/export:
    get:
      operationId: exportAuditEvents
      summary: Export the audit log
      tags: [Audit]
      description: Streams one `AuditEvent` per line in ascending `seq`, with hashes for offline verification. The stream is the contiguous `seq` span from the smallest to the largest `seq` whose `occurred_at` lies in [`from`, `to`), so every line links the line before by `prev_hash`; a row inside the span whose `occurred_at` lies outside the range (an aggregated denial keeps the time of its first denial) is part of it. A gap inside the stream breaks the hash links (`prev_hash`); an error after the first line aborts the transfer instead of ending it, so a client sees a broken response, not a shorter complete one.
      x-authz-action: audit.read
      x-rate-limit-class: rl.admin
      parameters:
      - name: from
        in: query
        description: Start of the range (inclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: to
        in: query
        description: End of the range (exclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      responses:
        '200':
          description: OK. One JSON object per line (`application/x-ndjson`).
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/x-ndjson:
              schema:
                $ref: '#/components/schemas/AuditEvent'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /audit/verify:
    post:
      operationId: verifyAuditChain
      summary: Verify the hash chain
      tags: [Audit]
      x-authz-action: audit.verify
      x-rate-limit-class: rl.admin
      x-idempotency-exempt: read_only
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AuditVerifyRequest'
            examples:
              default:
                summary: Request
                value: {}
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuditVerifyResult'
              examples:
                default:
                  summary: Response
                  value:
                    ok: true
                    checked_from_seq: 1
                    checked_to_seq: 5301
                    checkpoints_compared: 12
                    last_checkpoint_seq: 5290
                    warnings: []
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /stats/usage:
    get:
      operationId: getUsageStats
      summary: Usage per space
      tags: [Stats]
      description: 'Metrics per space and bucket: `messages` (messages not deleted) and by origin `messages_human`, `messages_agent`, `messages_app`, `messages_import`; `threads_created`, `threads_done` (threads closed in the bucket); `runs` (runs that ended in the bucket); `active_people` (distinct people with at least one message, only for granularity `week` or `month`; below `privacy.stats_min_group_size` the value is withheld and named in `below_min_group`); `storage_bytes` (current sum of the files of the space, in the last bucket only). Grouped by space (`group_key` is the space ID). Authorized with `stats.read` on the instance (owner) or, with `space_id`, on that space (`manage`); without `space_id` and without owner status `403`, an unreadable space `404`. Every number of a channel counts only the channels the caller may read (`message.read`), intersected with the space; channels without a space (DMs) are not counted. No metric is grouped by person and no person is named. `from` and `to` are aligned down and up to the granularity (default `day`): `hour` in UTC, `day`, `week` (ISO, Monday) and `month` from local midnight of `stats.time_zone`, returned as UTC instants; a range longer than `stats.max_range`, with more than `api.stats_max_buckets` buckets or with `from >= to` is `400 validation_failed`. Buckets without a value are left out. The result is cached per caller scope for `stats.cache_ttl`.'
      x-authz-action: stats.read
      x-rate-limit-class: rl.read
      parameters:
      - name: from
        in: query
        description: Start of the range (inclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: to
        in: query
        description: End of the range (exclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: granularity
        in: query
        description: Bucket size; default `day`.
        schema:
          $ref: '#/components/schemas/StatsGranularity'
      - name: space_id
        in: query
        description: Restrict to a space; required without owner role.
        schema:
          $ref: '#/components/schemas/Uuid'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsSeries'
              examples:
                default:
                  summary: Response
                  value:
                    granularity: hour
                    buckets:
                    - start: '2026-10-10T09:30:00.000Z'
                      group_key: 0192f4a0-7b1c-7000-8a00-000000000101
                      metrics: {}
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /stats/runs:
    get:
      operationId: getRunStats
      summary: Runs per agent
      tags: [Stats]
      description: 'Metrics per agent and bucket of the end of a run: `runs`, `completed`, `failed`, `cancelled`, `duration_ms_p50`, `duration_ms_p95` (from start to end of the runs that started), and `failed_reason_<code>` for every `state_reason` code of failed runs that occurs. Grouped by agent (`group_key` is the agent ID). Authorized with `stats.read` on the instance (owner) or, with `space_id`, on that space (`manage`); without `space_id` and without owner status `403`, an unreadable space `404`. Every number of a channel counts only the channels the caller may read (`message.read`), intersected with the space; channels without a space (DMs) are not counted. No metric is grouped by person and no person is named. `from` and `to` are aligned down and up to the granularity (default `day`): `hour` in UTC, `day`, `week` (ISO, Monday) and `month` from local midnight of `stats.time_zone`, returned as UTC instants; a range longer than `stats.max_range`, with more than `api.stats_max_buckets` buckets or with `from >= to` is `400 validation_failed`. Buckets without a value are left out. The result is cached per caller scope for `stats.cache_ttl`.'
      x-authz-action: stats.read
      x-rate-limit-class: rl.read
      parameters:
      - name: from
        in: query
        description: Start of the range (inclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: to
        in: query
        description: End of the range (exclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: granularity
        in: query
        description: Bucket size; default `day`.
        schema:
          $ref: '#/components/schemas/StatsGranularity'
      - name: space_id
        in: query
        description: Restrict to a space.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: agent_id
        in: query
        description: Restrict to an agent.
        schema:
          $ref: '#/components/schemas/Uuid'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsSeries'
              examples:
                default:
                  summary: Response
                  value:
                    granularity: hour
                    buckets:
                    - start: '2026-10-10T09:30:00.000Z'
                      group_key: 0192f4a0-7b1c-7000-8a00-000000000101
                      metrics: {}
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /stats/tool-calls:
    get:
      operationId: getToolCallStats
      summary: Tool calls by tool name
      tags: [Stats]
      description: 'Metrics per tool name and bucket of the start of the call: `calls`, `failed`, `duration_ms_p50` (finished calls). Grouped by the tool name from `run_steps` (`group_key`); no step detail, no input and no output. Authorized with `stats.read` on the instance (owner) or, with `space_id`, on that space (`manage`); without `space_id` and without owner status `403`, an unreadable space `404`. Every number of a channel counts only the channels the caller may read (`message.read`), intersected with the space; channels without a space (DMs) are not counted. No metric is grouped by person and no person is named. `from` and `to` are aligned down and up to the granularity (default `day`): `hour` in UTC, `day`, `week` (ISO, Monday) and `month` from local midnight of `stats.time_zone`, returned as UTC instants; a range longer than `stats.max_range`, with more than `api.stats_max_buckets` buckets or with `from >= to` is `400 validation_failed`. Buckets without a value are left out. The result is cached per caller scope for `stats.cache_ttl`.'
      x-authz-action: stats.read
      x-rate-limit-class: rl.read
      parameters:
      - name: from
        in: query
        description: Start of the range (inclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: to
        in: query
        description: End of the range (exclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: granularity
        in: query
        description: Bucket size; default `day`.
        schema:
          $ref: '#/components/schemas/StatsGranularity'
      - name: space_id
        in: query
        description: Restrict to a space.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: agent_id
        in: query
        description: Restrict to an agent.
        schema:
          $ref: '#/components/schemas/Uuid'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsSeries'
              examples:
                default:
                  summary: Response
                  value:
                    granularity: hour
                    buckets:
                    - start: '2026-10-10T09:30:00.000Z'
                      group_key: 0192f4a0-7b1c-7000-8a00-000000000101
                      metrics: {}
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /stats/errors:
    get:
      operationId: getErrorStats
      summary: Error rates of runs and deliveries
      tags: [Stats]
      description: 'Metrics per bucket: `run_failure_rate` (failed per ended runs), `invocation_denials` (messages refused with `billing_mode_personal_not_holder` plus runs failed with `billing_mode_mismatch`); only for the instance (owner, without `space_id`): `webhook_failure_rate` (`failed` and `dead` per finished deliveries still within their retention), `push_failure_rate_<provider>` (registrations disabled for a provider reason per registrations active in the bucket), `scan_errors` and `scan_infected`; these have no channel and count the whole instance. A rate without a denominator is left out. Without `space_id` there is no `group_key`; with `space_id` it is the space ID. HTTP error rates come from telemetry, not from this API. Authorized with `stats.read` on the instance (owner) or, with `space_id`, on that space (`manage`); without `space_id` and without owner status `403`, an unreadable space `404`. Every number of a channel counts only the channels the caller may read (`message.read`), intersected with the space; channels without a space (DMs) are not counted. No metric is grouped by person and no person is named. `from` and `to` are aligned down and up to the granularity (default `day`): `hour` in UTC, `day`, `week` (ISO, Monday) and `month` from local midnight of `stats.time_zone`, returned as UTC instants; a range longer than `stats.max_range`, with more than `api.stats_max_buckets` buckets or with `from >= to` is `400 validation_failed`. Buckets without a value are left out. The result is cached per caller scope for `stats.cache_ttl`.'
      x-authz-action: stats.read
      x-rate-limit-class: rl.read
      parameters:
      - name: from
        in: query
        description: Start of the range (inclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: to
        in: query
        description: End of the range (exclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: granularity
        in: query
        description: Bucket size; default `day`.
        schema:
          $ref: '#/components/schemas/StatsGranularity'
      - name: space_id
        in: query
        description: Restrict to a space.
        schema:
          $ref: '#/components/schemas/Uuid'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsSeries'
              examples:
                default:
                  summary: Response
                  value:
                    granularity: hour
                    buckets:
                    - start: '2026-10-10T09:30:00.000Z'
                      group_key: 0192f4a0-7b1c-7000-8a00-000000000101
                      metrics: {}
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /stats/costs:
    get:
      operationId: getCostStats
      summary: Costs per space, agent or instance
      tags: [Stats]
      description: 'Valid cost sums (entries without a successor) per bucket and `billing_mode`, grouped by `group_by` (`space`, `agent`, `none`; default `none`). Authorized with `cost.read.aggregate` on the instance (owner) or, with `space_id`, on that space (`manage`); without `space_id` and without owner status `403`, an unreadable space `404`; only channels the caller may read count. `personal` and `organization` are never summed together; amounts of mode `personal` are information only and appear only when `billing.show_personal_estimates` is on. `is_estimate` is true when any summed entry is an estimate. One currency per response: `currency` selects it; without it a range whose entries have one currency answers in that currency (`USD` when there is none), and a range with several currencies is `400 validation_failed` with `errors[].code` `currency_required` and the currencies found in `detail`. Ranges, buckets, time zone and cache as for `getUsageStats`.'
      x-authz-action: cost.read.aggregate
      x-rate-limit-class: rl.read
      parameters:
      - name: from
        in: query
        description: Start of the range (inclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: to
        in: query
        description: End of the range (exclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: granularity
        in: query
        description: Bucket size; default `day`.
        schema:
          $ref: '#/components/schemas/StatsGranularity'
      - name: group_by
        in: query
        description: Grouping of the buckets; default `none`.
        schema:
          type: string
          enum: [space, agent, none]
      - name: space_id
        in: query
        description: Restrict to a space.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: agent_id
        in: query
        description: Restrict to an agent.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: currency
        in: query
        description: ISO 4217 code of the costs to sum; required when the range has costs in several currencies.
        schema:
          type: string
          pattern: '^[A-Z]{3}$'
          examples: [USD]
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CostSeries'
              examples:
                default:
                  summary: Response
                  value:
                    granularity: hour
                    currency: USD
                    buckets:
                    - start: '2026-10-10T09:30:00.000Z'
                      billing_mode: personal
                      amount_micros: 1
                      is_estimate: true
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /stats/live:
    get:
      operationId: getStatsLive
      summary: Current live frame
      tags: [Stats]
      description: |-
        The latest live frame (`StatsLiveFrame`) of the instance or of one space, the same frame the gateway sends as `stats.live` (docs/spec/25-visualization.md, section 5). The server returns the base frame built for the current `stats.live_interval`; polling does not rebuild it. Filtered to the channels visible to the caller; cost members only with `cost.read.aggregate`. `403 feature_disabled` when `stats.live_enabled` is off.
      x-authz-action: stats.read
      x-rate-limit-class: rl.read
      parameters:
      - name: space_id
        in: query
        description: Restrict to a space; required without owner role.
        schema:
          $ref: '#/components/schemas/Uuid'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsLiveFrame'
              examples:
                default:
                  summary: Response
                  value:
                    at: '2026-10-10T09:30:00.000Z'
                    interval_ms: 1000
                    scope:
                      kind: instance
                    runs:
                    - run_id: 0192f4a0-7b1c-70cd-8013-27d7ac09a77f
                      thread_id: 0192f4a0-7b1c-7000-8a00-000000000301
                      channel_id: 0192f4a0-7b1c-7000-8a00-000000000201
                      space_id: 0192f4a0-7b1c-7000-8a00-000000000101
                      agent_id: 0192f4a0-7b1c-7000-8a00-000000000401
                      state: running
                      queued_at: '2026-10-10T09:29:10.000Z'
                      started_at: '2026-10-10T09:29:11.000Z'
                      billing_mode: organization
                      step:
                        kind: tool_call
                        name: Bash
                      approval_pending: false
                      cost_micros: 420000
                      currency: USD
                      cost_is_estimate: true
                    runs_truncated: false
                    agents:
                    - agent_id: 0192f4a0-7b1c-7000-8a00-000000000401
                      presence: online
                      billing_mode: organization
                      reported_billing_mode: organization
                      rate_limit:
                        state: ok
                    rates:
                    - metric: messages_created
                      last_minute: 14
                      current_minute: 3
                    sli:
                    - metric: sli.delivery_latency
                      total: 5210
                      good: 5207
                      ratio: 0.99942
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /stats/timeline:
    get:
      operationId: getStatsTimeline
      summary: Minute buckets and runs for playback
      tags: [Stats]
      description: |-
        Minute aggregates from `stats_minutes` and run intervals for a range of at most `stats.timeline_max_range` inside `retention.stats_minutes` (docs/spec/25-visualization.md, section 6.5). Filtered to the channels visible to the caller. Rows without a channel (`sli.*`, gateway metrics) only with `stats.read` on the instance; `cost` only with `cost.read.aggregate`.
      x-authz-action: stats.read
      x-rate-limit-class: rl.read
      parameters:
      - name: from
        in: query
        description: Start of the range (inclusive), a full minute.
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: to
        in: query
        description: End of the range (exclusive), a full minute.
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: space_id
        in: query
        description: Restrict to a space; required without owner role.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: metric
        in: query
        description: Metrics to return; absent means all metrics the caller may see.
        style: form
        explode: true
        schema:
          type: array
          items:
            $ref: '#/components/schemas/StatsMetric'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsTimeline'
              examples:
                default:
                  summary: Response
                  value:
                    from: '2026-10-10T00:00:00.000Z'
                    to: '2026-10-11T00:00:00.000Z'
                    buckets:
                    - start: '2026-10-10T09:30:00.000Z'
                      metric: runs_finished
                      channel_id: 0192f4a0-7b1c-7000-8a00-000000000201
                      agent_id: 0192f4a0-7b1c-7000-8a00-000000000401
                      dimension: completed
                      count: 2
                      sum: 84000
                      good: 0
                    runs:
                    - run_id: 0192f4a0-7b1c-70cd-8013-27d7ac09a77f
                      channel_id: 0192f4a0-7b1c-7000-8a00-000000000201
                      agent_id: 0192f4a0-7b1c-7000-8a00-000000000401
                      queued_at: '2026-10-10T09:29:10.000Z'
                      started_at: '2026-10-10T09:29:11.000Z'
                      finished_at: '2026-10-10T09:30:35.000Z'
                      state: completed
                    runs_truncated: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /stats/slo:
    get:
      operationId: getStatsSlo
      summary: SLO traffic light from own measurement
      tags: [Stats]
      description: |-
        One entry per SLO of the Sloth files, computed from `stats_minutes` over the SLO period (docs/spec/25-visualization.md, section 6.4). Label "own measurement": values may differ from Prometheus. Requires `stats.read` on the instance (owner); a space manager gets `403 forbidden`.
      x-authz-action: stats.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsSloReport'
              examples:
                default:
                  summary: Response
                  value:
                    at: '2026-10-10T09:30:00.000Z'
                    measured_by: own
                    items:
                    - slo: bothy-sync/delivery-latency
                      metric: sli.delivery_latency
                      objective: 99.5
                      threshold_ms: 500
                      period_days: 30
                      total: 1250000
                      good: 1246100
                      ratio: 0.99688
                      budget_remaining: 0.376
                      state: green
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /stats/backends:
    get:
      operationId: getStatsBackends
      summary: Backend policy view per agent
      tags: [Stats]
      description: |-
        Per agent: configured and reported billing mode, supported modes, switches, denied invocations (I2, I3), cost per mode and the rate-limit state reported by the adapter (docs/spec/25-visualization.md, section 6.2). Agents of channels visible to the caller; cost members only with `cost.read.aggregate`. Never credentials, subscriptions or runtime names of an instance.
      x-authz-action: stats.read
      x-rate-limit-class: rl.read
      parameters:
      - name: from
        in: query
        description: Start of the range (inclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: to
        in: query
        description: End of the range (exclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: space_id
        in: query
        description: Restrict to a space; required without owner role.
        schema:
          $ref: '#/components/schemas/Uuid'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatsBackendReport'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - agent_id: 0192f4a0-7b1c-7000-8a00-000000000401
                      billing_mode: organization
                      reported_billing_mode: organization
                      billing_modes_supported: [organization]
                      switches: 1
                      invocations_denied:
                        i2_denied: 0
                        billing_mode_mismatch: 0
                      costs:
                      - billing_mode: organization
                        currency: USD
                        amount_micros: 12400000
                        is_estimate: true
                      rate_limit:
                        state: near_limit
                        resets_at: '2026-10-10T10:00:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /prices:
    get:
      operationId: listPrices
      summary: List prices
      description: Every row of the price table, closed ones included, ordered by `provider`, `model`, `unit`, `valid_from` and `id`; `valid_to` is absent while a row is open. A small unpaginated list (06, 6 point 6). Owner only; an owner's personal token gets 403.
      tags: [Stats]
      x-authz-action: price.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PriceList'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7096-8437-e1ee469c906c
                      provider: anthropic
                      model: model-large
                      unit: input_mtok
                      price_micros: 1
                      currency: USD
                      valid_from: '2026-10-10T09:30:00.000Z'
                      source_url: https://provider.example.com/pricing
                      retrieved_at: '2026-10-10T09:30:00.000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createPrice
      summary: Add a price with source
      description: 'Owner with step-up, audited with row, source and retrieval time (10, 14). `source_url` is an absolute https URL without user info and fragment, `retrieved_at` not in the future, `currency` an ISO 4217 code. 409 `conflict` with `reason` `price_overlap` when the validity overlaps a row of the same provider, model and unit, `price_currency_mismatch` when an overlapping row of the same provider and model has another currency (one currency per price set).'
      tags: [Stats]
      x-authz-action: price.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PriceCreate'
            examples:
              default:
                summary: Request
                value:
                  provider: anthropic
                  model: model-large
                  unit: input_mtok
                  price_micros: 1
                  currency: USD
                  valid_from: '2026-10-10T09:30:00.000Z'
                  source_url: https://provider.example.com/pricing
                  retrieved_at: '2026-10-10T09:30:00.000Z'
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Price'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7096-8437-e1ee469c906c
                    provider: anthropic
                    model: model-large
                    unit: input_mtok
                    price_micros: 1
                    currency: USD
                    valid_from: '2026-10-10T09:30:00.000Z'
                    source_url: https://provider.example.com/pricing
                    retrieved_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /prices/{price_id}/close:
    post:
      operationId: closePrice
      summary: End the validity of a price
      description: 'Sets `valid_to` of an open row, the only change a row ever gets (R-DM-23). The same end again answers 200 without change. 409 `conflict` with `reason` `price_closed` when the row is already closed with another end (a closed row is never reopened or moved; a gap is filled by a new row), `price_in_use` when a `computed` cost entry at or after the new end used the row.'
      tags: [Stats]
      x-authz-action: price.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/PriceIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PriceClose'
            examples:
              default:
                summary: Request
                value:
                  valid_to: '2026-10-10T09:30:00.000Z'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Price'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7096-8437-e1ee469c906c
                    provider: anthropic
                    model: model-large
                    unit: input_mtok
                    price_micros: 1
                    currency: USD
                    valid_from: '2026-10-10T09:30:00.000Z'
                    source_url: https://provider.example.com/pricing
                    retrieved_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /costs/reconcile:
    post:
      operationId: reconcileCosts
      summary: Reconcile estimates with invoices
      tags: [Stats]
      description: |-
        For an owner in a session with a fresh step-up; app, agent and personal tokens are refused (05, R-AZ-03). Stores new `reconciled` entries next to the estimates and never changes or deletes an entry (10, 12.4). An entry is matched when its run has at least one valid cost entry with `occurred_at` in the half-open period `[period_start, period_end)` and all of them have mode `organization`, the given `provider` and the entry's `currency`; otherwise its run ID is listed in `unmatched_run_ids`. The amount of a matched run is split over its valid entries in the period in proportion to their amounts, the remainder on the largest entry; a run whose valid entries in the period are already reconciled with the same sum is not written again. The job `billing.reconcile` then applies the entries to the run and agent session totals.
      x-authz-action: cost.reconcile
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CostReconcile'
            examples:
              default:
                summary: Request
                value:
                  provider: anthropic
                  period_start: '2026-10-10T09:30:00.000Z'
                  period_end: '2026-10-10T09:30:00.000Z'
                  entries:
                  - run_id: 0192f4a0-7b1c-70cd-8013-27d7ac09a77f
                    amount_micros: 1
                    currency: USD
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CostReconcileResult'
              examples:
                default:
                  summary: Response
                  value:
                    reconciled: 1
                    unmatched_run_ids: [0192f4a0-7b1c-7250-807f-c268a7b21797]
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /costs/reconciliations:
    post:
      operationId: submitCostReconciliation
      summary: Submit reconciled cost entries (app)
      tags: [Stats]
      description: |-
        For the reconciliation app with scope `costs:reconcile`, without step-up. Only adds `reconciled` entries to existing runs; deletes nothing (10, 12.4). Matching, period, split and repeat rules as `reconcileCosts`: an entry is matched when its run has at least one valid cost entry with `occurred_at` in `[period_start, period_end)` and all of them have mode `organization`, the given `provider` and the entry's `currency`; unmatched run IDs are listed in `unmatched_run_ids`; a run already reconciled with the same sum is not written again. The job `billing.reconcile` then applies the entries to the run and agent session totals.
      x-authz-action: cost.reconcile.submit
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CostReconcile'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CostReconcileResult'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /costs/estimates:
    get:
      operationId: listCostEstimates
      summary: Cost estimates per run for reconciliation
      tags: [Stats]
      description: |-
        For the reconciliation app with scope `costs:reconcile` and for owners in a session, without step-up (05, R-AZ-28). One item per run of the given agents with cost entries of mode `organization` whose `occurred_at` lies in the range, sorted by `run_id`; runs whose entries in the range mix currencies are left out. Entries of mode `personal` never appear (03 `cost_entries`). Items name no channel, thread or content; the list is evaluated on the instance and not filtered by visible channels (06, 4 point 2).
      x-authz-action: cost.reconcile.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: from
        in: query
        description: Start of the range (inclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: to
        in: query
        description: End of the range (exclusive).
        schema:
          $ref: '#/components/schemas/Timestamp'
        required: true
      - name: agent_id
        in: query
        description: Agents whose runs are listed; repeat the parameter for several agents.
        required: true
        schema:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/Uuid'
        style: form
        explode: true
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CostEstimatePage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - run_id: 0192f4a0-7b1c-70cd-8013-27d7ac09a77f
                      agent_id: 0192f4a0-7b1c-7250-807f-c268a7b21797
                      provider: anthropic
                      currency: USD
                      estimate_micros: 1250000
                      reconciled: false
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /status:
    get:
      operationId: getStatus
      summary: Status page
      tags: [Status]
      x-authz-action: status.read
      x-rate-limit-class: rl.read
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusPage'
              examples:
                default:
                  summary: Response
                  value:
                    overall_state: operational
                    components:
                    - id: 0192f4a0-7b1c-7cbd-8bdb-de45278e4e7b
                      name: Messages
                      description: Support team
                      sort_key: a0
                      state: operational
                      health_source: db
                      updated_at: '2026-10-10T09:30:00.000Z'
                    incidents:
                    - id: 0192f4a0-7b1c-7b46-88f5-a7d58fbd8809
                      title: Push notifications delayed
                      kind: incident
                      impact: none
                      state: scheduled
                      started_at: '2026-10-10T09:30:00.000Z'
                      component_ids: [0192f4a0-7b1c-790b-86bb-302f588b2602]
                      updates:
                      - id: 0192f4a0-7b1c-7d72-8102-dd7af4afabb5
                        state: scheduled
                        body_md: The push provider reports delays. We are monitoring.
                        created_at: '2026-10-10T09:30:00.000Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /status/public:
    get:
      operationId: getPublicStatus
      summary: Status page without login
      tags: [Status]
      description: 404 unless `status.public_enabled`.
      x-authz-action: status.public.read
      x-rate-limit-class: rl.public_auth
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusPage'
              examples:
                default:
                  summary: Response
                  value:
                    overall_state: operational
                    components:
                    - id: 0192f4a0-7b1c-7cbd-8bdb-de45278e4e7b
                      name: Messages
                      description: Support team
                      sort_key: a0
                      state: operational
                      health_source: db
                      updated_at: '2026-10-10T09:30:00.000Z'
                    incidents:
                    - id: 0192f4a0-7b1c-7b46-88f5-a7d58fbd8809
                      title: Push notifications delayed
                      kind: incident
                      impact: none
                      state: scheduled
                      started_at: '2026-10-10T09:30:00.000Z'
                      component_ids: [0192f4a0-7b1c-790b-86bb-302f588b2602]
                      updates:
                      - id: 0192f4a0-7b1c-7d72-8102-dd7af4afabb5
                        state: scheduled
                        body_md: The push provider reports delays. We are monitoring.
                        created_at: '2026-10-10T09:30:00.000Z'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
      security: []

  /status/components:
    post:
      operationId: createStatusComponent
      summary: Add a status component
      tags: [Status]
      x-authz-action: status.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StatusComponentInput'
            examples:
              default:
                summary: Request
                value:
                  name: Messages
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusComponent'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7cbd-8bdb-de45278e4e7b
                    name: Messages
                    description: Support team
                    sort_key: a0
                    state: operational
                    health_source: db
                    updated_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /status/components/{component_id}:
    patch:
      operationId: updateStatusComponent
      summary: Change a status component
      tags: [Status]
      x-authz-action: status.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ComponentIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StatusComponentInput'
            examples:
              default:
                summary: Request
                value:
                  name: Messages
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusComponent'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7cbd-8bdb-de45278e4e7b
                    name: Messages
                    description: Support team
                    sort_key: a0
                    state: operational
                    health_source: db
                    updated_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    delete:
      operationId: deleteStatusComponent
      summary: Remove a status component
      tags: [Status]
      x-authz-action: status.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ComponentIdPath'
      responses:
        '204':
          description: No content.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /status/incidents:
    post:
      operationId: createStatusIncident
      summary: Open an incident or maintenance
      tags: [Status]
      x-authz-action: status.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StatusIncidentCreate'
            examples:
              default:
                summary: Request
                value:
                  title: Push notifications delayed
                  kind: incident
                  impact: none
                  started_at: '2026-10-10T09:30:00.000Z'
                  component_ids: [0192f4a0-7b1c-790b-86bb-302f588b2602]
                  body_md: The push provider reports delays.
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusIncident'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7b46-88f5-a7d58fbd8809
                    title: Push notifications delayed
                    kind: incident
                    impact: none
                    state: scheduled
                    started_at: '2026-10-10T09:30:00.000Z'
                    component_ids: [0192f4a0-7b1c-790b-86bb-302f588b2602]
                    updates:
                    - id: 0192f4a0-7b1c-7d72-8102-dd7af4afabb5
                      state: scheduled
                      body_md: The push provider reports delays. We are monitoring.
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /status/incidents/{incident_id}/updates:
    post:
      operationId: addStatusIncidentUpdate
      summary: Add an update to an incident
      tags: [Status]
      x-authz-action: status.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IncidentIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StatusIncidentUpdateCreate'
            examples:
              default:
                summary: Request
                value:
                  state: scheduled
                  body_md: Delays are resolved.
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StatusIncident'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7b46-88f5-a7d58fbd8809
                    title: Push notifications delayed
                    kind: incident
                    impact: none
                    state: scheduled
                    started_at: '2026-10-10T09:30:00.000Z'
                    component_ids: [0192f4a0-7b1c-790b-86bb-302f588b2602]
                    updates:
                    - id: 0192f4a0-7b1c-7d72-8102-dd7af4afabb5
                      state: scheduled
                      body_md: The push provider reports delays. We are monitoring.
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports:
    get:
      operationId: listImports
      summary: List import sources
      tags: [Import]
      x-authz-action: import.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSourcePage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-78d2-8a6a-ec465435b8a0
                      kind: discord
                      name: Old community server
                      config: {}
                      state: draft
                      stats: {}
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createImport
      summary: Add an import source
      tags: [Import]
      x-authz-action: import.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportSourceCreate'
            examples:
              default:
                summary: Request
                value:
                  kind: discord
                  name: Old community server
                  config: {}
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSource'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78d2-8a6a-ec465435b8a0
                    kind: discord
                    name: Old community server
                    config: {}
                    state: draft
                    stats: {}
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}:
    get:
      operationId: getImport
      summary: Read progress of an import
      tags: [Import]
      x-authz-action: import.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSource'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78d2-8a6a-ec465435b8a0
                    kind: discord
                    name: Old community server
                    config: {}
                    state: draft
                    stats: {}
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/items:
    get:
      operationId: listImportItems
      summary: List imported items
      tags: [Import]
      x-authz-action: import.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: state
        in: query
        description: Filter by state.
        schema:
          $ref: '#/components/schemas/ImportItemState'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportItemPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7e43-8919-6113546a4c70
                      origin_key: message:1029384756:0
                      item_kind: space
                      state: pending
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/start:
    post:
      operationId: startImport
      summary: Start or continue an import
      tags: [Import]
      description: |-
        Moves a `draft` or `paused` source to `running` (22, 7.4); `started_at` is set on the first start. Starting a `running` source answers it unchanged. Any other state is `409 conflict` with `reason` `import_state`.
      x-authz-action: import.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: Accepted; processing continues asynchronously.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSource'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78d2-8a6a-ec465435b8a0
                    kind: discord
                    name: Old community server
                    config: {}
                    state: draft
                    stats: {}
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/pause:
    post:
      operationId: pauseImport
      summary: Pause an import
      tags: [Import]
      description: |-
        Moves a `running` source to `paused`; batches and cursor writes then answer `409 conflict` with `reason` `import_not_running`. Pausing a `paused` source answers it unchanged. Any other state is `409 conflict` with `reason` `import_state`.
      x-authz-action: import.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSource'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78d2-8a6a-ec465435b8a0
                    kind: discord
                    name: Old community server
                    config: {}
                    state: draft
                    stats: {}
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/cancel:
    post:
      operationId: cancelImport
      summary: Cancel an import
      tags: [Import]
      description: |-
        Ends a `draft`, `running` or `paused` source as `failed` with the counter `server.cancelled` set to `true` in `stats` and sets `finished_at`. Any other state is `409 conflict` with `reason` `import_state`.
      x-authz-action: import.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSource'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78d2-8a6a-ec465435b8a0
                    kind: discord
                    name: Old community server
                    config: {}
                    state: draft
                    stats: {}
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/complete:
    post:
      operationId: completeImport
      summary: Complete an import
      tags: [Import]
      description: |-
        When every channel is led by this server: stop the mirror, source `completed` (22, 13.5).
        Only from `running` or `paused`, else `409 conflict` with `reason` `import_state`. Every channel of the mapping whose target is not `skip` and whose channel item is `posted` must have `lead = bothy`, else `409 conflict` with `reason` `lead_not_bothy`; mapped channels that were never imported do not block it.
      x-authz-action: import.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSource'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78d2-8a6a-ec465435b8a0
                    kind: discord
                    name: Old community server
                    config: {}
                    state: completed
                    stats: {}
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/leads:
    put:
      operationId: setImportLead
      summary: Set the leading side of a channel
      tags: [Import]
      description: |-
        Switches `lead` of one mapped channel and the read-only state of its target channel in one transaction (22, 12.1 and 13).
        The channel must be in the mapping with a target other than `skip` (else `404`) and its channel item must be `posted` (else `409 conflict` with `reason` `channel_not_imported`). `lead = discord` sets the target channel read-only and cancels its active runs; `lead = bothy` lifts read-only. A `completed` or `failed` source is `409 conflict` with `reason` `import_state`. A lead equal to the current one changes nothing.
      x-authz-action: import.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportLeadSet'
            examples:
              default:
                summary: Request
                value:
                  source_channel: '1029384000'
                  lead: bothy
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSource'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78d2-8a6a-ec465435b8a0
                    kind: discord
                    name: Old community server
                    config: {}
                    state: completed
                    stats: {}
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/batches:
    post:
      operationId: applyImportBatch
      summary: Write a batch of imported objects
      tags: [Import]
      description: |-
        One transaction, at most `import.batch_max_items` objects. Every target must lie in a channel of this running source. Existing `posted` keys return their target without a duplicate or event (22, 7.2). Writes `thread.updated` and `import.progress` instead of one event per message.
        A source that is not `running` is `409 conflict` with `reason` `import_not_running`; a target outside the channels of the source is `409 conflict` with `reason` `target_outside_source`; a new channel whose name equals a live channel of its space is `409 conflict` with `reason` `name_collision`. The `data` of a structure item follows `ImportSpaceData`, `ImportChannelData`, `ImportForumTagData` or `ImportUserData`.
        A batch with `thread_origin_key` concerns exactly one thread (`thread:<id>`, or `channel:<id>` for the main thread of a text channel; a forum has none, `400 validation_failed`). Its optional first item is the `thread` item of that key (`ImportThreadData`), every other item a `message` (`ImportMessageData`) or a `pin` (`ImportPinData`); a structure item there, or a content item in a batch without `thread_origin_key`, is `400 validation_failed` at its `item_kind`. A thread that is neither posted nor created by the batch is `409 conflict` with `reason` `thread_not_imported`; a message whose author item is not posted `409 conflict` with `reason` `author_not_imported`. Imported messages have `origin = import`, are silent, write no mentions and never trigger an agent; a posted thread only follows the mirror rules of 22 12.2 step 3. At its end a batch that changed something sets `threads.unread_floor_seq` to the end of the thread and appends one `thread.updated` and one `import.progress`, never `message.created` (04 15.8).
      x-authz-action: import.apply
      x-rate-limit-class: rl.import
      x-idempotency-exempt: origin_key
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportBatch'
            examples:
              default:
                summary: Request
                value:
                  thread_origin_key: 'thread:1029384700'
                  items:
                  - origin_key: 'message:1029384756'
                    item_kind: message
                    data:
                      author_origin_key: 'user:1029384000'
                      content_md: The invoice export fails.
                      created_at: '2025-03-01T08:00:00.000Z'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportBatchResult'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7e43-8919-6113546a4c70
                      origin_key: 'message:1029384756'
                      item_kind: message
                      target_kind: message
                      target_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                      state: posted
                      created_at: '2026-10-10T09:30:00.000Z'
                    unread_floor_seq: 42
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/files:
    post:
      operationId: createImportFile
      summary: Reserve an imported file
      tags: [Import]
      description: |-
        Writes the `pending` item and reserves the file ID with an upload session of mode `direct`, without the quota check of `createUpload` (12, 11); the parts and the completion follow through `uploadImportFilePart` and `completeImportFile`, which a read-only mirror channel accepts (05 rule 1.8). A repeated call with the same key returns the same ID: with the open session, with a new session after an aborted or expired one, and without `upload` once the item is `posted` (22, 7.2). `channel_id` must be the target of a posted channel item of the source (`409 conflict`, `reason` `target_outside_source`), `author_id` the target of a posted user item (`409 conflict`, `reason` `author_not_imported`); the scan of an import file runs with the lowest job priority.
      x-authz-action: import.apply
      x-rate-limit-class: rl.import
      x-idempotency-exempt: origin_key
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportFileCreate'
            examples:
              default:
                summary: Request
                value:
                  origin_key: 'file:1029384799'
                  channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                  author_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                  name: report.pdf
                  size_bytes: 48213
                  sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportFileSlot'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/files/{file_id}/parts/{part_number}:
    put:
      operationId: uploadImportFilePart
      summary: Upload one part of an imported file
      tags: [Import]
      description: |-
        One part of the file of a file item of this source in mode `direct`, with the rules of `uploadPart` (12, 3.2): the same part number with the same `Content-Digest` answers 200 without new work, with another digest `409 conflict` with `reason` `part_conflict`; a wrong length or digest is `400 validation_failed` with `errors[].code` `part_digest_mismatch` and stores nothing; an expired session answers `410 gone` (a repeated `createImportFile` opens a new one). It authorizes `import.apply` on the running source instead of `file.upload`, so a read-only mirror channel accepts it (05 rule 1.8, 4.12). A file that is not the target of a file item of this source is `404`.
      x-authz-action: import.apply
      x-rate-limit-class: rl.import
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      - $ref: '#/components/parameters/FileIdPath'
      - $ref: '#/components/parameters/PartNumberPath'
      - name: Content-Digest
        in: header
        required: true
        description: 'RFC 9530 digest of the part with the member `sha-256`, e.g. `sha-256=:<base64>:`; other members are ignored.'
        schema:
          type: string
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UploadPartResult'
              examples:
                default:
                  summary: Response
                  value:
                    part_number: 1
                    etag: '"3858f62230ac3c915f300c664312c11f"'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          $ref: '#/components/responses/Gone'
        '413':
          $ref: '#/components/responses/PayloadTooLarge'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/files/{file_id}/complete:
    post:
      operationId: completeImportFile
      summary: Finish the upload of an imported file
      tags: [Import]
      description: |-
        Completes the upload of the file of a file item of this source with the rules of `completeUpload` (12, 3.4): `parts` is checked against the confirmed parts, missing parts are `409 conflict` with `reason` `upload_incomplete`, another `etag` `409 conflict` with `reason` `part_conflict`, an expired session `410 gone`. The job `files.scan` checks size and SHA-256 against the declaration of `createImportFile`, detects the type and scans; it runs with the lowest job priority, so live uploads are scanned first (12, 11). The item becomes `posted` in the same transaction; a repeated call answers the posted item. It authorizes `import.apply` on the running source instead of `file.upload` (05 rule 1.8, 4.12). A file that is not the target of a file item of this source is `404`.
      x-authz-action: import.apply
      x-rate-limit-class: rl.import
      x-idempotency-exempt: origin_key
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      - $ref: '#/components/parameters/FileIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UploadComplete'
            examples:
              default:
                summary: Request
                value:
                  parts:
                  - part_number: 1
                    etag: '"3858f62230ac3c915f300c664312c11f"'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportFileSlot'
              examples:
                default:
                  summary: Response
                  value:
                    item:
                      id: 0192f4a0-7b1c-7e43-8919-6113546a4c71
                      origin_key: 'file:1029384799'
                      item_kind: file
                      target_kind: file
                      target_id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                      state: posted
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /imports/{import_id}/cursor:
    put:
      operationId: setImportCursor
      summary: Advance the import cursor
      tags: [Import]
      description: |-
        Only after a successful batch (22, 7.3).
        Replaces `cursor` with the given object and merges the top-level members of `stats` into the member `tool` of `ImportSource.stats` (the server's own counters stay under `server`). Only for a `running` source, else `409 conflict` with `reason` `import_not_running`.
      x-authz-action: import.apply
      x-rate-limit-class: rl.import
      parameters:
      - $ref: '#/components/parameters/ImportIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportCursorSet'
            examples:
              default:
                summary: Request
                value:
                  cursor: {}
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportSource'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-78d2-8a6a-ec465435b8a0
                    kind: discord
                    name: Old community server
                    config: {}
                    state: completed
                    stats: {}
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /gdpr-requests:
    get:
      operationId: listGdprRequests
      summary: List privacy requests
      tags: [Privacy]
      x-authz-action: gdpr.request.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - name: subject_user_id
        in: query
        description: Only requests about this person.
        schema:
          $ref: '#/components/schemas/Uuid'
      - name: state
        in: query
        description: Filter by state.
        schema:
          $ref: '#/components/schemas/GdprRequestState'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GdprRequestPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7ac0-85c3-2e83fd50d9e7
                      subject_user_id: 0192f4a0-7b1c-7277-8379-95fe87046d8a
                      kind: export
                      state: requested
                      requested_by: 0192f4a0-7b1c-7e23-8a0f-0b7e316bfc58
                      reason: Request by the person
                      due_at: '2026-10-10T09:30:00.000Z'
                      created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createGdprRequest
      summary: Request export, deletion or rectification
      tags: [Privacy]
      x-authz-action: gdpr.request.create
      x-rate-limit-class: rl.export
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GdprRequestCreate'
            examples:
              default:
                summary: Request
                value:
                  kind: export
                  reason: Request by the person
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GdprRequest'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7ac0-85c3-2e83fd50d9e7
                    subject_user_id: 0192f4a0-7b1c-7277-8379-95fe87046d8a
                    kind: export
                    state: requested
                    requested_by: 0192f4a0-7b1c-7e23-8a0f-0b7e316bfc58
                    reason: Request by the person
                    due_at: '2026-10-10T09:30:00.000Z'
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /gdpr-requests/{request_id}:
    get:
      operationId: getGdprRequest
      summary: Read a privacy request
      tags: [Privacy]
      x-authz-action: gdpr.request.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/RequestIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GdprRequest'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7ac0-85c3-2e83fd50d9e7
                    subject_user_id: 0192f4a0-7b1c-7277-8379-95fe87046d8a
                    kind: export
                    state: requested
                    requested_by: 0192f4a0-7b1c-7e23-8a0f-0b7e316bfc58
                    reason: Request by the person
                    due_at: '2026-10-10T09:30:00.000Z'
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /gdpr-requests/{request_id}/decide:
    post:
      operationId: decideGdprRequest
      summary: Approve or reject a privacy request
      tags: [Privacy]
      x-authz-action: gdpr.request.decide
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/RequestIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GdprDecision'
            examples:
              default:
                summary: Request
                value:
                  decision: approve
                  reason: Identity verified
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GdprRequest'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7ac0-85c3-2e83fd50d9e7
                    subject_user_id: 0192f4a0-7b1c-7277-8379-95fe87046d8a
                    kind: export
                    state: requested
                    requested_by: 0192f4a0-7b1c-7e23-8a0f-0b7e316bfc58
                    reason: Request by the person
                    due_at: '2026-10-10T09:30:00.000Z'
                    created_at: '2026-10-10T09:30:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /gdpr-requests/{request_id}/export:
    get:
      operationId: downloadGdprExport
      summary: Download the export package
      tags: [Privacy]
      x-authz-action: gdpr.export.download
      x-rate-limit-class: rl.export
      parameters:
      - $ref: '#/components/parameters/RequestIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/zip:
              schema:
                type: string
                format: binary
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '410':
          $ref: '#/components/responses/Gone'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /hosts:
    get:
      operationId: listHosts
      summary: List hosts
      tags: [Hosts]
      description: |-
        Owners see every host; other people see hosts they are assigned to or hold a `terminal` grant on (07, 2.12). Ordered by name.
      x-authz-action: host.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HostPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                      name: Build server
                      state: active
                      platform: linux
                      assigned_user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      llm_credentials: organization
                      declared_server_host: false
                      key_fingerprint: 7K2Q-M9XD-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000
                      key_generation: 1
                      profiles:
                      - name: shell
                        display_name: Shell
                        kind: shell
                      - name: claude
                        display_name: Claude Code
                        kind: claude
                      doctor:
                      - check: hd1_user
                        ok: true
                      session_users:
                      - user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        os_user: bothy-s-robin
                        created_at: '2026-10-10T09:42:00.000Z'
                      reported_session_users:
                      - os_user: bothy-s-robin
                        workdirs: [Project A]
                      connected: true
                      host_version: 1.0.0
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3

        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    post:
      operationId: createHost
      summary: Register a host and issue its pairing code
      tags: [Hosts]
      description: |-
        Creates a host in state `pairing` and returns the pairing code `bothy_hpc_...` exactly once; it is valid for `terminal.pairing_code_ttl` (24, 3.2). Needs step-up. The code is a one-time secret: a replay with the same `Idempotency-Key` answers `409 conflict` with `reason` `one_time_secret` instead of repeating it (06, 7).
      x-authz-action: host.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HostCreate'
            examples:
              default:
                summary: Request
                value:
                  name: Build server
                  assigned_user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                  reason: Shared build machine for Alex
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HostPairingCode'
              examples:
                default:
                  summary: Response
                  value:
                    host_id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                    pairing_code: bothy_hpc_EXAMPLE000000000000000000000000000000000000_EXAMPL
                    expires_at: '2026-10-10T09:45:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /hosts/pair:
    post:
      operationId: pairHost
      summary: Redeem a pairing code from the host service
      tags: [Hosts]
      description: |-
        Called by `bothy-host pair` without login. Registers the static X25519 key (Noise) and the Ed25519 key (login) of the host, its profiles and the names of its session users (`reported_session_users`, 24 section 2.2). The host becomes `confirming`, `key_generation` rises; an owner confirms the fingerprint (24, 3.2). Every code that does not work answers `401 unauthenticated` with the same body: a wrong form or checksum (checked without database access), an unknown, used, replaced or expired code; a retry after a lost answer gets `401` too, the operator asks for a new code. Key rules (`400 validation_failed`, the code stays valid): `noise_public_key` strict base64url of 32 bytes and no low-order point (`errors[].code` `invalid_key`); `auth_public_jwk` exactly `kty` `OKP`, `crv` `Ed25519` and `x` of 32 bytes (`invalid_key`), any other member such as the private `d` is `unknown_member`. Report rules: at least one session user (`empty`), names distinct without regard to case (`duplicate`) and at most `host.os_user_max_chars` characters (`too_long`), labels and profile names at most `terminal.report_label_max_chars` characters (`too_long`), each list at most `terminal.report_max_entries` entries (`limit_reached`), distinct profile names (`duplicate`), `host_version` SemVer without `v` (`invalid_format`). `409 conflict` with `reason` `host_key_in_use` when the login key belongs to another host, `host_key_reused` when the keys equal the keys a re-pairing dropped; the code stays valid.
      x-authz-action: host.pair
      x-rate-limit-class: rl.public_auth
      x-idempotency-exempt: auth_flow
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HostPairRequest'
            examples:
              default:
                summary: Request
                value:
                  pairing_code: bothy_hpc_EXAMPLE000000000000000000000000000000000000_EXAMPL
                  noise_public_key: Zm9vYmFyZm9vYmFyZm9vYmFyZm9vYmFyZm9vYmFyMTI
                  auth_public_jwk:
                    kty: OKP
                    crv: Ed25519
                    x: EXAMPLE-ed25519-public-key-base64url-0000000
                  platform: linux
                  os_version: Debian 13
                  host_version: 1.0.0
                  profiles:
                  - name: shell
                    display_name: Shell
                    kind: shell
                  - name: claude
                    display_name: Claude Code
                    kind: claude
                  session_users:
                  - os_user: bothy-s-robin
                    workdirs: [Project A]
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HostPairResult'
              examples:
                default:
                  summary: Response
                  value:
                    host_id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                    key_fingerprint: 7K2Q-M9XD-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000
                    instance_id: 0192f4a0-7b1c-7000-8a00-000000000001
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /hosts/{host_id}:
    get:
      operationId: getHost
      summary: Read a host
      tags: [Hosts]
      x-authz-action: host.read
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/HostIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Host'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                    name: Build server
                    state: active
                    platform: linux
                    assigned_user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    llm_credentials: organization
                    declared_server_host: false
                    key_fingerprint: 7K2Q-M9XD-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000
                    key_generation: 1
                    profiles:
                    - name: shell
                      display_name: Shell
                      kind: shell
                    - name: claude
                      display_name: Claude Code
                      kind: claude
                    doctor:
                    - check: hd1_user
                      ok: true
                    session_users:
                    - user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      os_user: bothy-s-robin
                      created_at: '2026-10-10T09:42:00.000Z'
                    reported_session_users:
                    - os_user: bothy-s-robin
                      workdirs: [Project A]
                    connected: true
                    host_version: 1.0.0
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'
    patch:
      operationId: updateHost
      summary: Change name or assignee of a host
      tags: [Hosts]
      description: |-
        A new `assigned_user_id` ends the sessions of people who no longer qualify under SR10 (`access_revoked`). `add_session_users` assigns own session users (ruling T12; a taken name gives `409 conflict` with `reason: session_user_taken`, a name the host does not report in `reported_session_users` gives `400 validation_failed` with `errors[].code` `session_user_not_on_host`); `revoke_session_users` ends the assignment of the listed people, ends their sessions on the host (`access_revoked`) and keeps the name taken (24 section 2.2). Needs step-up.
      x-authz-action: host.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/HostIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HostUpdate'
            examples:
              default:
                summary: Request
                value:
                  assigned_user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                  reason: Robin hands the machine to Alex
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Host'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                    name: Build server
                    state: active
                    platform: linux
                    assigned_user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    llm_credentials: organization
                    declared_server_host: false
                    key_fingerprint: 7K2Q-M9XD-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000
                    key_generation: 1
                    profiles:
                    - name: shell
                      display_name: Shell
                      kind: shell
                    - name: claude
                      display_name: Claude Code
                      kind: claude
                    doctor:
                    - check: hd1_user
                      ok: true
                    session_users:
                    - user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      os_user: bothy-s-robin
                      created_at: '2026-10-10T09:42:00.000Z'
                    reported_session_users:
                    - os_user: bothy-s-robin
                      workdirs: [Project A]
                    connected: true
                    host_version: 1.0.0
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /hosts/{host_id}/pairing-code:
    post:
      operationId: issueHostPairingCode
      summary: Issue a new pairing code (re-pairing)
      tags: [Hosts]
      description: |-
        Sets the host to `pairing` from `pairing`, `confirming`, `active` or `disabled` (also `duplicate_connection`, whose reason is cleared); `retired` gives `409 invalid_state_transition`. From `confirming`, `active` and `disabled` it drops the stored keys at once; from `active` it ends the sessions (`host_rekeyed`) and closes the connections. Every owner gets `host.updated`, with `key_changed` exactly when stored keys were dropped (24, 3.3); `key_generation` rises at the next `pairHost`. A repeated call issues another code and invalidates the previous one. Needs step-up. The code is a one-time secret: a replay with the same `Idempotency-Key` answers `409 conflict` with `reason` `one_time_secret` instead of repeating it (06, 7).
      x-authz-action: host.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/HostIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Host reinstalled
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HostPairingCode'
              examples:
                default:
                  summary: Response
                  value:
                    host_id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                    pairing_code: bothy_hpc_EXAMPLE000000000000000000000000000000000000_EXAMPL
                    expires_at: '2026-10-10T09:45:00.000Z'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '422':
          $ref: '#/components/responses/UnprocessableEntity'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /hosts/{host_id}/confirm:
    post:
      operationId: confirmHost
      summary: Confirm the fingerprint and declare the LLM login
      tags: [Hosts]
      description: |-
        The owner types the first `terminal.fingerprint_confirm_chars` characters shown by `bothy-host pair`, without separators, case-insensitive; a prefix of another length or with a character outside the Crockford alphabet (also `I`, `L`, `O`, `U`) gives `400 validation_failed` with `errors[].code` `invalid_format` and does not count. A mismatch gives `409 fingerprint_mismatch` with `attempts_remaining`; the failure counter is not part of `Host`, so `version` and the `If-Match` stay. After `terminal.fingerprint_max_attempts` failures the host falls back to `pairing` without keys (`attempts_remaining` `0`), and a new pairing code is needed. A failed attempt counts also under an `Idempotency-Key`; its replay returns the stored `409`. I6 is checked in the same transaction (`409 i6_violation`). `session_users` assigns each person their own session user on this host (ruling T12, 24 section 2.2); a name that belongs or belonged to another person on this host, or a second valid assignment of one person, gives `409 conflict` with `reason: session_user_taken`; a name the host did not report (`reported_session_users`) gives `400 validation_failed` with `errors[].code` `session_user_not_on_host`, and nothing is stored. Without an assignment a person gets no terminal there (SR12). Needs step-up.
      x-authz-action: host.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/HostIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HostConfirm'
            examples:
              default:
                summary: Request
                value:
                  fingerprint_prefix: 7K2QM9XD
                  llm_credentials: organization
                  declared_server_host: false
                  session_users:
                  - user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    os_user: bothy-s-robin
                  reason: Build machine of the engineering team
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Host'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                    name: Build server
                    state: active
                    platform: linux
                    assigned_user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    llm_credentials: organization
                    declared_server_host: false
                    key_fingerprint: 7K2Q-M9XD-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000
                    key_generation: 1
                    profiles:
                    - name: shell
                      display_name: Shell
                      kind: shell
                    - name: claude
                      display_name: Claude Code
                      kind: claude
                    doctor:
                    - check: hd1_user
                      ok: true
                    session_users:
                    - user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      os_user: bothy-s-robin
                      created_at: '2026-10-10T09:42:00.000Z'
                    reported_session_users:
                    - os_user: bothy-s-robin
                      workdirs: [Project A]
                    connected: true
                    host_version: 1.0.0
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /hosts/{host_id}/llm-credentials:
    put:
      operationId: setHostLlmCredentials
      summary: Declare the LLM login of a host
      tags: [Hosts]
      description: |-
        `personal` needs `credential_holder_id`, a person that is not deleted; `none` and `organization` take none. Fails with `409 i6_violation` (member `conflicts`, one entry per host) if any valid grant on the host names another subject than the holder or a group; switching to `personal` or changing the holder ends the sessions of other people in the same transaction (`close_reason` `i6_violation`, 24, 7.3). A `retired` host answers `409 conflict`. The declaration the host already has answers `200` with the current host, without a new version, an event or a new rights version (audited with `no_change`). Needs step-up and the mandatory `reason`.
      x-authz-action: host.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/HostIdPath'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HostLlmCredentialsSet'
            examples:
              default:
                summary: Request
                value:
                  llm_credentials: personal
                  credential_holder_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                  reason: Alex uses the own subscription on this laptop
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Host'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                    name: Build server
                    state: active
                    platform: linux
                    assigned_user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    llm_credentials: organization
                    declared_server_host: false
                    key_fingerprint: 7K2Q-M9XD-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000
                    key_generation: 1
                    profiles:
                    - name: shell
                      display_name: Shell
                      kind: shell
                    - name: claude
                      display_name: Claude Code
                      kind: claude
                    doctor:
                    - check: hd1_user
                      ok: true
                    session_users:
                    - user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      os_user: bothy-s-robin
                      created_at: '2026-10-10T09:42:00.000Z'
                    reported_session_users:
                    - os_user: bothy-s-robin
                      workdirs: [Project A]
                    connected: true
                    host_version: 1.0.0
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /hosts/{host_id}/state:
    post:
      operationId: setHostState
      summary: Disable, enable or retire a host
      tags: [Hosts]
      description: |-
        `disabled` and `retired` end all sessions (`host_disabled`, `host_retired`) and close the connections of the host service. `retired` is final. Needs step-up.
      x-authz-action: host.manage
      x-rate-limit-class: rl.admin
      parameters:
      - $ref: '#/components/parameters/HostIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      - $ref: '#/components/parameters/IfMatchRequired'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/HostStateSet'
            examples:
              default:
                summary: Request
                value:
                  state: disabled
                  reason: Machine sent for repair
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
            ETag:
              $ref: '#/components/headers/ETag'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Host'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                    name: Build server
                    state: active
                    platform: linux
                    assigned_user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    llm_credentials: organization
                    declared_server_host: false
                    key_fingerprint: 7K2Q-M9XD-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000
                    key_generation: 1
                    profiles:
                    - name: shell
                      display_name: Shell
                      kind: shell
                    - name: claude
                      display_name: Claude Code
                      kind: claude
                    doctor:
                    - check: hd1_user
                      ok: true
                    session_users:
                    - user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      os_user: bothy-s-robin
                      created_at: '2026-10-10T09:42:00.000Z'
                    reported_session_users:
                    - os_user: bothy-s-robin
                      workdirs: [Project A]
                    connected: true
                    host_version: 1.0.0
                    created_at: '2026-10-10T09:30:00.000Z'
                    version: 3
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'
        '428':
          $ref: '#/components/responses/PreconditionRequired'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /hosts/{host_id}/terminal/sessions:
    post:
      operationId: openTerminalSession
      summary: Open a terminal session
      tags: [Terminal]
      description: |-
        Needs a fresh, unused step-up (passkey or step-up device key; in the console only a passkey with `userVerification = required`), the `terminal` capability, SR10, SR11 (I6) and SR12 (an own session user on the host whose name the host reports, otherwise 403 `terminal_denied` with `reason: no_session_user`). With a `claude` profile `workdir_index` must point into the caller's own working directories (ruling T13). Consumes the step-up and returns a one-time session token for the Noise handshake (24, 5 and 6.1). The device then connects to `stream_path`.
      x-authz-action: terminal.open
      x-rate-limit-class: rl.terminal
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/HostIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TerminalSessionOpen'
            examples:
              default:
                summary: Request
                value:
                  profile: shell
                  cols: 120
                  rows: 40
                  client_ephemeral_key_hash: EXAMPLE-sha256-of-ephemeral-key-base64url00
      responses:
        '201':
          description: Created.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TerminalSessionGrant'
              examples:
                default:
                  summary: Response
                  value:
                    session:
                      id: 0192f4a0-7b1c-7b22-9d4f-5a6b7c8d9e0f
                      host_id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      profile: shell
                      state: active
                      opened_at: '2026-10-10T09:31:00.000Z'
                      started_at: '2026-10-10T09:31:01.000Z'
                      bytes_in: 4096
                      bytes_out: 65536
                      version: 2
                    session_token: eyJhbGciOiJFZERTQSIsImtpZCI6InRlcm0tMjAyNi0xMCIsInR5cCI6InRlcm1pbmFsLXNlc3Npb24rand0In0.EXAMPLE.EXAMPLE
                    session_token_expires_at: '2026-10-10T09:31:30.000Z'
                    stream_path: /api/v1/terminal/sessions/0192f4a0-7b1c-7b22-9d4f-5a6b7c8d9e0f/stream
                    noise_public_key: Zm9vYmFyZm9vYmFyZm9vYmFyZm9vYmFyZm9vYmFyMTI
                    key_fingerprint: 7K2Q-M9XD-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000
                    key_generation: 1
                    noise_protocol: Noise_NK_25519_ChaChaPoly_SHA256
                    prologue_version: bothy-terminal/1
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /terminal/sessions:
    get:
      operationId: listTerminalSessions
      summary: List terminal sessions
      tags: [Terminal]
      description: |-
        Own sessions; owners see all sessions. Metadata only, never content.
      x-authz-action: terminal.read_own
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/Cursor'
      - $ref: '#/components/parameters/Limit'
      - $ref: '#/components/parameters/HostIdQuery'
      - $ref: '#/components/parameters/TerminalSessionStateQuery'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TerminalSessionPage'
              examples:
                default:
                  summary: Response
                  value:
                    items:
                    - id: 0192f4a0-7b1c-7b22-9d4f-5a6b7c8d9e0f
                      host_id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      profile: shell
                      state: active
                      opened_at: '2026-10-10T09:31:00.000Z'
                      started_at: '2026-10-10T09:31:01.000Z'
                      bytes_in: 4096
                      bytes_out: 65536
                      version: 2
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /terminal/sessions/{session_id}:
    get:
      operationId: getTerminalSession
      summary: Read a terminal session
      tags: [Terminal]
      x-authz-action: terminal.read_own
      x-rate-limit-class: rl.read
      parameters:
      - $ref: '#/components/parameters/TerminalSessionIdPath'
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TerminalSession'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7b22-9d4f-5a6b7c8d9e0f
                    host_id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                    user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    profile: shell
                    state: active
                    opened_at: '2026-10-10T09:31:00.000Z'
                    started_at: '2026-10-10T09:31:01.000Z'
                    bytes_in: 4096
                    bytes_out: 65536
                    version: 2
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /terminal/sessions/{session_id}/attach:
    post:
      operationId: attachTerminalSession
      summary: Attach to an own session again
      tags: [Terminal]
      description: |-
        For a session in state `opening`, `active` or `detached`. Needs a fresh step-up, except from the same device in the same login session within `terminal.quick_reattach_window` after the disconnect. Returns a new session token; older tokens become worthless. A connection of another device is closed with `4103` (24, 6.2). An attach to an `opening` session restarts the opening (`att = open`, new deadline).
      x-authz-action: terminal.attach
      x-rate-limit-class: rl.terminal
      x-idempotency-exempt: auth_flow
      parameters:
      - $ref: '#/components/parameters/TerminalSessionIdPath'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TerminalSessionAttach'
            examples:
              default:
                summary: Request
                value:
                  cols: 120
                  rows: 40
                  client_ephemeral_key_hash: EXAMPLE-sha256-of-ephemeral-key-base64url00
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TerminalSessionGrant'
              examples:
                default:
                  summary: Response
                  value:
                    session:
                      id: 0192f4a0-7b1c-7b22-9d4f-5a6b7c8d9e0f
                      host_id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                      user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                      profile: shell
                      state: active
                      opened_at: '2026-10-10T09:31:00.000Z'
                      started_at: '2026-10-10T09:31:01.000Z'
                      bytes_in: 4096
                      bytes_out: 65536
                      version: 2
                    session_token: eyJhbGciOiJFZERTQSIsImtpZCI6InRlcm0tMjAyNi0xMCIsInR5cCI6InRlcm1pbmFsLXNlc3Npb24rand0In0.EXAMPLE.EXAMPLE
                    session_token_expires_at: '2026-10-10T09:31:30.000Z'
                    stream_path: /api/v1/terminal/sessions/0192f4a0-7b1c-7b22-9d4f-5a6b7c8d9e0f/stream
                    noise_public_key: Zm9vYmFyZm9vYmFyZm9vYmFyZm9vYmFyZm9vYmFyMTI
                    key_fingerprint: 7K2Q-M9XD-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000
                    key_generation: 1
                    noise_protocol: Noise_NK_25519_ChaChaPoly_SHA256
                    prologue_version: bothy-terminal/1
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

  /terminal/sessions/{session_id}/close:
    post:
      operationId: closeTerminalSession
      summary: End a terminal session
      tags: [Terminal]
      description: |-
        Own sessions; owners any session (`closed_by_owner`). The server closes both connections and tells the host service to end the PTY.
      x-authz-action: terminal.close
      x-rate-limit-class: rl.write
      parameters:
      - $ref: '#/components/parameters/TerminalSessionIdPath'
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReasonBody'
            examples:
              default:
                summary: Request
                value:
                  reason: Done
      responses:
        '200':
          description: OK.
          headers:
            Correlation-Id:
              $ref: '#/components/headers/Correlation-Id'
            RateLimit:
              $ref: '#/components/headers/RateLimit'
            RateLimit-Policy:
              $ref: '#/components/headers/RateLimit-Policy'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TerminalSession'
              examples:
                default:
                  summary: Response
                  value:
                    id: 0192f4a0-7b1c-7b22-9d4f-5a6b7c8d9e0f
                    host_id: 0192f4a0-7b1c-7a11-8c3e-4f5a6b7c8d9e
                    user_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    device_id: 0192f4a0-7b1c-7848-8501-1716705c7dbe
                    profile: shell
                    state: closed
                    opened_at: '2026-10-10T09:31:00.000Z'
                    started_at: '2026-10-10T09:31:01.000Z'
                    bytes_in: 4096
                    bytes_out: 65536
                    version: 2
                    closed_at: '2026-10-10T10:00:00.000Z'
                    close_reason: client_closed
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        default:
          $ref: '#/components/responses/Error'

webhooks:
  message.created:
    post:
      operationId: onMessageCreatedEvent
      summary: Event `message.created`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: message.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageCreatedEvent'
            examples:
              default:
                summary: message.created
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: message
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: message.created
                  payload:
                    message:
                      id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      thread_seq: 42
                      author:
                        id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        kind: user
                        handle: alex
                        display_name: Alex
                      client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                      origin: human
                      kind: text
                      content_md: '@support-agent the invoice export fails since this morning.'
                      content_format: 1
                      silent: false
                      attachments:
                      - position: 1
                        file:
                          id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                          size_bytes: 48213
                          mime_type: image/png
                          original_name: screenshot.png
                          uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                          purpose: attachment
                          upload_state: complete
                          scan_status: clean
                          transcript_status: none
                          thumbnails:
                          - variant: small
                            mime_type: image/jpeg
                            width: 320
                            height: 180
                          created_at: '2026-10-10T09:30:00.000Z'
                      mention_ids: [0192f4a0-7b1c-718f-86e7-9cb8b914b848]
                      reactions:
                      - emoji: 👍
                        count: 3
                      pinned: false
                      components:
                      - id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                        custom_id: approve-refund
                        type: button
                        definition: {}
                        position: 1
                        single_use: true
                        restricted_to: any
                        state: active
                        version: 3
                      version: 3
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  message.updated:
    post:
      operationId: onMessageUpdatedEvent
      summary: Event `message.updated`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: message.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageUpdatedEvent'
            examples:
              default:
                summary: message.updated
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: message
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: message.updated
                  payload:
                    message:
                      id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      thread_seq: 42
                      author:
                        id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                        kind: user
                        handle: alex
                        display_name: Alex
                      client_msg_id: 0192f4a0-7b1c-7f4e-8959-c3574b6ad93b
                      origin: human
                      kind: text
                      content_md: '@support-agent the invoice export fails since this morning.'
                      content_format: 1
                      silent: false
                      attachments:
                      - position: 1
                        file:
                          id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                          size_bytes: 48213
                          mime_type: image/png
                          original_name: screenshot.png
                          uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                          purpose: attachment
                          upload_state: complete
                          scan_status: clean
                          transcript_status: none
                          thumbnails:
                          - variant: small
                            mime_type: image/jpeg
                            width: 320
                            height: 180
                          created_at: '2026-10-10T09:30:00.000Z'
                      mention_ids: [0192f4a0-7b1c-718f-86e7-9cb8b914b848]
                      reactions:
                      - emoji: 👍
                        count: 3
                      pinned: false
                      components:
                      - id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                        custom_id: approve-refund
                        type: button
                        definition: {}
                        position: 1
                        single_use: true
                        restricted_to: any
                        state: active
                        version: 3
                      version: 3
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  message.deleted:
    post:
      operationId: onMessageDeletedEvent
      summary: Event `message.deleted`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: message.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/MessageDeletedEvent'
            examples:
              default:
                summary: message.deleted
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: message
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    version: 4
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: message.deleted
                  payload:
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    thread_seq: 5301
                    deleted_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  reaction.added:
    post:
      operationId: onReactionAddedEvent
      summary: Event `reaction.added`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: message.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReactionAddedEvent'
            examples:
              default:
                summary: reaction.added
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: message
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: reaction.added
                  payload:
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    emoji: 👍
                    principal_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    count: 1
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  reaction.removed:
    post:
      operationId: onReactionRemovedEvent
      summary: Event `reaction.removed`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: message.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ReactionRemovedEvent'
            examples:
              default:
                summary: reaction.removed
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: message
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: reaction.removed
                  payload:
                    message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    emoji: 👍
                    principal_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                    count: 1
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  pin.added:
    post:
      operationId: onPinAddedEvent
      summary: Event `pin.added`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: thread.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PinAddedEvent'
            examples:
              default:
                summary: pin.added
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: message
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: pin.added
                  payload:
                    pin:
                      message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      pinned_by: 0192f4a0-7b1c-72de-8fae-dfbd3e36ff9a
                      pinned_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  pin.removed:
    post:
      operationId: onPinRemovedEvent
      summary: Event `pin.removed`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: thread.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PinRemovedEvent'
            examples:
              default:
                summary: pin.removed
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: message
                    id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: pin.removed
                  payload:
                    pin:
                      message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      pinned_by: 0192f4a0-7b1c-72de-8fae-dfbd3e36ff9a
                      pinned_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  thread.created:
    post:
      operationId: onThreadCreatedEvent
      summary: Event `thread.created`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: thread.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadCreatedEvent'
            examples:
              default:
                summary: thread.created
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: thread
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: thread.created
                  payload:
                    thread:
                      id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: thread
                      title: Invoice export fails
                      title_source: auto
                      state: open
                      last_message_seq: 5301
                      last_activity_at: '2026-10-10T09:30:00.000Z'
                      message_count: 1
                      created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3
                      tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  thread.updated:
    post:
      operationId: onThreadUpdatedEvent
      summary: Event `thread.updated`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: thread.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadUpdatedEvent'
            examples:
              default:
                summary: thread.updated
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: thread
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: thread.updated
                  payload:
                    thread:
                      id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: thread
                      title: Invoice export fails
                      title_source: auto
                      state: open
                      last_message_seq: 5301
                      last_activity_at: '2026-10-10T09:30:00.000Z'
                      message_count: 1
                      created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3
                      tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  thread.closed:
    post:
      operationId: onThreadClosedEvent
      summary: Event `thread.closed`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: thread.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadClosedEvent'
            examples:
              default:
                summary: thread.closed
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: thread
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: thread.closed
                  payload:
                    thread:
                      id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: thread
                      title: Invoice export fails
                      title_source: auto
                      state: open
                      last_message_seq: 5301
                      last_activity_at: '2026-10-10T09:30:00.000Z'
                      message_count: 1
                      created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3
                      tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  thread.reopened:
    post:
      operationId: onThreadReopenedEvent
      summary: Event `thread.reopened`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: thread.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadReopenedEvent'
            examples:
              default:
                summary: thread.reopened
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: thread
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: thread.reopened
                  payload:
                    thread:
                      id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: thread
                      title: Invoice export fails
                      title_source: auto
                      state: open
                      last_message_seq: 5301
                      last_activity_at: '2026-10-10T09:30:00.000Z'
                      message_count: 1
                      created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3
                      tag_ids: [0192f4a0-7b1c-712d-8017-1dad64203598]
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  thread.moved:
    post:
      operationId: onThreadMovedEvent
      summary: Event `thread.moved`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: thread.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadMovedEvent'
            examples:
              default:
                summary: thread.moved
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: thread
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: thread.moved
                  payload:
                    thread:
                      id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: thread
                      title: Invoice export fails
                      title_source: auto
                      state: open
                      last_message_seq: 5301
                      last_activity_at: '2026-10-10T09:30:00.000Z'
                      message_count: 1
                      created_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3
                      moved_at: '2026-10-10T09:30:00.000Z'
                      tag_ids: []
                    from_channel_id: 0192f4a0-7b1c-745e-8621-aacfb6cd0aad
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  thread.moved_out:
    post:
      operationId: onThreadMovedOutEvent
      summary: Event `thread.moved_out`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: channel.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ThreadMovedOutEvent'
            examples:
              default:
                summary: thread.moved_out
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                  resource:
                    kind: channel
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: thread.moved_out
                  payload:
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  agent_session.changed:
    post:
      operationId: onAgentSessionChangedEvent
      summary: Event `agent_session.changed`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: thread.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentSessionChangedEvent'
            examples:
              default:
                summary: agent_session.changed
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: thread
                    id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: agent_session.changed
                  payload:
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                    agent_session:
                      id: 0192f4a0-7b1c-741b-8617-885056753dc0
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      is_current: true
                      started: true
                      cost:
                        amount_micros: 1250000
                        currency: USD
                        is_estimate: true
                      created_at: '2026-10-10T09:30:00.000Z'
                      version: 3
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  run.state_changed:
    post:
      operationId: onRunStateChangedEvent
      summary: Event `run.state_changed`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: run.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunStateChangedEvent'
            examples:
              default:
                summary: run.state_changed
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: run
                    id: 0192f4a0-7b1c-767f-8292-c1b14d7d40df
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: run.state_changed
                  payload:
                    run:
                      id: 0192f4a0-7b1c-767f-8292-c1b14d7d40df
                      thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                      channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      agent_id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      triggered_by: 0192f4a0-7b1c-7db7-8afc-99f48568ad99
                      state: queued
                      version: 3
                      billing_mode_expected: personal
                      cost:
                        amount_micros: 1250000
                        currency: USD
                        is_estimate: true
                      step_count: 1
                      queued_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  space.created:
    post:
      operationId: onSpaceCreatedEvent
      summary: Event `space.created`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: space.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpaceCreatedEvent'
            examples:
              default:
                summary: space.created
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                  resource:
                    kind: space
                    id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: space.created
                  payload:
                    space:
                      id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                      name: Support
                      sort_key: a0
                      version: 3
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  space.updated:
    post:
      operationId: onSpaceUpdatedEvent
      summary: Event `space.updated`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: space.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpaceUpdatedEvent'
            examples:
              default:
                summary: space.updated
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                  resource:
                    kind: space
                    id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: space.updated
                  payload:
                    space:
                      id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                      name: Support
                      sort_key: a0
                      version: 3
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  space.archived:
    post:
      operationId: onSpaceArchivedEvent
      summary: Event `space.archived`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: space.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SpaceArchivedEvent'
            examples:
              default:
                summary: space.archived
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                  resource:
                    kind: space
                    id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: space.archived
                  payload:
                    space:
                      id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                      name: Support
                      sort_key: a0
                      version: 3
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  channel.created:
    post:
      operationId: onChannelCreatedEvent
      summary: Event `channel.created`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: channel.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelCreatedEvent'
            examples:
              default:
                summary: channel.created
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                  resource:
                    kind: channel
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: channel.created
                  payload:
                    channel:
                      id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: text
                      name: tickets
                      topic: Customer tickets
                      sort_key: a1
                      read_only: false
                      operator:
                        mode: inherit
                      version: 3
                      settings_version: 1
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  channel.updated:
    post:
      operationId: onChannelUpdatedEvent
      summary: Event `channel.updated`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: channel.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelUpdatedEvent'
            examples:
              default:
                summary: channel.updated
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                  resource:
                    kind: channel
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: channel.updated
                  payload:
                    channel:
                      id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: text
                      name: tickets
                      topic: Customer tickets
                      sort_key: a1
                      read_only: false
                      operator:
                        mode: inherit
                      version: 3
                      settings_version: 1
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  channel.settings_changed:
    post:
      operationId: onChannelSettingsChangedEvent
      summary: Event `channel.settings_changed`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: channel.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelSettingsChangedEvent'
            examples:
              default:
                summary: channel.settings_changed
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                  resource:
                    kind: channel
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: channel.settings_changed
                  payload:
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    settings_version: 1
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  channel.archived:
    post:
      operationId: onChannelArchivedEvent
      summary: Event `channel.archived`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: channel.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ChannelArchivedEvent'
            examples:
              default:
                summary: channel.archived
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                  resource:
                    kind: channel
                    id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: channel.archived
                  payload:
                    channel:
                      id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                      kind: text
                      name: tickets
                      topic: Customer tickets
                      sort_key: a1
                      read_only: false
                      operator:
                        mode: inherit
                      version: 3
                      settings_version: 1
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  command.changed:
    post:
      operationId: onCommandChangedEvent
      summary: Event `command.changed`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: command.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommandChangedEvent'
            examples:
              default:
                summary: command.changed
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope: {}
                  resource:
                    kind: command
                    id: 0192f4a0-7b1c-7beb-80fc-f336f4fe79aa
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: command.changed
                  payload:
                    command:
                      id: 0192f4a0-7b1c-7beb-80fc-f336f4fe79aa
                      owner_principal_id: 0192f4a0-7b1c-7176-8de0-ddf186d11091
                      name: escalate
                      description: Escalate the ticket to second level
                      scope_kind: instance
                      options:
                      - name: ticket
                        description: Ticket number
                        type: string
                        required: true
                      required_level: write
                      control: false
                      version: 3
                    change: registered
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  file.updated:
    post:
      operationId: onFileUpdatedEvent
      summary: Event `file.updated`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: file.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FileUpdatedEvent'
            examples:
              default:
                summary: file.updated
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope: {}
                  resource:
                    kind: file
                    id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: file.updated
                  payload:
                    file:
                      id: 0192f4a0-7b1c-75c7-88f6-199cc394b56a
                      size_bytes: 48213
                      mime_type: image/png
                      original_name: screenshot.png
                      uploaded_by: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      purpose: attachment
                      upload_state: complete
                      scan_status: clean
                      transcript_status: none
                      thumbnails:
                      - variant: small
                        mime_type: image/jpeg
                        width: 320
                        height: 180
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  user.updated:
    post:
      operationId: onUserUpdatedEvent
      summary: Event `user.updated`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: user.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserUpdatedEvent'
            examples:
              default:
                summary: user.updated
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope: {}
                  resource:
                    kind: user
                    id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: user.updated
                  payload:
                    user:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      handle: sam
                      display_name: Sam
                      status: active
                      is_owner: false
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  agent.updated:
    post:
      operationId: onAgentUpdatedEvent
      summary: Event `agent.updated`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: agent.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentUpdatedEvent'
            examples:
              default:
                summary: agent.updated
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope: {}
                  resource:
                    kind: agent
                    id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                    version: 3
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: agent.updated
                  payload:
                    agent:
                      id: 0192f4a0-7b1c-7d07-84b0-358556a08a99
                      handle: support-agent
                      display_name: Support Agent
                      status: active
                      adapter_kind: hermes
                      tool_class: chat
                      description: Answers support tickets
                      billing_mode: personal
                      presence:
                        state: unknown
                      operator_config: {}
                      version: 3
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  instance.updated:
    post:
      operationId: onInstanceUpdatedEvent
      summary: Event `instance.updated`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: instance.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InstanceUpdatedEvent'
            examples:
              default:
                summary: instance.updated
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope: {}
                  resource:
                    kind: instance
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: instance.updated
                  payload:
                    changed_keys: [branding.product_name]
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  status.updated:
    post:
      operationId: onStatusUpdatedEvent
      summary: Event `status.updated`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: status.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StatusUpdatedEvent'
            examples:
              default:
                summary: status.updated
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope: {}
                  resource:
                    kind: instance
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: status.updated
                  payload:
                    overall_state: operational
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
  access.changed:
    post:
      operationId: onAccessChangedEvent
      summary: Event `access.changed`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: self
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccessChangedEvent'
            examples:
              default:
                summary: access.changed
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  audience: self
                  scope: {}
                  resource:
                    kind: principal
                    id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: access.changed
                  payload:
                    permission_version: 418
                    added:
                    - kind: space
                      id: 0192f4a0-7b1c-7835-8fba-fdbdfb6d3b85
                    removed:
                    - kind: space
                      id: 0192f4a0-7b1c-7835-8fba-fdbdfb6d3b85
                    changed:
                    - kind: space
                      id: 0192f4a0-7b1c-7835-8fba-fdbdfb6d3b85
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
      description: Only for the subscribing app itself.
  agent.invocation:
    post:
      operationId: onAgentInvocationEvent
      summary: Event `agent.invocation`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: agent.events.consume
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentInvocationEvent'
      responses:
        '204':
          description: Accepted. Any 2xx status acknowledges every event up to this `seq`.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries this event with backoff and sends nothing newer before it succeeds.
      description: Only for webhook subscriptions of the agent itself (webhook as agent transport, docs/spec/04-sync-protocol.md section 15.6). Carries a freshly issued invocation token in the delivery fields.
  agent.followup:
    post:
      operationId: onAgentFollowupEvent
      summary: Event `agent.followup`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: agent.events.consume
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentFollowupEvent'
      responses:
        '204':
          description: Accepted. Any 2xx status acknowledges every event up to this `seq`.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries this event with backoff and sends nothing newer before it succeeds.
      description: Only for webhook subscriptions of the agent itself (webhook as agent transport, docs/spec/04-sync-protocol.md section 15.6). Carries a freshly issued invocation token in the delivery fields.
  run.cancel_requested:
    post:
      operationId: onRunCancelRequestedEvent
      summary: Event `run.cancel_requested`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: agent.events.consume
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RunCancelRequestedEvent'
      responses:
        '204':
          description: Accepted. Any 2xx status acknowledges every event up to this `seq`.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries this event with backoff and sends nothing newer before it succeeds.
      description: Only for webhook subscriptions of the agent itself (webhook as agent transport, docs/spec/04-sync-protocol.md section 15.6). Carries a freshly issued invocation token in the delivery fields.
  agent.config_changed:
    post:
      operationId: onAgentConfigChangedEvent
      summary: Event `agent.config_changed`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: agent.config.read
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AgentConfigChangedEvent'
      responses:
        '204':
          description: Accepted. Any 2xx status acknowledges every event up to this `seq`.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries this event with backoff and sends nothing newer before it succeeds.
      description: Only for webhook subscriptions of the agent itself (webhook as agent transport, docs/spec/04-sync-protocol.md section 15.6). Carries a freshly issued invocation token in the delivery fields.
  interaction.created:
    post:
      operationId: onInteractionCreatedEvent
      summary: Event `interaction.created`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: agent.events.consume
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InteractionCreatedEvent'
            examples:
              default:
                summary: interaction.created
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    thread_id: 0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                  resource:
                    kind: interaction
                    id: 0192f4a0-7b1c-77f5-8fe4-151103a7b8b5
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: interaction.created
                  payload:
                    interaction:
                      id: 0192f4a0-7b1c-77f5-8fe4-151103a7b8b5
                      component_id: 0192f4a0-7b1c-7080-8a1e-eb7183f5a9a2
                      message_id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                      principal_id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      client_interaction_id: 0192f4a0-7b1c-704d-8f46-22a420803f92
                      values: {}
                      state: received
                      created_at: '2026-10-10T09:30:00.000Z'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
      description: Only for the subscribing app itself.
  command.invoked:
    post:
      operationId: onCommandInvokedEvent
      summary: Event `command.invoked`
      tags: [Webhook events]
      x-authz-action: webhook_out.deliver
      x-rate-limit-class: none
      x-receive-action: agent.events.consume
      security: []
      parameters:
      - $ref: '#/components/parameters/WebhookId'
      - $ref: '#/components/parameters/WebhookTimestamp'
      - $ref: '#/components/parameters/WebhookSignature'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CommandInvokedEvent'
            examples:
              default:
                summary: command.invoked
                value:
                  id: 0192f4a0-7b1c-7ff0-88b6-7d3b051173dd
                  seq: 5301
                  schema_version: 1
                  occurred_at: '2026-10-10T09:30:00.000Z'
                  permission_version: 418
                  scope:
                    space_id: 0192f4a0-7b1c-7881-8522-0a58634abc14
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                  resource:
                    kind: command
                    id: 0192f4a0-7b1c-7beb-80fc-f336f4fe79aa
                  sync_epoch: 0192f4a0-7b1c-7a3e-8c41-5d2e9b7f1a60
                  type: command.invoked
                  payload:
                    invocation_id: 0192f4a0-7b1c-712e-8ccb-17c85f8d11c9
                    command_id: 0192f4a0-7b1c-7beb-80fc-f336f4fe79aa
                    command_name: escalate
                    control: false
                    channel_id: 0192f4a0-7b1c-72d4-8bae-7661d2b2f203
                    invoked_by:
                      id: 0192f4a0-7b1c-7f89-8d6b-6960453241bc
                      kind: user
                      handle: alex
                      display_name: Alex
                    options:
                    - name: ticket
                      value: '4711'
      responses:
        '204':
          description: Accepted. Any 2xx status counts as success.
        '410':
          description: The receiver gave up the endpoint; the server disables the subscription.
        '429':
          description: The server slows down and honors `Retry-After`; same for 502, 503 and 504.
        default:
          description: Failure; the server retries with backoff (docs/api/webhooks.md).
      description: Only for the subscribing app itself.

components:
  securitySchemes:
    dpopAuth:
      type: http
      scheme: DPoP
      description: |-
        Access token of a person, bound to the device key (RFC 9449). Send `Authorization: DPoP <token>`
        and a `DPoP` proof header with `htm`, `htu`, `iat`, `jti`, `ath` and the latest server nonce.
        Also used by API tokens that were created with `dpop_jkt`. Inside a run an agent also sends
        the header `Invocation-Token` (`#/components/parameters/InvocationToken`, 07 guard rule SR9).
    bearerAuth:
      type: http
      scheme: bearer
      description: |-
        API token of an agent, app or personal tool without key binding (`Authorization: Bearer
        <token>`). On `postIncomingWebhookWithBearer` only, the token of an incoming webhook
        (`bothy_whi_...`) instead. Inside a run an agent also sends the header `Invocation-Token`
        (`#/components/parameters/InvocationToken`, 07 guard rule SR9).
    consoleSession:
      type: apiKey
      in: cookie
      name: __Host-console_session
      description: 'Web console cookie; CSRF header `ConsoleRequest` and own `Origin` on writes (08, 7.5).'
    fileTicket:
      type: apiKey
      in: query
      name: ticket
      description: |-
        Short-lived fetch ticket from `createFileTicket` for media players that cannot send a DPoP
        proof (12, 9.2; ADR 0048). Only `downloadFile` and `downloadThumbnail` accept it; every fetch
        checks signature, expiry, session and `file.read`, and any failure gives 404.
    pushPreviewToken:
      type: http
      scheme: DPoP
      description: |-
        Preview token of a push registration (`bothy_pvw_...`, issued once by `registerPush`) with a DPoP proof of the device key of the registration's session, `ath` over the preview token (13, 6.4; R-PN-07). Only `getPushPreview` accepts it; it has no refresh and ends with its registration, its session, its device or its person.
  parameters:
    Cursor:
      name: cursor
      in: query
      description: Cursor from `next_cursor` of the previous page.
      schema:
        $ref: '#/components/schemas/Cursor'
    Limit:
      name: limit
      in: query
      schema:
        $ref: '#/components/schemas/PageLimit'
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      description: UUIDv7 chosen by the client per logical operation. A repeat with the same request returns the stored response; a different request gives 422 `idempotency_key_reuse`; a repeat while the first is running gives 409 `request_in_progress`. Kept for `sync.idempotency_key_ttl`.
      schema:
        $ref: '#/components/schemas/Uuid'
    InvocationToken:
      name: Invocation-Token
      in: header
      required: false
      description: 'The compact JWS of `EventEnvelope.invocation_token` of the run, sent on every request an agent makes inside a run (07 guard rule SR9). The server checks it on every request (signature, audience, run active) and binds the request to the person who triggered the run, so the agent has at most that person''s rights; it never adds a right. The DPoP proof does not cover it. Declared once for every route like `Authorization`, not as a parameter of each operation; an invalid token gives 403 `invocation_token_invalid` with `reason: invalid` on every operation, a valid token of an ended run `reason: run_inactive`, except on `startRun`, `resumeRun` and `renewInvocationToken`, which answer a valid token as without the header. The operations of `webhook_in.post` ignore the header.'
      schema:
        type: string
    IfMatchRequired:
      name: If-Match
      in: header
      required: true
      description: ETag of the version the change is based on, e.g. `"7"`. Mismatch gives 412 `version_conflict`.
      schema:
        type: string
        examples: ['"7"']
    IfMatchOptional:
      name: If-Match
      in: header
      description: Optional ETag; when given, a mismatch gives 412 `version_conflict`.
      schema:
        type: string
        examples: ['"7"']
    DpopProof:
      name: DPoP
      in: header
      required: true
      description: DPoP proof (RFC 9449) signed with the device key that the new tokens will be bound to.
      schema:
        type: string
    DpopProofLogin:
      name: DPoP
      in: header
      description: As `DpopProof`; not for `client = console`.
      schema:
        type: string
    ConsoleRequest:
      name: X-Console-Request
      in: header
      description: CSRF header of the console (SEC-CORS-4).
      schema:
        type: string
        enum: ['1']
    WebhookId:
      name: webhook-id
      in: header
      required: true
      description: Event ID; stable across retries, use it to deduplicate.
      schema:
        $ref: '#/components/schemas/Uuid'
    WebhookTimestamp:
      name: webhook-timestamp
      in: header
      required: true
      description: Unix seconds of this attempt. Reject values outside `webhook.receiver_tolerance`.
      schema:
        type: integer
        format: int64
    WebhookSignature:
      name: webhook-signature
      in: header
      required: true
      description: Space-separated signatures `v1a,<base64>` (Ed25519) and, if enabled, `v1,<base64>` (HMAC-SHA256) over `{webhook-id}.{webhook-timestamp}.{body}`.
      schema:
        type: string
    CodePath:
      name: code
      in: path
      required: true
      schema:
        type: string
        pattern: ^[a-z0-9_]+$
        examples: [version_conflict]
    UserIdPath:
      name: user_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    IdentityIdPath:
      name: identity_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    PasskeyIdPath:
      name: passkey_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    DeviceIdPath:
      name: device_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    RegistrationIdPath:
      name: registration_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    SessionIdPath:
      name: session_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    PrincipalIdPath:
      name: principal_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    TokenIdPath:
      name: token_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    GroupIdPath:
      name: group_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    GrantIdPath:
      name: grant_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    SpaceIdPath:
      name: space_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    ChannelIdPath:
      name: channel_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    KeyPath:
      name: key
      in: path
      required: true
      schema:
        type: string
        pattern: ^[a-z0-9_]+(\.[a-z0-9_]+)*$
        examples: [locale.default]
    ProviderIdPath:
      name: provider_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    TagIdPath:
      name: tag_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    TemplateIdPath:
      name: template_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    WebhookIdPath:
      name: webhook_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    TokenPath:
      name: token
      in: path
      required: true
      schema:
        type: string
        examples: [bothy_whi_EXAMPLE000000000000000000000000000000000000_2WwD3e]
    ThreadIdPath:
      name: thread_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    MessageIdPath:
      name: message_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    EmojiPath:
      name: emoji
      in: path
      required: true
      schema:
        type: string
        description: One Unicode grapheme cluster, percent-encoded.
        examples: ['%F0%9F%91%8D']
    ComponentIdPath:
      name: component_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    InteractionIdPath:
      name: interaction_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    FileIdPath:
      name: file_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    PartNumberPath:
      name: part_number
      in: path
      required: true
      schema:
        type: integer
        minimum: 1
    FileTicketQuery:
      name: ticket
      in: query
      required: false
      description: |-
        Fetch ticket of `createFileTicket` (security scheme `fileTicket`, 12 9.2). Accepted only by
        `downloadFile` and `downloadThumbnail`; ignored when the request carries `Authorization`. A
        bearer secret: never logged.
      schema:
        type: string
    Range:
      name: Range
      in: header
      required: false
      description: |-
        One byte range (RFC 9110 section 14), for example `bytes=0-1023`, `bytes=1024-` or
        `bytes=-500`. Exactly one range gives 206; several ranges and invalid values are ignored (200);
        a range that starts at or after the end gives 416 (12, 9.3).
      schema:
        type: string
    If-Range:
      name: If-Range
      in: header
      required: false
      description: |-
        Applies `Range` only when equal to the current strong `ETag`; any other value, an HTTP date
        included, gives the whole variant (200).
      schema:
        type: string
    VariantPath:
      name: variant
      in: path
      required: true
      schema:
        type: string
        enum: [small, medium, large, poster]
    TargetKindPath:
      name: target_kind
      in: path
      required: true
      schema:
        type: string
        enum: [space, channel, thread]
    TargetIdPath:
      name: target_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    RuleIdPath:
      name: rule_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    AgentIdPath:
      name: agent_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    HostIdPath:
      name: host_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    TerminalSessionIdPath:
      name: session_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    HostIdQuery:
      name: host_id
      in: query
      description: Only sessions on this host.
      schema:
        $ref: '#/components/schemas/Uuid'
    TerminalSessionStateQuery:
      name: state
      in: query
      description: Only sessions in this state; without it, all states.
      schema:
        $ref: '#/components/schemas/TerminalSessionState'
    ConsumerPath:
      name: consumer
      in: path
      required: true
      schema:
        type: string
        pattern: ^[a-z0-9][a-z0-9_-]*$
        examples: [default]
    RunIdPath:
      name: run_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    CommandIdPath:
      name: command_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    RequestIdPath:
      name: request_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    AppIdPath:
      name: app_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    SubscriptionIdPath:
      name: subscription_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    DeliveryIdPath:
      name: delivery_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    PriceIdPath:
      name: price_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    IncidentIdPath:
      name: incident_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
    ImportIdPath:
      name: import_id
      in: path
      required: true
      schema:
        $ref: '#/components/schemas/Uuid'
  headers:
    Correlation-Id:
      description: Correlation ID of this request (its W3C trace ID written as UUID); also in problem bodies, events and audit entries. Clients never send it.
      schema:
        $ref: '#/components/schemas/CorrelationId'
    RateLimit:
      description: Remaining quota of the rate-limit class, format of draft-ietf-httpapi-ratelimit-headers (work in progress), e.g. `"rl.message";r=17;t=4`.
      schema:
        type: string
    RateLimit-Policy:
      description: Quota policy of the rate-limit class, e.g. `"rl.message";q=60;w=60`.
      schema:
        type: string
    Retry-After:
      description: Seconds to wait before retrying (RFC 9110).
      schema:
        type: integer
    ETag:
      description: Strong entity tag of the resource version, e.g. `"7"`.
      schema:
        type: string
    File-ETag:
      description: '`"<sha256 hex>"`, the lowercase hex SHA-256 of the whole delivered variant (12, 9.3); not a version.'
      schema:
        type: string
    Repr-Digest:
      description: '`sha-256=:<base64>:` of the whole delivered variant (RFC 9530); the client checks it after the download.'
      schema:
        type: string
    Accept-Ranges:
      description: Always `bytes`; exactly one range is supported.
      schema:
        type: string
    Content-Range:
      description: '`bytes <first>-<last>/<size>` with 206, `bytes */<size>` with 416 (RFC 9110).'
      schema:
        type: string
    Content-Disposition:
      description: '`inline` only for the inline list of 12 9.3, otherwise `attachment`; always `filename="<ASCII fallback>"` before `filename*=UTF-8''''<percent-encoded>` (RFC 6266, RFC 8187).'
      schema:
        type: string
    File-Content-Security-Policy:
      description: Always `default-src 'none'; sandbox`, so an upload never runs as a page of the instance (12, 9.3).
      schema:
        type: string
    Cross-Origin-Resource-Policy:
      description: Always `same-origin`.
      schema:
        type: string
    File-Cache-Control:
      description: '`private, max-age=<client.cache_max_age>, immutable` with normal authentication, `private, no-store` with a ticket (12, 9.3; ADR 0048).'
      schema:
        type: string
    Idempotent-Replay:
      description: '`true` when the response is the stored original of an earlier identical request.'
      schema:
        type: boolean
    Location:
      description: Target of the redirect.
      schema:
        type: string
        format: uri
    Set-Cookie:
      description: Console cookie or its clearing value (08, 7.5).
      schema:
        type: string
    WWW-Authenticate:
      description: Authentication challenge, e.g. `DPoP error="use_dpop_nonce"` (RFC 9449).
      schema:
        type: string
    DPoP-Nonce:
      description: Fresh server nonce for the next DPoP proof (RFC 9449).
      schema:
        type: string
    Deprecation:
      description: Deprecation date of this operation as structured field date (RFC 9745), e.g. `@1798761599`. Sent by deprecated operations only.
      schema:
        type: string
    Sunset:
      description: Date after which the operation stops working (RFC 8594). Not earlier than `Deprecation` plus `api.deprecation_min_notice`.
      schema:
        type: string
    Link:
      description: '`rel="deprecation"` link to the migration notes of a deprecated operation (RFC 9745).'
      schema:
        type: string
  responses:
    BadRequest:
      x-status: 400
      description: 'Problem codes: `validation_failed`, `malformed_request`, `cursor_invalid`, `webhook_host_not_allowed`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Validation failed
              value:
                type: /api/v1/problems/validation_failed
                title: Validation failed
                status: 400
                code: validation_failed
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                errors:
                - pointer: /content_md
                  code: too_long
                  detail: Longer than `message.max_content_bytes`.
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
    ConsoleLogin:
      x-status: 200
      description: Console session (08, 7.5).
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
        Set-Cookie:
          $ref: '#/components/headers/Set-Cookie'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ConsoleSession'
    StepUpDone:
      x-status: 200
      description: OK; a console session gets a new cookie (08, 7.5).
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
        Set-Cookie:
          $ref: '#/components/headers/Set-Cookie'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/StepUpResult'
    Unauthorized:
      x-status: 401
      description: 'Problem codes: `unauthenticated`, `token_expired`, `session_revoked`, `step_up_required`, `use_dpop_nonce`, `invalid_dpop_proof`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Unauthenticated
              value:
                type: /api/v1/problems/unauthenticated
                title: Unauthenticated
                status: 401
                code: unauthenticated
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
        Set-Cookie:
          $ref: '#/components/headers/Set-Cookie'
        WWW-Authenticate:
          $ref: '#/components/headers/WWW-Authenticate'
        DPoP-Nonce:
          $ref: '#/components/headers/DPoP-Nonce'
    Forbidden:
      x-status: 403
      description: 'Problem codes: `forbidden`, `scope_missing`, `device_pending`, `channel_read_only`, `feature_disabled`, `agent_invocation_denied`, `invocation_token_invalid`, `terminal_denied`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Forbidden
              value:
                type: /api/v1/problems/forbidden
                title: Forbidden
                status: 403
                code: forbidden
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
    NotFound:
      x-status: 404
      description: 'Problem codes: `not_found`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Not found
              value:
                type: /api/v1/problems/not_found
                title: Not found
                status: 404
                code: not_found
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
    Conflict:
      x-status: 409
      description: 'Problem codes: `conflict`, `request_in_progress`, `i1_violation`, `i6_violation`, `last_owner`, `already_started`, `billing_mode_mismatch`, `tool_class_mismatch`, `invalid_state_transition`, `base_version_conflict`, `component_used`, `operator_mismatch`, `file_not_ready`, `file_blocked`, `title_manual`, `host_unavailable`, `fingerprint_mismatch`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Conflict
              value:
                type: /api/v1/problems/conflict
                title: Conflict
                status: 409
                code: conflict
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
    FileConflict:
      x-status: 409
      description: 'Problem codes: `file_not_ready`, `file_blocked`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: File not ready
              value:
                type: /api/v1/problems/file_not_ready
                title: File not ready
                status: 409
                code: file_not_ready
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/files/0192f4a0-7b1c-75c7-88f6-199cc394b56a/content
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
    Gone:
      x-status: 410
      description: 'Problem codes: `resync_required`, `message_deleted`, `gone`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Gone
              value:
                type: /api/v1/problems/gone
                title: Gone
                status: 410
                code: gone
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
    PreconditionFailed:
      x-status: 412
      description: 'Problem codes: `version_conflict`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Version conflict
              value:
                type: /api/v1/problems/version_conflict
                title: Version conflict
                status: 412
                code: version_conflict
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
                current:
                  id: 0192f4a0-7b1c-7353-8d8c-dea659467151
                  version: 8
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
    PayloadTooLarge:
      x-status: 413
      description: 'Problem codes: `payload_too_large`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Payload too large
              value:
                type: /api/v1/problems/payload_too_large
                title: Payload too large
                status: 413
                code: payload_too_large
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
    UnsupportedMediaType:
      x-status: 415
      description: 'Problem codes: `unsupported_media_type`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Unsupported media type
              value:
                type: /api/v1/problems/unsupported_media_type
                title: Unsupported media type
                status: 415
                code: unsupported_media_type
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
    RangeNotSatisfiable:
      x-status: 416
      description: 'Problem codes: `range_not_satisfiable`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Range not satisfiable
              value:
                type: /api/v1/problems/range_not_satisfiable
                title: Range not satisfiable
                status: 416
                code: range_not_satisfiable
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
        Content-Range:
          $ref: '#/components/headers/Content-Range'
    UnprocessableEntity:
      x-status: 422
      description: 'Problem codes: `idempotency_key_reuse`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Idempotency key reused
              value:
                type: /api/v1/problems/idempotency_key_reuse
                title: Idempotency key reused
                status: 422
                code: idempotency_key_reuse
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
    PreconditionRequired:
      x-status: 428
      description: 'Problem codes: `precondition_required`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Precondition required
              value:
                type: /api/v1/problems/precondition_required
                title: Precondition required
                status: 428
                code: precondition_required
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
    TooManyRequests:
      x-status: 429
      description: 'Problem codes: `rate_limited`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Too many requests
              value:
                type: /api/v1/problems/rate_limited
                title: Too many requests
                status: 429
                code: rate_limited
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
        RateLimit:
          $ref: '#/components/headers/RateLimit'
        RateLimit-Policy:
          $ref: '#/components/headers/RateLimit-Policy'
    ServiceUnavailable:
      x-status: 503
      description: 'Problem codes: `unavailable`, `maintenance`, `setup_required`.'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Service unavailable
              value:
                type: /api/v1/problems/unavailable
                title: Service unavailable
                status: 503
                code: unavailable
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
        Retry-After:
          $ref: '#/components/headers/Retry-After'
    Error:
      x-status: 500
      description: Any other error, e.g. `internal_error` (500).
      headers:
        Correlation-Id:
          $ref: '#/components/headers/Correlation-Id'
      content:
        application/problem+json:
          schema:
            $ref: '#/components/schemas/Problem'
          examples:
            default:
              summary: Internal error
              value:
                type: /api/v1/problems/internal_error
                title: Internal error
                status: 500
                code: internal_error
                correlation_id: 0192f4a0-7b1c-7577-8bf0-c4f4ee4f887e
                instance: /api/v1/threads/0192f4a0-7b1c-78f9-8074-fa94e84bab7d
  schemas:
    Uuid:
      type: string
      format: uuid
      description: UUIDv7 (RFC 9562), lowercase canonical form.
    CorrelationId:
      type: string
      format: uuid
      description: W3C trace ID (16 bytes) of the request, written as a lowercase UUID; not a UUIDv7. Generated by the server.
      examples: [4bf92f35-77b3-4da6-a3ce-929d0e0e4736]
    Timestamp:
      type: string
      format: date-time
      description: RFC 3339 timestamp in UTC with millisecond precision and suffix `Z`.
    Cursor:
      type: string
      description: Opaque pagination cursor. Clients must not parse or build cursors.
      examples: [eyJrIjoiMDE5MmY0YTAtN2IxYy03MDAwLThhMDAtMDAwMDAwMDAwMDAxIn0]
    Level:
      type: string
      description: Grant level. Ordered `read` < `write` < `trigger`.
      enum: [read, write, trigger]
    Capability:
      type: string
      description: Capability next to the level. `terminal` exists only on the target `host`.
      enum: [approve_actions, upload, export, manage, terminal]
    PrincipalKind:
      type: string
      enum: [user, agent, app]
    ActorKind:
      type: string
      enum: [user, agent, app, system]
    BillingMode:
      type: string
      description: Backend policy of an agent. `personal` runs on the subscription of exactly one person.
      enum: [personal, organization]
    ToolClass:
      type: string
      enum: [chat, scoped, host]
      description: Tool class of an agent runtime, ordered `chat` < `scoped` < `host` (07, 2.11).
    ToolClassSet:
      type: object
      additionalProperties: false
      required: [tool_class, reason]
      properties:
        tool_class:
          $ref: '#/components/schemas/ToolClass'
        reason:
          type: string
          maxLength: 500
          examples: [Agent now works in a project checkout]
    BillingEvidence:
      type: object
      additionalProperties: false
      description: How the runtime proves its actual billing mode. Never a key value or prefix (schema `billing_evidence.v1`).
      required: [key_type]
      properties:
        provider:
          type: string
          pattern: '^[a-z0-9][a-z0-9_.:-]*$'
          examples: [anthropic]
        key_type:
          type: string
          enum: [api_key, oauth, cloud, none, unknown]
        method:
          type: string
          pattern: '^[a-z0-9][a-z0-9_.:-]*$'
          examples: [resolved_credentials]
    Money:
      type: object
      description: Amount in micro units of the currency. Never a float.
      required: [amount_micros, currency, is_estimate]
      properties:
        amount_micros:
          type: integer
          format: int64
          examples: [1250000]
        currency:
          type: string
          description: ISO 4217 code.
          examples: [USD]
        is_estimate:
          type: boolean
          description: True until the amount was reconciled with the provider invoice.
    CspReportList:
      type: array
      description: Reports of the Reporting API (https://www.w3.org/TR/reporting-1/); each item is a report object whose `body` is a CSP violation report (https://www.w3.org/TR/CSP3/#reporting). The server reads only `type`, `body.effectiveDirective`, `body.blockedURL` and `body.disposition`.
      items:
        $ref: '#/components/schemas/JsonObject'
    CspReportLegacy:
      type: object
      description: Legacy report of the `report-uri` directive (CSP Level 2). The server reads only `effective-directive` (else `violated-directive`), `blocked-uri` and `disposition` of `csp-report`.
      additionalProperties: false
      required: [csp-report]
      properties:
        csp-report:
          $ref: '#/components/schemas/JsonObject'
    JsonObject:
      type: object
      description: Free-form JSON object whose schema is named in the field description.
      additionalProperties: true
    JsonValue:
      description: Any JSON value except null. The schema of the value is defined by its key.
    JsonPatchOperation:
      type: object
      additionalProperties: false
      description: One RFC 6902 operation, restricted to `add`, `replace` and `remove`.
      required: [op, path]
      properties:
        op:
          type: string
          enum: [add, replace, remove]
        path:
          type: string
          description: JSON Pointer (RFC 6901) into the run document.
          examples: [/steps/0192f4a0-7b1c-7000-8a00-0000000000a1/status]
        value:
          $ref: '#/components/schemas/JsonValue'
    PrincipalRef:
      type: object
      description: Minimal public view of a principal, embedded in other objects.
      required: [id, kind, handle, display_name]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        kind:
          $ref: '#/components/schemas/PrincipalKind'
        handle:
          type: string
          examples: [alex]
        display_name:
          type: string
          examples: [Alex]
        avatar_file_id:
          $ref: '#/components/schemas/Uuid'
    Problem:
      type: object
      description: |
        Error body after RFC 9457 (`application/problem+json`). `type` is a relative URI with full
        path `/api/v1/problems/{code}`; `code` repeats its last segment for convenience.
      x-problem-codes:
      - code: validation_failed
        status: 400
        title: 'Validation failed'
        description: 'The request violates the schema or a rule; `errors[]` names the fields.'
        client_action: 'Correct the input; do not retry.'
      - code: malformed_request
        status: 400
        title: 'Malformed request'
        description: 'The body is not valid JSON, has a duplicate member name, invalid UTF-8 or nesting deeper than `http.max_json_depth` (SEC-IN-2).'
        client_action: 'Programming error; do not retry.'
      - code: cursor_invalid
        status: 400
        title: 'Invalid cursor'
        description: 'The cursor is unknown or does not match the filters.'
        client_action: 'Start again without a cursor.'
      - code: webhook_host_not_allowed
        status: 400
        title: 'Webhook host not allowed'
        description: 'The target host is not in `webhook.allowed_hosts`.'
        client_action: 'The owner requests approval of the host.'
      - code: unauthenticated
        status: 401
        title: 'Unauthenticated'
        description: 'No valid principal (token missing, invalid or expired).'
        client_action: 'Renew the token; if that fails, log in again.'
      - code: token_expired
        status: 401
        title: 'Token expired'
        description: 'The access token has expired.'
        client_action: 'Refresh, then retry.'
      - code: session_revoked
        status: 401
        title: 'Session revoked'
        description: 'Session, device or principal revoked.'
        client_action: 'Delete local data and log out (R-SY-14).'
      - code: step_up_required
        status: 401
        title: 'Step-up required'
        description: 'The action needs a fresh confirmation (`auth.step_up_max_age`), or a step-up confirmation failed. For `terminal.open` and `terminal.attach`: younger than `terminal.step_up_max_age`, not yet consumed, by passkey or step-up device key; in the console only by passkey with `userVerification = required`, otherwise `reason: passkey_required` (08 section 6.3). The quick reattach without step-up never applies in the console.'
        client_action: 'Perform step-up, then repeat the same request with the same `Idempotency-Key`.'
      - code: use_dpop_nonce
        status: 401
        title: 'Use DPoP nonce'
        description: 'The server requires a fresh nonce in the DPoP proof; header `DPoP-Nonce`.'
        client_action: 'Build the proof again with the nonce and retry once.'
      - code: invalid_dpop_proof
        status: 401
        title: 'Invalid DPoP proof'
        description: 'The DPoP proof is missing, wrongly signed, expired or does not match `cnf.jkt`.'
        client_action: 'Programming error or device key lost; log in again.'
      - code: forbidden
        status: 403
        title: 'Forbidden'
        description: 'Read access exists, but the level or a capability is missing (R-AZ-01); `reason` is the deny reason of 07 section 4.1 where it reveals nothing, for example `owner_only`, `step_up_not_possible` or `subject_kind_not_allowed`.'
        client_action: 'Do not retry; hide the control.'
      - code: scope_missing
        status: 403
        title: 'Scope missing'
        description: 'The app lacks the required scope.'
        client_action: 'The owner grants the scope.'
      - code: device_pending
        status: 403
        title: 'Device pending'
        description: 'The device or the provisioned account waits for approval.'
        client_action: 'Wait; poll `GET /devices/{device_id}` or `GET /me`.'
      - code: channel_read_only
        status: 403
        title: 'Channel read-only'
        description: 'The channel is read-only (05 rule 1.8) or archived (`reason: channel_archived`, 07 section 4.5); applies to owners too.'
        client_action: 'Lock the input and show a note; do not retry.'
      - code: feature_disabled
        status: 403
        title: 'Feature disabled'
        description: 'The feature is switched off (for example personal tokens).'
        client_action: 'Do not retry.'
      - code: agent_invocation_denied
        status: 403
        title: 'Agent invocation denied'
        description: '`agent.invoke` denies; `reason` for example `billing_mode_personal_not_holder`, `project_not_ready`.'
        client_action: 'Show the reason; do not retry.'
      - code: terminal_denied
        status: 403
        title: 'Terminal denied'
        description: 'The `terminal` capability exists, but a guard rule or limit blocks; `reason` is `not_host_assignee`, `server_host_owner_only`, `i6_holder_only`, `no_session_user` (no own session user on the host, SR12), `session_limit` or `profile_unknown` (24 section 13).'
        client_action: 'Show the reason; do not retry.'
      - code: invocation_token_invalid
        status: 403
        title: 'Invocation token invalid'
        description: 'The invocation token is not accepted by `startRun`, `resumeRun` or `renewInvocationToken`; `reason` is `expired` (valid and current, only `exp` passed), `superseded` (valid, but no longer current: a newer delivery or renewal replaced its `jti`, or a start consumed it while the run waits for input) or `invalid` (signature, `kid`, `typ`, audience or run wrong). Also every request with the header `Invocation-Token` (07 guard rule SR9): `reason` is `invalid` (signature, audience, run, agent or triggering person wrong, or the header is repeated, empty or sent by a principal that is not an agent) or `run_inactive` (the token is valid, but its run has ended).'
        client_action: '`expired`: renew the token (`renewInvocationToken`) and start again. `superseded`: start nothing, keep the run; start with the newer token of the redelivery or, for a waiting run, of the answer. `invalid`: start nothing and report the run as `failed` with `invalid_invocation_token`; on any other request with `Invocation-Token`, stop using the token, the request is refused. `run_inactive`: the run has ended; end the work on it.'
      - code: not_found
        status: 404
        title: 'Not found'
        description: 'The resource does not exist or is not visible; both answer the same (R-AZ-01).'
        client_action: 'Mark the outbox entry as failed.'
      - code: method_not_allowed
        status: 405
        title: 'Method not allowed'
        description: 'The method is not defined on this path.'
        client_action: 'Programming error.'
        router: true
      - code: conflict
        status: 409
        title: 'Conflict'
        description: 'The state prevents the change (for example a name is taken); `reason` names the cause where one is defined.'
        client_action: 'Reload the state; a person decides.'
      - code: request_in_progress
        status: 409
        title: 'Request in progress'
        description: 'The first request with this `Idempotency-Key` is still running.'
        client_action: 'Retry with backoff.'
      - code: i1_violation
        status: 409
        title: 'I1 violation'
        description: 'The change would violate I1; `hint` names the way out.'
        client_action: 'Switch to `organization` first.'
      - code: i6_violation
        status: 409
        title: 'I6 violation'
        description: 'The change would let someone other than the holder of a personal LLM login open a terminal on the host (I6); `hint` names the way out.'
        client_action: 'Revoke the other grants or declare `organization` first.'
      - code: last_owner
        status: 409
        title: 'Last owner'
        description: 'At least one active owner must remain.'
        client_action: 'Appoint another owner first.'
      - code: already_started
        status: 409
        title: 'Already started'
        description: 'The run is no longer `queued`.'
        client_action: 'The adapter starts nothing.'
      - code: billing_mode_mismatch
        status: 409
        title: 'Billing mode mismatch'
        description: 'The reported actual mode differs from the target; the run becomes `failed`.'
        client_action: 'Check the adapter configuration.'
      - code: tool_class_mismatch
        status: 409
        title: 'Tool class mismatch'
        description: 'The reported tool class is above the one in the invocation token; the run becomes `failed`.'
        client_action: 'Check the adapter configuration.'
      - code: host_unavailable
        status: 409
        title: 'Host unavailable'
        description: 'The host is not `active` or its host service is not connected.'
        client_action: 'Show the host state; retry when `host.presence_changed` reports it connected.'
      - code: fingerprint_mismatch
        status: 409
        title: 'Fingerprint mismatch'
        description: 'The typed fingerprint prefix does not match the keys the host service registered; `attempts_remaining` counts the confirmations left, `0` means the host fell back to `pairing` without keys.'
        client_action: 'Compare with the output of `bothy-host pair` and type it again; at `attempts_remaining` `0` issue a new pairing code.'
      - code: invalid_state_transition
        status: 409
        title: 'Invalid state transition'
        description: 'The state machine does not allow the transition.'
        client_action: 'Reload the state.'
      - code: base_version_conflict
        status: 409
        title: 'Base version conflict'
        description: '`base_version` of a run patch is outdated; `current` holds the document.'
        client_action: 'Rebase on `current`.'
      - code: component_used
        status: 409
        title: 'Component used'
        description: 'The component is used, expired or disabled.'
        client_action: 'Refresh the view.'
      - code: operator_mismatch
        status: 409
        title: 'Operator mismatch'
        description: 'The target channel has another effective operator.'
        client_action: 'Choose another target channel.'
      - code: file_not_ready
        status: 409
        title: 'File not ready'
        description: 'Upload incomplete or scan still running.'
        client_action: 'Wait for `file.updated`.'
      - code: file_blocked
        status: 409
        title: 'File blocked'
        description: 'The file is never delivered: malware found, scan error or failed upload (12, 9.3).'
        client_action: 'Show the file as blocked; do not retry.'
      - code: title_manual
        status: 409
        title: 'Title set manually'
        description: 'The operator agent sets a thread title only while `title_source` is `auto` (09 section 12).'
        client_action: 'The agent keeps the manual title.'
      - code: resync_required
        status: 410
        title: 'Resync required'
        description: 'Catch-up is not possible; `reason` as in the gateway.'
        client_action: 'Load a snapshot (04, 7.3).'
      - code: message_deleted
        status: 410
        title: 'Message deleted'
        description: 'The message was deleted in the meantime.'
        client_action: 'Discard the entry; keep the text for copying.'
      - code: gone
        status: 410
        title: 'Gone'
        description: 'The object has expired (upload, export package, autocomplete).'
        client_action: 'Start again.'
      - code: version_conflict
        status: 412
        title: 'Version conflict'
        description: '`If-Match` does not match; `current` holds the current version.'
        client_action: 'Show both versions; a person decides.'
      - code: payload_too_large
        status: 413
        title: 'Payload too large'
        description: 'Body or file above the limit; `detail` names the configuration value.'
        client_action: 'Do not retry.'
      - code: unsupported_media_type
        status: 415
        title: 'Unsupported media type'
        description: 'The media type is not allowed; `reason` `file_type_denied` when the instance operator blocks the type (`file.denied_mime_types`, 12 section 6).'
        client_action: 'Do not retry.'
      - code: range_not_satisfiable
        status: 416
        title: 'Range not satisfiable'
        description: '`Range` lies outside the file.'
        client_action: 'Load without `Range`.'
      - code: idempotency_key_reuse
        status: 422
        title: 'Idempotency key reused'
        description: 'The same client ID or `Idempotency-Key` with different content.'
        client_action: 'Create a new ID only when the person sends again.'
      - code: precondition_required
        status: 428
        title: 'Precondition required'
        description: '`If-Match` is missing.'
        client_action: 'Programming error.'
      - code: rate_limited
        status: 429
        title: 'Too many requests'
        description: 'The rate-limit class is exhausted.'
        client_action: 'Wait for `Retry-After`.'
      - code: internal_error
        status: 500
        title: 'Internal error'
        description: 'Unexpected error; `correlation_id` for the log search.'
        client_action: 'Retry with backoff.'
      - code: unavailable
        status: 503
        title: 'Service unavailable'
        description: 'The server is not ready (start, missing dependency) or a resource is exhausted; `reason` names the cause where one is defined: `storage_full` (12, 3.1), `search_timeout` (14, 7).'
        client_action: 'Wait for `Retry-After`; with `reason` `search_timeout` narrow the query instead of repeating it.'
      - code: maintenance
        status: 503
        title: 'Maintenance'
        description: 'Maintenance mode (`server.maintenance_mode = read_only`); only writing actions are blocked.'
        client_action: 'Wait for `Retry-After`; reading continues, the outbox keeps drafts.'
      - code: setup_required
        status: 503
        title: 'Setup required'
        description: 'The instance is not set up yet; only discovery answers (08 section 4).'
        client_action: 'The owner sets up the instance on the host; there is no `Retry-After`.'
      required: [type, title, status, code, correlation_id]
      properties:
        type:
          type: string
          format: uri-reference
          examples: [/api/v1/problems/not_found]
        title:
          type: string
          examples: [Not found]
        status:
          type: integer
          examples: [404]
        detail:
          type: string
          description: Human-readable explanation for this occurrence. Never contains content or secrets.
        instance:
          type: string
          format: uri-reference
          description: Request path of this occurrence without query string. Path parameters that carry a secret (the `token` of `postIncomingWebhook`) appear as their template, e.g. `{token}`.
          examples: [/api/v1/threads/0192f4a0-7b1c-7000-8a00-000000000301]
        code:
          type: string
          description: Stable machine code; the known values are `x-problem-codes` of this schema. Clients treat unknown codes by HTTP status.
          examples: [not_found]
        correlation_id:
          $ref: '#/components/schemas/CorrelationId'
        reason:
          type: string
          description: 'Machine-readable cause that refines `code` (06 section 5): the deny reason of 07 section 4.1 for denials where it reveals nothing, or a cause named by the chapter of the operation, e.g. `billing_mode_personal_not_holder`, `quota_exceeded`. A new cause is a new `reason`, never a new `code` outside `x-problem-codes`. Clients treat unknown values by `code`.'
        hint:
          type: string
          description: Short instruction for the person making the change, e.g. switch the agent to `organization` first.
        errors:
          type: array
          description: Field errors for `validation_failed`.
          items:
            $ref: '#/components/schemas/FieldError'
        current:
          $ref: '#/components/schemas/JsonObject'
          description: Current representation of the resource for `version_conflict` and `base_version_conflict`.
        attempts_remaining:
          type: integer
          minimum: 0
          description: Present with `fingerprint_mismatch`; `0` means the host fell back to `pairing` and needs a new pairing code (24, 3.2).
        conflicts:
          type: array
          description: Present with `i1_violation` (one entry per agent, 10-backend-policy, 5.4) and `i6_violation` (one entry per host, 24, 7.3).
          items:
            $ref: '#/components/schemas/InvariantViolation'
    FieldError:
      type: object
      required: [pointer, code]
      properties:
        pointer:
          type: string
          description: 'Where the violation is: a JSON Pointer (RFC 6901) to the offending member of the request body (for an unknown or missing member the pointer names that member, e.g. `/name`), or `#path/<name>`, `#query/<name>`, `#header/<name>` for parameters; empty for a rule of the whole request or the body as a whole.'
          examples: [/content_md]
        code:
          type: string
          description: 'Machine-readable rule the field violates. Schema validation uses `required`, `unknown_member` (W7: request schemas are closed), `invalid_type`, `invalid_format`, `invalid_enum`, `too_long`, `too_short`, `too_large`, `too_small`, `too_many_items`, `too_few_items`, `pattern_mismatch` and `invalid` for any other rule; use cases add `limit_reached` and `not_enabled` (06 section 5). Clients treat unknown codes like `invalid`.'
          examples: [too_long]
        detail:
          type: string
          description: Short English explanation; never repeats the submitted value.
    ProblemType:
      type: object
      description: Documentation of one problem type, served at its `type` URI.
      required: [code, status, title, description]
      properties:
        code:
          type: string
          examples: [version_conflict]
        status:
          type: integer
          examples: [412]
        title:
          type: string
          examples: [Version conflict]
        description:
          type: string
        client_action:
          type: string
          description: What a client should do.
    Discovery:
      type: object
      description: Public instance metadata for login screens, SDKs and adapters.
      required:
      - instance_id
      - api_versions
      - spec_version
      - gateway
      - branding
      - auth_providers
      - jwks_uri
      - webhook_keys_uri
      - setup_required
      properties:
        instance_id:
          $ref: '#/components/schemas/Uuid'
        setup_required:
          type: boolean
          description: True until the first owner exists (08, 4); every other operation answers `503 setup_required` until then.
        api_versions:
          type: array
          items:
            type: string
            examples: [v1]
        spec_version:
          type: string
          description: SemVer of openapi.yaml served by this instance.
          examples: [1.0.0]
        features:
          type: array
          description: Feature keys this server implements beyond the base contract of its spec version.
          items:
            type: string
        gateway:
          $ref: '#/components/schemas/GatewayInfo'
        branding:
          $ref: '#/components/schemas/PublicBranding'
        auth_providers:
          type: array
          items:
            $ref: '#/components/schemas/AuthProviderInfo'
        local_password_enabled:
          type: boolean
        jwks_uri:
          type: string
          format: uri-reference
          examples: [/api/v1/instance/jwks]
        webhook_keys_uri:
          type: string
          format: uri-reference
          examples: [/api/v1/instance/webhook-keys]
        min_client_versions:
          type: array
          items:
            $ref: '#/components/schemas/ClientVersionRequirement'
    GatewayInfo:
      type: object
      required: [url, protocol_versions]
      properties:
        url:
          type: string
          format: uri-reference
          examples: [/api/v1/gateway]
        protocol_versions:
          type: array
          items:
            type: integer
            examples: [1]
    ClientVersionRequirement:
      type: object
      required: [platform, min_version]
      properties:
        platform:
          $ref: '#/components/schemas/DevicePlatform'
        min_version:
          type: string
          description: Below this version the app does not connect (`client.min_version.<platform>`, 18 section 9).
          examples: [1.0.0]
        recommended_version:
          type: string
          description: Below this version the app shows an update note (`client.recommended_version.<platform>`).
          examples: [1.2.0]
    AuthProviderInfo:
      type: object
      required: [id, kind, name]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        kind:
          $ref: '#/components/schemas/IdentityProviderKind'
        name:
          type: string
          examples: [Company SSO]
    PublicBranding:
      type: object
      required: [product_name]
      properties:
        product_name:
          type: string
          examples: [Team Chat]
        accent_color:
          type: string
          description: Key of a category color of the design system (`spruce`, `loch`, `heather`, `rowan`, `amber`, `gorse`, `moss`), never a free color value (ADR 0044).
          examples: [loch]
        logo_url:
          type: string
          format: uri-reference
          examples: [/api/v1/instance/branding/logo]
    Branding:
      type: object
      required: [product_name, version]
      properties:
        product_name:
          type: string
          examples: [Team Chat]
        accent_color:
          type: string
          examples: [loch]
        logo_file_id:
          $ref: '#/components/schemas/Uuid'
        version:
          type: integer
          format: int64
    BrandingUpdate:
      type: object
      additionalProperties: false
      required: [product_name]
      properties:
        product_name:
          type: string
          minLength: 1
          examples: [Team Chat]
        accent_color:
          type: string
          examples: [loch]
    Jwks:
      type: object
      description: JSON Web Key Set (RFC 7517) with the public keys for access tokens, invocation tokens and terminal session tokens.
      required: [keys]
      properties:
        keys:
          type: array
          items:
            $ref: '#/components/schemas/JsonObject'
    WebhookKeys:
      type: object
      description: Public keys for Standard Webhooks `v1a` signatures.
      required: [keys]
      properties:
        keys:
          type: array
          items:
            $ref: '#/components/schemas/WebhookKey'
    WebhookKey:
      type: object
      required: [kid, public_key, state]
      properties:
        kid:
          type: string
          examples: [whk-2026-10]
        public_key:
          type: string
          description: Ed25519 public key with prefix `whpk_`, followed by the standard base64 (with padding) of its DER-encoded SubjectPublicKeyInfo (RFC 8410).
          examples: [whpk_MCowBQYDK2VwAyEA6h3uZ0Q4i1Yv1o1Wg8bQq3c0o8bq9oQm3Kc8n3m3v0I=]
        state:
          type: string
          enum: [next, active, retiring]
        not_after:
          $ref: '#/components/schemas/Timestamp'
    InstanceInfo:
      type: object
      description: Instance view for an authenticated principal.
      required:
      - instance_id
      - branding
      - locale_default
      - features
      - limits
      - spec_version
      properties:
        instance_id:
          $ref: '#/components/schemas/Uuid'
        branding:
          $ref: '#/components/schemas/Branding'
        locale_default:
          type: string
          examples: [de]
        spec_version:
          type: string
          examples: [1.0.0]
        features:
          type: array
          description: Feature flags evaluated for the caller.
          items:
            $ref: '#/components/schemas/EvaluatedFlag'
        limits:
          $ref: '#/components/schemas/InstanceLimits'
        telemetry_dashboard_url:
          type: string
          format: uri
          description: 'Link of the administration to the telemetry backend, from the configuration key `telemetry.dashboard_url` (17 8). Present only for owners and only when the key is set, because the address names internal infrastructure; clients open it in the system browser or a new tab, never in a WebView or iframe.'
          examples: ['https://telemetry.example.com/d/bothy']
        stats_time_zone:
          type: string
          description: 'IANA name of the time zone in which the day, week and month buckets of the statistics operations start (`stats.time_zone`, 17 8); clients align the ranges of their statistics queries to its calendar days.'
          examples: [Europe/Berlin]
    EvaluatedFlag:
      type: object
      required: [key, enabled]
      properties:
        key:
          type: string
          examples: [dm.enabled]
        enabled:
          type: boolean
    InstanceLimits:
      type: object
      description: Effective values of limits that clients enforce before sending.
      required:
      - message_max_content_bytes
      - message_max_attachments
      - file_max_upload_bytes
      - thread_title_max_chars
      - voice_max_duration_ms
      properties:
        message_max_content_bytes:
          type: integer
          format: int64
          examples: [262144]
        message_max_attachments:
          type: integer
          examples: [10]
        file_max_upload_bytes:
          type: integer
          format: int64
          examples: [104857600]
        thread_title_max_chars:
          type: integer
          examples: [200]
        voice_max_duration_ms:
          type: integer
          format: int64
          description: Longest voice message (`voice.max_duration`, 12).
          examples: [900000]
    InstanceSetting:
      type: object
      description: A registered key without a stored row has its default value and `version` 0, without `updated_by` and `updated_at`.
      required: [key, value, version]
      properties:
        key:
          type: string
          examples: [locale.default]
        value:
          $ref: '#/components/schemas/JsonValue'
        version:
          type: integer
          format: int64
        updated_by:
          $ref: '#/components/schemas/Uuid'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
    InstanceSettingPut:
      type: object
      additionalProperties: false
      required: [value]
      properties:
        value:
          $ref: '#/components/schemas/JsonValue'
    InstanceSettingList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/InstanceSetting'
    FeatureFlag:
      type: object
      description: Registration from the code plus the stored value (ADR 0064). A key without a stored row has its default and `version` 0; a stored key without registration is `orphaned` and off.
      required: [key, enabled, rollout, version, orphaned]
      properties:
        key:
          type: string
          examples: [voice.enabled]
        kind:
          type: string
          enum: [release, option]
          description: Absent for orphaned keys.
        description:
          type: string
          description: From the registration (English); absent for orphaned keys.
          examples: [Voice messages with transcription]
        default:
          type: boolean
          description: Registered default; absent for orphaned keys.
        remove_by:
          type: string
          description: Server version by which a `release` flag is removed.
          examples: [1.4.0]
        module:
          type: string
          description: Module that owns the flag.
          examples: [files]
        orphaned:
          type: boolean
        enabled:
          type: boolean
          description: Global value; the stored value or the registered default.
        rollout:
          $ref: '#/components/schemas/FlagRollout'
        version:
          type: integer
          format: int64
        updated_by:
          $ref: '#/components/schemas/Uuid'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
    FlagRollout:
      type: object
      additionalProperties: false
      description: Principals and groups for which the flag has the opposite value of `enabled`.
      properties:
        user_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        group_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
    FeatureFlagPut:
      type: object
      additionalProperties: false
      required: [enabled, reason]
      properties:
        enabled:
          type: boolean
        rollout:
          $ref: '#/components/schemas/FlagRollout'
        reason:
          type: string
          minLength: 1
          examples: [Pilot with the support team]
    FeatureFlagList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/FeatureFlag'
    SigningKeyRotate:
      type: object
      additionalProperties: false
      required: [purpose, mode, reason]
      properties:
        purpose:
          $ref: '#/components/schemas/SigningKeyPurpose'
        mode:
          type: string
          description: |
            `scheduled` announces the next key (state `next`); the rotation job makes it `active`
            after `agent.jwks_max_age`. `emergency` retires every key of the purpose at once and
            creates a new `active` key. Required, because the two modes have opposite effects.
          enum: [scheduled, emergency]
          examples: [scheduled]
        reason:
          type: string
          examples: [Scheduled rotation]
    SigningKeyRotation:
      type: object
      required: [purpose, new_kid, previous_kid, previous_not_after]
      properties:
        purpose:
          $ref: '#/components/schemas/SigningKeyPurpose'
        new_kid:
          type: string
          examples: [inv-2026-11]
        previous_kid:
          type: string
          examples: [inv-2026-10]
        previous_not_after:
          $ref: '#/components/schemas/Timestamp'
    IdentityProvider:
      type: object
      required: [id, kind, name, config, enabled, allow_provisioning]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        kind:
          $ref: '#/components/schemas/IdentityProviderKind'
        name:
          type: string
          examples: [Company SSO]
        config:
          $ref: '#/components/schemas/JsonObject'
          description: Schema `identity_provider.<kind>.v1`. Secrets appear only as `secret_ref` names.
        enabled:
          type: boolean
        allow_provisioning:
          type: boolean
        updated_at:
          $ref: '#/components/schemas/Timestamp'
    IdentityProviderCreate:
      type: object
      additionalProperties: false
      required: [kind, name, config]
      properties:
        kind:
          $ref: '#/components/schemas/IdentityProviderKind'
        name:
          type: string
          examples: [Company SSO]
        config:
          $ref: '#/components/schemas/JsonObject'
        enabled:
          type: boolean
        allow_provisioning:
          type: boolean
    IdentityProviderUpdate:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
        config:
          $ref: '#/components/schemas/JsonObject'
        enabled:
          type: boolean
        allow_provisioning:
          type: boolean
    IdentityProviderList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/IdentityProvider'
    DevicePlatform:
      type: string
      enum: [ios, android, macos, windows, web, cli]
    DeviceRegistration:
      type: object
      additionalProperties: false
      description: Device and device keys sent with every login completion. The DPoP proof of the request must be signed with the `dpop` key (`public_jwk`); `step_up_key` is the second key with user verification (08, 6.1), absent where no secure key store exists.
      required: [device_id, name, platform, public_jwk, alg]
      properties:
        device_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Alex iPhone]
        platform:
          $ref: '#/components/schemas/DevicePlatform'
        app_version:
          type: string
          examples: [1.4.0]
        public_jwk:
          $ref: '#/components/schemas/JsonObject'
          description: Public JWK (RFC 7517) of the device key, without private members.
        alg:
          type: string
          enum: [ES256, EdDSA]
        attestation:
          $ref: '#/components/schemas/JsonObject'
          description: Platform attestation, if the platform provides one.
        step_up_key:
          $ref: '#/components/schemas/DeviceKeyRegistration'
    DeviceKeyRegistration:
      type: object
      additionalProperties: false
      required: [public_jwk, alg]
      properties:
        public_jwk:
          $ref: '#/components/schemas/JsonObject'
          description: Public JWK (RFC 7517) without private members.
        alg:
          type: string
          enum: [ES256, EdDSA]
        attestation:
          $ref: '#/components/schemas/JsonObject'
    PairingBeginRequest:
      type: object
      additionalProperties: false
      required: [device]
      properties:
        device:
          $ref: '#/components/schemas/DeviceRegistration'
    PairingStarted:
      type: object
      required: [pairing_id, pairing_code, expires_at]
      properties:
        pairing_id:
          $ref: '#/components/schemas/Uuid'
        pairing_code:
          type: string
          description: Shown on the new device only; `auth.pairing_code_chars` characters (Crockford Base32).
          examples: [7KQ2-M9XD]
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    DeviceApprove:
      type: object
      additionalProperties: false
      required: [verification_code]
      properties:
        verification_code:
          type: string
          pattern: '^[0-9]+$'
          description: Code shown on the pending device, typed by the approving person.
          examples: ['042517']
    PairingApprove:
      type: object
      additionalProperties: false
      required: [pairing_code]
      properties:
        pairing_code:
          type: string
          examples: [7KQ2-M9XD]
    PairingCompleteRequest:
      type: object
      additionalProperties: false
      required: [pairing_id]
      properties:
        pairing_id:
          $ref: '#/components/schemas/Uuid'
    PairingStatus:
      type: object
      required: [pairing_id, status, expires_at]
      properties:
        pairing_id:
          $ref: '#/components/schemas/Uuid'
        status:
          type: string
          enum: [requested]
          description: Not yet approved; ask again after a short wait.
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    OidcBeginRequest:
      type: object
      additionalProperties: false
      required: [provider_id, app_redirect_uri, client_challenge]
      properties:
        client:
          $ref: '#/components/schemas/LoginClient'
        provider_id:
          $ref: '#/components/schemas/Uuid'
        app_redirect_uri:
          type: string
          description: Redirect target of the app (loopback or claimed scheme, RFC 8252). Must match the allow-list of the provider.
          examples: ['com.example.teamchat:/login']
        client_challenge:
          type: string
          description: Base64url SHA-256 of a client verifier, binds the later `login_code` to this app instance.
          examples: [E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM]
        enrollment_code:
          type: string
          description: Optional enrollment code (purpose `first_login`, `recovery` or `identity_link`). Checked at begin, consumed at complete; an unknown subject is then linked to the person of the code (08, 3.1).
          examples: [en_EXAMPLE]
    OidcBeginResponse:
      type: object
      required: [login_attempt_id, authorization_url, expires_at]
      properties:
        login_attempt_id:
          $ref: '#/components/schemas/Uuid'
        authorization_url:
          type: string
          format: uri
          examples: ['https://login.example.com/authorize?client_id=chat&state=s1']
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    OidcCompleteRequest:
      type: object
      additionalProperties: false
      required: [login_code, client_verifier]
      properties:
        client:
          $ref: '#/components/schemas/LoginClient'
        login_code:
          type: string
          description: One-time code the server appended to `app_redirect_uri` after the provider callback.
          examples: [lc_8f2d1c]
        client_verifier:
          type: string
          examples: [dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk]
        device:
          $ref: '#/components/schemas/DeviceRegistration'
    TrustedHeaderLoginRequest:
      type: object
      additionalProperties: false
      properties:
        client:
          $ref: '#/components/schemas/LoginClient'
        device:
          $ref: '#/components/schemas/DeviceRegistration'
        enrollment_code:
          type: string
          description: Optional enrollment code; links an unknown header login to the person of the code (08, 3.1).
          examples: [en_EXAMPLE]
    EnrollRedeemRequest:
      type: object
      additionalProperties: false
      description: Redeems an enrollment code with device keys only; the redeeming device becomes `active` (08, 3.2).
      required: [enrollment_code, device]
      properties:
        enrollment_code:
          type: string
          examples: [en_EXAMPLE]
        device:
          $ref: '#/components/schemas/DeviceRegistration'
    ExtensionSessionCreate:
      type: object
      additionalProperties: false
      description: Child session of the calling app session for an app extension with its own refresh family (08, 7.4).
      required: [client_component]
      properties:
        client_component:
          type: string
          enum: [share_ext]
    WebAuthnChallenge:
      type: object
      required: [challenge_id, public_key, expires_at]
      properties:
        challenge_id:
          $ref: '#/components/schemas/Uuid'
          description: ID of the stored ceremony challenge; valid once until `expires_at`.
        public_key:
          $ref: '#/components/schemas/JsonObject'
          description: WebAuthn `PublicKeyCredentialRequestOptions` or `PublicKeyCredentialCreationOptions` in JSON form.
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    PasskeyLoginBeginRequest:
      type: object
      additionalProperties: false
      properties:
        login_name:
          type: string
          description: Optional; without it the server offers discoverable credentials.
          examples: [alex]
    PasskeyLoginCompleteRequest:
      type: object
      additionalProperties: false
      required: [challenge_id, credential]
      properties:
        client:
          $ref: '#/components/schemas/LoginClient'
        challenge_id:
          $ref: '#/components/schemas/Uuid'
        credential:
          $ref: '#/components/schemas/JsonObject'
          description: WebAuthn `PublicKeyCredential` in JSON form.
        device:
          $ref: '#/components/schemas/DeviceRegistration'
    EnrollBeginRequest:
      type: object
      additionalProperties: false
      required: [enrollment_code]
      properties:
        enrollment_code:
          type: string
          examples: [en_4KX9-2MQA]
    EnrollCompleteRequest:
      type: object
      additionalProperties: false
      required: [challenge_id, credential, passkey_name, device]
      properties:
        challenge_id:
          $ref: '#/components/schemas/Uuid'
        credential:
          $ref: '#/components/schemas/JsonObject'
        passkey_name:
          type: string
          examples: [Laptop]
        device:
          $ref: '#/components/schemas/DeviceRegistration'
    PasswordLoginRequest:
      type: object
      additionalProperties: false
      required: [login_name, password, totp_code]
      properties:
        client:
          $ref: '#/components/schemas/LoginClient'
        login_name:
          type: string
          examples: [alex]
        password:
          type: string
          format: password
        totp_code:
          type: string
          examples: ['492039']
        device:
          $ref: '#/components/schemas/DeviceRegistration'
    TokenRefreshRequest:
      type: object
      additionalProperties: false
      required: [refresh_token]
      properties:
        refresh_token:
          type: string
          examples: [rt_q2Z0]
    TokenResponse:
      type: object
      description: Token pair after login or refresh. `token_type` is always `DPoP` (RFC 9449).
      required:
      - access_token
      - token_type
      - expires_in
      - refresh_token
      - refresh_expires_at
      - session
      - device
      properties:
        access_token:
          type: string
          examples: [eyJhbGciOiJFZERTQSJ9.e30.sig]
        token_type:
          type: string
          enum: [DPoP]
        expires_in:
          type: integer
          description: Seconds until the access token expires.
          examples: [900]
        refresh_token:
          type: string
          examples: [rt_q2Z0]
        refresh_expires_at:
          $ref: '#/components/schemas/Timestamp'
        session:
          $ref: '#/components/schemas/Session'
        device:
          $ref: '#/components/schemas/Device'
    LoginClient:
      type: string
      enum: [app, console]
      description: '`console`: no `device` or DPoP; cookie, no token (08, 7.5).'
    ConsoleSession:
      type: object
      required: [user_id, display_name, expires_at, idle_timeout_seconds, step_up_methods_available]
      properties:
        user_id:
          $ref: '#/components/schemas/Uuid'
        display_name:
          type: string
        expires_at:
          $ref: '#/components/schemas/Timestamp'
        idle_timeout_seconds:
          type: integer
        step_up_at:
          $ref: '#/components/schemas/Timestamp'
        step_up_method:
          type: string
          enum: [passkey, password_totp, oidc_reauth]
        step_up_valid_until:
          $ref: '#/components/schemas/Timestamp'
        step_up_methods_available:
          type: array
          items:
            type: string
            enum: [passkey, oidc_reauth, password_totp]
        view_as:
          type: object
          required: [user_id, display_name, started_at, ends_at]
          properties:
            user_id:
              $ref: '#/components/schemas/Uuid'
            display_name:
              type: string
            started_at:
              $ref: '#/components/schemas/Timestamp'
            ends_at:
              $ref: '#/components/schemas/Timestamp'
    StepUpPasswordRequest:
      type: object
      additionalProperties: false
      required: [challenge_id, password, totp_code]
      properties:
        challenge_id:
          $ref: '#/components/schemas/Uuid'
        password:
          type: string
          format: password
        totp_code:
          type: string
    StepUpOidcBeginRequest:
      type: object
      additionalProperties: false
      required: [challenge_id, provider_id, app_redirect_uri, client_challenge]
      properties:
        challenge_id:
          $ref: '#/components/schemas/Uuid'
        provider_id:
          $ref: '#/components/schemas/Uuid'
        app_redirect_uri:
          type: string
        client_challenge:
          type: string
    StepUpChallenge:
      type: object
      required: [challenge_id, challenge, expires_at]
      properties:
        challenge_id:
          $ref: '#/components/schemas/Uuid'
        challenge:
          type: string
          description: Base64url random value to sign. Single use, valid `auth.step_up_challenge_ttl`. Clients show their own localized prompt text.
          examples: [Zm9vYmFyYmF6]
        passkey_options:
          $ref: '#/components/schemas/JsonObject'
          description: WebAuthn request options when the user has passkeys; their challenge is this step-up challenge.
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    StepUpPasskeyRequest:
      type: object
      additionalProperties: false
      required: [challenge_id, credential]
      properties:
        challenge_id:
          $ref: '#/components/schemas/Uuid'
        credential:
          $ref: '#/components/schemas/JsonObject'
    StepUpDeviceKeyRequest:
      type: object
      additionalProperties: false
      required: [challenge_id, signature]
      properties:
        challenge_id:
          $ref: '#/components/schemas/Uuid'
        signature:
          type: string
          description: 'Compact JWS signed by the active `step_up` key of the session''s device after user verification on the device. Header `alg` only (no `jwk`, `jku`, `x5u`, `x5c`, `crit`); payload `{"challenge": <challenge>, "sid": <session id>, "iat": <NumericDate>}` (08, 6.2).'
          examples: [eyJhbGciOiJFUzI1NiJ9.EXAMPLEpayload.EXAMPLEsignature]
    StepUpResult:
      type: object
      required: [step_up_at, valid_until]
      properties:
        step_up_at:
          $ref: '#/components/schemas/Timestamp'
        valid_until:
          $ref: '#/components/schemas/Timestamp'
    Me:
      type: object
      description: The calling principal.
      required: [principal, is_owner, permission_version]
      properties:
        principal:
          $ref: '#/components/schemas/PrincipalRef'
        is_owner:
          type: boolean
        email:
          type: string
          format: email
          examples: [alex@example.com]
        locale:
          type: string
          examples: [de]
        time_zone:
          type: string
          examples: [Europe/Berlin]
        permission_version:
          type: integer
          format: int64
        session_id:
          $ref: '#/components/schemas/Uuid'
        device_id:
          $ref: '#/components/schemas/Uuid'
        step_up_valid_until:
          $ref: '#/components/schemas/Timestamp'
    User:
      type: object
      description: Directory entry of a person.
      required: [id, handle, display_name, status, is_owner, created_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        handle:
          type: string
          examples: [sam]
        display_name:
          type: string
          examples: [Sam]
        avatar_file_id:
          $ref: '#/components/schemas/Uuid'
        status:
          $ref: '#/components/schemas/UserStatus'
          examples: [active]
        is_owner:
          type: boolean
        locale:
          type: string
          examples: [en]
        time_zone:
          type: string
          examples: [Europe/Berlin]
        created_at:
          $ref: '#/components/schemas/Timestamp'
    UserCreate:
      type: object
      additionalProperties: false
      required: [handle, display_name]
      properties:
        handle:
          type: string
          pattern: ^[a-z0-9][a-z0-9._-]*$
          examples: [robin]
        display_name:
          type: string
          examples: [Robin]
        email:
          type: string
          format: email
          examples: [robin@example.com]
        locale:
          type: string
          examples: [de]
        time_zone:
          type: string
          examples: [Europe/Berlin]
        local_account:
          type: boolean
          description: Create an enrollment code for the local identity provider.
    UserCreated:
      type: object
      required: [user]
      properties:
        user:
          $ref: '#/components/schemas/User'
        enrollment:
          $ref: '#/components/schemas/EnrollmentCode'
    UserUpdate:
      type: object
      additionalProperties: false
      properties:
        display_name:
          type: string
        email:
          type: string
          format: email
        locale:
          type: string
        time_zone:
          type: string
    ProfileUpdate:
      type: object
      additionalProperties: false
      properties:
        display_name:
          type: string
          examples: [Alex]
        locale:
          type: string
          examples: [de]
        time_zone:
          type: string
          examples: [Europe/Berlin]
    EnrollmentCode:
      type: object
      description: 'One-time code shown once to the owner, handed to the person out of band. A replay of the creating request with the same `Idempotency-Key` does not repeat it (`409 conflict`, `reason: one_time_secret`).'
      required: [code, purpose, expires_at]
      properties:
        code:
          type: string
          examples: [en_4KX9-2MQA]
        purpose:
          type: string
          enum: [first_login, recovery, identity_link]
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    ReasonBody:
      type: object
      additionalProperties: false
      required: [reason]
      properties:
        reason:
          type: string
          minLength: 1
          examples: [Left the team]
    ResolveI1:
      type: object
      additionalProperties: false
      description: 'Guided flow (05 rule 1.6, 10 section 11): switch these agents to `organization` first, in the same transaction as the change.'
      required: [switch_to_organization, reason]
      properties:
        switch_to_organization:
          type: array
          minItems: 1
          uniqueItems: true
          items:
            $ref: '#/components/schemas/Uuid'
        reason:
          type: string
          minLength: 1
          description: Reason of the switch, in the audit entry of every switched agent; at most `audit.reason_max_chars` characters, no control or bidi characters.
          examples: [Sam triggers the assistant too]
    OwnerAdd:
      type: object
      additionalProperties: false
      required: [user_id, reason]
      properties:
        user_id:
          $ref: '#/components/schemas/Uuid'
        reason:
          type: string
          examples: [Second owner as deputy]
        resolve_i1:
          $ref: '#/components/schemas/ResolveI1'
    Owner:
      type: object
      required: [user, granted_at, reason]
      properties:
        user:
          $ref: '#/components/schemas/PrincipalRef'
        granted_by:
          $ref: '#/components/schemas/Uuid'
        granted_at:
          $ref: '#/components/schemas/Timestamp'
        reason:
          type: string
    OwnerList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Owner'
    Identity:
      type: object
      required: [id, provider_id, provider_kind, subject, linked_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        provider_id:
          $ref: '#/components/schemas/Uuid'
        provider_kind:
          $ref: '#/components/schemas/IdentityProviderKind'
        subject:
          type: string
          examples: [alex]
        issuer:
          type: string
          examples: ['https://login.example.com']
        email_at_link:
          type: string
          format: email
        linked_at:
          $ref: '#/components/schemas/Timestamp'
        last_login_at:
          $ref: '#/components/schemas/Timestamp'
    Passkey:
      type: object
      required: [id, name, created_at, backup_eligible]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Laptop]
        aaguid:
          $ref: '#/components/schemas/Uuid'
        backup_eligible:
          type: boolean
        created_at:
          $ref: '#/components/schemas/Timestamp'
        last_used_at:
          $ref: '#/components/schemas/Timestamp'
    CredentialOverview:
      type: object
      required: [identities, passkeys, local_password_set, totp_enrolled]
      properties:
        identities:
          type: array
          items:
            $ref: '#/components/schemas/Identity'
        passkeys:
          type: array
          items:
            $ref: '#/components/schemas/Passkey'
        local_password_set:
          type: boolean
        totp_enrolled:
          type: boolean
    IdentityLinkBegin:
      type: object
      additionalProperties: false
      required: [provider_id, app_redirect_uri, client_challenge]
      properties:
        provider_id:
          $ref: '#/components/schemas/Uuid'
        app_redirect_uri:
          type: string
          examples: ['com.example.teamchat:/link']
        client_challenge:
          type: string
          examples: [E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM]
    IdentityLinkComplete:
      type: object
      additionalProperties: false
      required: [login_code, client_verifier]
      properties:
        login_code:
          type: string
          examples: [lc_8f2d1c]
        client_verifier:
          type: string
          examples: [dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk]
    PasskeyRegisterComplete:
      type: object
      additionalProperties: false
      required: [challenge_id, credential, name]
      properties:
        challenge_id:
          $ref: '#/components/schemas/Uuid'
        credential:
          $ref: '#/components/schemas/JsonObject'
        name:
          type: string
          examples: [Laptop]
    LocalCredentialSet:
      type: object
      additionalProperties: false
      description: Sets or replaces the local password, starts a TOTP enrollment (`totp_enroll`, the response carries the new secret once) or confirms it (`totp_code`). At least one member; the first call sets `password`. The password path counts as established once TOTP is confirmed (08, 2.5).
      properties:
        password:
          type: string
          format: password
        totp_enroll:
          type: boolean
        totp_code:
          type: string
          pattern: '^[0-9]{6}$'
          description: Current code of the enrolled TOTP secret; confirms the enrollment.
          examples: ['492039']
    LocalCredentialResult:
      type: object
      required: [password_set, totp_enabled]
      properties:
        password_set:
          type: boolean
        totp_enabled:
          type: boolean
          description: True once a TOTP enrollment is confirmed.
        totp_secret_uri:
          type: string
          description: otpauth URI, returned once when `totp_enroll` was set.
          examples: ['otpauth://totp/Team%20Chat:alex?secret=JBSWY3DPEHPK3PXP']
    Device:
      type: object
      required: [id, user_id, name, platform, state, created_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        user_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Alex iPhone]
        platform:
          $ref: '#/components/schemas/DevicePlatform'
        state:
          type: string
          enum: [pending, active, revoked]
          examples: [active]
        app_version:
          type: string
          examples: [1.4.0]
        key_thumbprint:
          type: string
          description: RFC 7638 thumbprint of the active `dpop` device key (`cnf.jkt`).
          examples: [0ZcOCORZNYy-DWpqq30jZyJGHTN0d2HglBV3uiguA4I]
        step_up_key_thumbprint:
          type: string
          description: RFC 7638 thumbprint of the active `step_up` device key, if the device has one.
        verification_code:
          type: string
          pattern: '^[0-9]+$'
          description: Only in responses to the pending device itself (`TokenResponse`, `getDevice`, `listMyDevices` for its own entry); never shown to other devices or owners (08, 5.2).
        revoke_reason:
          type: string
          enum: [revoked_by_self, revoked_by_owner, user_disabled, inactive, verification_failed, pending_expired, pairing_expired]
        approved_at:
          $ref: '#/components/schemas/Timestamp'
        revoked_at:
          $ref: '#/components/schemas/Timestamp'
        created_at:
          $ref: '#/components/schemas/Timestamp'
        last_seen_at:
          $ref: '#/components/schemas/Timestamp'
        push_registrations:
          type: array
          description: Every push registration of the device, active and disabled, ascending by `id`, never with a token (13, 13). Absent when the device has none.
          items:
            $ref: '#/components/schemas/PushRegistration'
    Session:
      type: object
      required:
      - id
      - user_id
      - device_id
      - auth_method
      - created_at
      - expires_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        user_id:
          $ref: '#/components/schemas/Uuid'
        device_id:
          $ref: '#/components/schemas/Uuid'
        auth_method:
          type: string
          enum: [oidc, trusted_header, passkey, password_totp, enrollment_code, device_pairing]
        client_component:
          type: string
          enum: [app, share_ext, console]
        created_at:
          $ref: '#/components/schemas/Timestamp'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
        step_up_at:
          $ref: '#/components/schemas/Timestamp'
        step_up_method:
          type: string
          enum: [passkey, device_key, password_totp, oidc_reauth]
        last_seen_at:
          $ref: '#/components/schemas/Timestamp'
        current:
          type: boolean
          description: True for the session of this request. Set only in REST responses.
    ApiToken:
      type: object
      required: [id, principal_id, name, token_prefix, scopes, created_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        principal_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Adapter production]
        token_prefix:
          type: string
          description: '`bothy_<kind>_` plus the first characters of the random part (03 `api_tokens`).'
          examples: [bothy_agt_EXAMPL]
        scopes:
          type: array
          items:
            $ref: '#/components/schemas/AppScope'
        dpop_jkt:
          type: string
        expires_at:
          $ref: '#/components/schemas/Timestamp'
        created_at:
          $ref: '#/components/schemas/Timestamp'
        last_used_at:
          $ref: '#/components/schemas/Timestamp'
        revoked_at:
          $ref: '#/components/schemas/Timestamp'
    ApiTokenCreate:
      type: object
      additionalProperties: false
      required: [name, expires_at]
      properties:
        name:
          type: string
          examples: [Adapter production]
        scopes:
          type: array
          description: Only for app tokens, a subset of the granted scopes of the installation; absent or empty takes all granted scopes at this moment. A scope granted later needs a new token. Agent tokens (all agent scopes implicitly) and personal tokens (the grants of the person) carry none.
          items:
            $ref: '#/components/schemas/AppScope'
        dpop_jkt:
          type: string
          description: RFC 7638 thumbprint (43 base64url characters) of the key the token is bound to; requests must then use DPoP. Required for the kinds in `auth.dpop_required_for`.
        expires_at:
          $ref: '#/components/schemas/Timestamp'
          description: Required; at most `auth.api_token_max_ttl` (agents, apps) or `auth.personal_token_max_ttl` (personal) ahead.
    ApiTokenCreated:
      type: object
      required: [token, secret]
      properties:
        token:
          $ref: '#/components/schemas/ApiToken'
        secret:
          type: string
          description: Full token value `bothy_<kind>_<random>_<checksum>` (prefix for secret scanners). Shown exactly once.
          examples: [bothy_agt_EXAMPLEOnlyNotARealTokenForTheChecksumTests_3NymP3]
    AppScope:
      type: string
      enum:
      - messages:read
      - messages:write
      - files:read
      - files:write
      - commands:write
      - components:write
      - users:read
      - events:subscribe
      - costs:reconcile
    Group:
      type: object
      required: [id, name, description, source, member_count, created_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Support]
        description:
          type: string
          examples: [Support team]
        source:
          $ref: '#/components/schemas/GroupSource'
        provider_id:
          $ref: '#/components/schemas/Uuid'
          description: OIDC provider whose group claim manages the members; set exactly for `source = oidc_claim`.
        external_ref:
          type: string
          description: Claim value of the provider; set exactly for `source = oidc_claim`.
        member_count:
          type: integer
        created_at:
          $ref: '#/components/schemas/Timestamp'
    GroupCreate:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name:
          type: string
          examples: [Support]
        description:
          type: string
        source:
          $ref: '#/components/schemas/GroupSource'
        provider_id:
          $ref: '#/components/schemas/Uuid'
          description: Required with `source = oidc_claim`; the OIDC provider whose claim manages the members.
        external_ref:
          type: string
          description: Required with `source = oidc_claim`; the claim value.
    GroupUpdate:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          examples: [Support]
        description:
          type: string
    GroupMember:
      type: object
      required: [user, added_at]
      properties:
        user:
          $ref: '#/components/schemas/PrincipalRef'
        added_by:
          $ref: '#/components/schemas/Uuid'
        added_at:
          $ref: '#/components/schemas/Timestamp'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    GroupMemberAdd:
      type: object
      additionalProperties: false
      required: [user_id]
      properties:
        user_id:
          $ref: '#/components/schemas/Uuid'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
        resolve_i1:
          $ref: '#/components/schemas/ResolveI1'
    TargetKind:
      type: string
      description: '`host` is a connected host for the terminal; it has no level and only the capability `terminal` (24, 8.1).'
      enum: [instance, space, channel, host]
    GrantSubjectKind:
      type: string
      enum: [user, group, agent, app]
    Grant:
      type: object
      required:
      - id
      - subject_kind
      - subject_id
      - target_kind
      - capabilities
      - reason
      - granted_by
      - granted_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        subject_kind:
          $ref: '#/components/schemas/GrantSubjectKind'
        subject_id:
          $ref: '#/components/schemas/Uuid'
          description: Principal ID for `user`, `agent`, `app`; group ID for `group`.
        target_kind:
          $ref: '#/components/schemas/TargetKind'
        target_id:
          $ref: '#/components/schemas/Uuid'
          description: Absent for `instance`.
        level:
          $ref: '#/components/schemas/Level'
          description: Absent exactly for `target_kind = host`.
        capabilities:
          type: array
          items:
            $ref: '#/components/schemas/Capability'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
        reason:
          type: string
          examples: [Support team works in this space]
        granted_by:
          $ref: '#/components/schemas/Uuid'
        granted_at:
          $ref: '#/components/schemas/Timestamp'
        revoked_at:
          $ref: '#/components/schemas/Timestamp'
        revoked_by:
          $ref: '#/components/schemas/Uuid'
        revoke_reason:
          type: string
    GrantCreate:
      type: object
      additionalProperties: false
      description: '`level` is required for every target except `host`, where it must be absent and `capabilities` must be exactly `[terminal]` (07, 2.12).'
      required: [subject_kind, subject_id, target_kind, reason]
      properties:
        subject_kind:
          $ref: '#/components/schemas/GrantSubjectKind'
        subject_id:
          $ref: '#/components/schemas/Uuid'
        target_kind:
          $ref: '#/components/schemas/TargetKind'
        target_id:
          $ref: '#/components/schemas/Uuid'
        level:
          $ref: '#/components/schemas/Level'
        capabilities:
          type: array
          items:
            $ref: '#/components/schemas/Capability'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
        reason:
          type: string
          examples: [Support team works in this space]
        resolve_i1:
          $ref: '#/components/schemas/ResolveI1'
    GrantPreview:
      type: object
      description: Effect of a grant before it is created. No state changes (07, 9.3).
      required: [allowed, effects, invariant_violations, hints, text, text_locale]
      properties:
        allowed:
          type: boolean
          description: False if a delegation limit of the caller (`denial_reason`) or an invariant would reject the grant.
        denial_reason:
          type: string
          description: Reason code of 07 section 4.1 when a delegation limit rejects the grant, for example `delegation_exceeds_own_level`.
          examples: [delegation_exceeds_own_level]
        effects:
          type: array
          items:
            $ref: '#/components/schemas/AccessEffect'
        invariant_violations:
          type: array
          items:
            $ref: '#/components/schemas/InvariantViolation'
        hints:
          type: array
          items:
            $ref: '#/components/schemas/PreviewHint'
        text:
          type: string
          description: Plain text from templates in `text_locale`, lines separated by a line feed, no trailing line feed.
        text_locale:
          type: string
          enum: [de, en]
    PreviewHint:
      type: object
      description: '`terminal_not_effective` (grant on a host, 07, 2.12 point 4) says that SR10 or SR12 keep the grant from taking effect: with `principal_id` for a user subject, with `group_id` and `count` (members in effect it does not reach, absent when 0) for a group subject.'
      required: [code]
      properties:
        code:
          type: string
          enum: [read_horizon, trigger_blocked_tool_class, group_reach, expiry_missing, terminal_not_effective]
        agent_id:
          $ref: '#/components/schemas/Uuid'
        principal_id:
          $ref: '#/components/schemas/Uuid'
        group_id:
          $ref: '#/components/schemas/Uuid'
        space_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        count:
          type: integer
          format: int64
    AccessEffect:
      type: object
      required:
      - principal
      - target_kind
      - before_level
      - after_level
      - capabilities_added
      properties:
        principal:
          $ref: '#/components/schemas/PrincipalRef'
        target_kind:
          $ref: '#/components/schemas/TargetKind'
        target_id:
          $ref: '#/components/schemas/Uuid'
        before_level:
          $ref: '#/components/schemas/LevelOrNone'
        after_level:
          $ref: '#/components/schemas/LevelOrNone'
        capabilities_added:
          type: array
          items:
            $ref: '#/components/schemas/Capability'
    InvariantViolation:
      type: object
      description: '`i1_violation` names `agent_id`; `i6_violation` names `host_id`.'
      required: [code, principal_ids]
      properties:
        code:
          type: string
          examples: [i1_violation]
        agent_id:
          $ref: '#/components/schemas/Uuid'
        host_id:
          $ref: '#/components/schemas/Uuid'
        credential_holder_id:
          $ref: '#/components/schemas/Uuid'
          description: Credential holder of the agent (`i1_violation`) or of the personal LLM login of the host (`i6_violation`); absent if the agent has none.
        principal_ids:
          type: array
          description: With `i1_violation` the principals that would gain `trigger` next to the credential holder; with `i6_violation` the people other than the holder that a valid grant on the host names, directly or as members in effect of a group grant (empty for a group without members).
          items:
            $ref: '#/components/schemas/Uuid'
        organization_ready:
          type: boolean
          description: True when the agent reports `organization` among its `billing_modes_supported`.
        hint:
          type: string
          examples: [Switch the agent to organization first]
    AccessExplanation:
      type: object
      required: [target_kind, target_id, entries]
      properties:
        target_kind:
          $ref: '#/components/schemas/AccessTargetKind'
        target_id:
          $ref: '#/components/schemas/Uuid'
        entries:
          type: array
          items:
            $ref: '#/components/schemas/AccessEntry'
    AccessEntry:
      type: object
      required: [principal, level, capabilities, sources]
      properties:
        principal:
          $ref: '#/components/schemas/PrincipalRef'
        level:
          $ref: '#/components/schemas/LevelOrNone'
          description: '`none` only for entries whose sources are all `via_agent`.'
        inactive:
          type: boolean
          description: True when the principal is not `active`; the level is what its grants would give (07, 9.1).
        capabilities:
          type: array
          items:
            $ref: '#/components/schemas/Capability'
        sources:
          type: array
          items:
            $ref: '#/components/schemas/AccessSource'
    AccessSource:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [owner, direct, group, inherited, dm_participant, operator, via_agent, guard_blocked]
        grant_id:
          $ref: '#/components/schemas/Uuid'
        group_id:
          $ref: '#/components/schemas/Uuid'
        from_target_kind:
          $ref: '#/components/schemas/TargetKind'
        from_target_id:
          $ref: '#/components/schemas/Uuid'
        agent_id:
          $ref: '#/components/schemas/Uuid'
          description: Agent of an `operator`, `via_agent` or `guard_blocked` source.
        rule:
          type: string
          enum: [sr2, sr3]
          description: Guard rule of a `guard_blocked` source.
        expires_at:
          $ref: '#/components/schemas/Timestamp'
          description: Expiry of the grant.
        membership_expires_at:
          $ref: '#/components/schemas/Timestamp'
        reason:
          type: string
          description: Reason of the grant.
    AccessTarget:
      type: object
      required: [kind, id]
      properties:
        kind:
          $ref: '#/components/schemas/AccessTargetKind'
        id:
          $ref: '#/components/schemas/Uuid'
        level:
          $ref: '#/components/schemas/Level'
        previous_level:
          $ref: '#/components/schemas/Level'
        capabilities:
          type: array
          items:
            $ref: '#/components/schemas/Capability'
    Space:
      type: object
      required: [id, name, sort_key, version, created_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Support]
        icon:
          type: string
          description: Glyph name from the list `space-icons` of the design tokens (`kernel/designkeys`). No emoji.
          examples: [headset]
        color:
          type: string
          description: Key of a category color of the design system, never a hex value.
          examples: [moss]
        sort_key:
          type: string
          description: Fractional index, compared bytewise.
          examples: [a0]
        default_operator_agent_id:
          $ref: '#/components/schemas/Uuid'
        archived_at:
          $ref: '#/components/schemas/Timestamp'
        version:
          type: integer
          format: int64
        created_at:
          $ref: '#/components/schemas/Timestamp'
    SpaceCreate:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          description: At most `space.name_max_chars` characters.
          examples: [Research]
        icon:
          type: string
          description: Glyph name from the list `space-icons` of the design tokens.
          examples: [flask]
        color:
          type: string
          description: Key of a category color of the design system, never a hex value.
          examples: [heather]
        after_space_id:
          $ref: '#/components/schemas/Uuid'
          description: Insert after this space; absent inserts at the end.
    SpaceUpdate:
      type: object
      additionalProperties: false
      description: Changes only the fields sent; `icon` and `color` cannot be cleared.
      properties:
        name:
          type: string
          minLength: 1
          description: At most `space.name_max_chars` characters.
          examples: [Research]
        icon:
          type: string
          description: Glyph name from the list `space-icons` of the design tokens.
        color:
          type: string
          description: Key of a category colour of the design system, never a hex value.
    MovePosition:
      type: object
      additionalProperties: false
      description: New position relative to siblings. Exactly one of both fields; none moves to the start.
      properties:
        after_id:
          $ref: '#/components/schemas/Uuid'
        before_id:
          $ref: '#/components/schemas/Uuid'
    OperatorSet:
      type: object
      additionalProperties: false
      description: Operator assignment. `inherit` is only valid for channels.
      required: [mode, reason]
      properties:
        mode:
          $ref: '#/components/schemas/OperatorMode'
        agent_id:
          $ref: '#/components/schemas/Uuid'
          description: Required for mode `agent`.
        reason:
          type: string
          examples: [Support agent takes over]
        resolve_i1:
          $ref: '#/components/schemas/ResolveI1'
    SpaceView:
      type: object
      required: [space, level, capabilities]
      properties:
        space:
          $ref: '#/components/schemas/Space'
        level:
          $ref: '#/components/schemas/Level'
          description: Effective level on the space. A space visible only through one of its channels reports `read` and no capabilities.
        capabilities:
          type: array
          items:
            $ref: '#/components/schemas/Capability'
    ChannelKind:
      type: string
      enum: [text, forum, dm]
    ChannelOperator:
      type: object
      required: [mode]
      properties:
        mode:
          $ref: '#/components/schemas/OperatorMode'
        agent_id:
          $ref: '#/components/schemas/Uuid'
        effective_agent_id:
          $ref: '#/components/schemas/Uuid'
          description: Operator after resolving inheritance; absent means no operator.
    Channel:
      type: object
      required:
      - id
      - kind
      - name
      - topic
      - sort_key
      - operator
      - version
      - settings_version
      - created_at
      - read_only
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        space_id:
          $ref: '#/components/schemas/Uuid'
          description: Absent for `dm`.
        kind:
          $ref: '#/components/schemas/ChannelKind'
        name:
          type: string
          examples: [tickets]
        topic:
          type: string
          examples: [Customer tickets, one thread per ticket]
        sort_key:
          type: string
          examples: [a1]
        operator:
          $ref: '#/components/schemas/ChannelOperator'
        forum_default_sort:
          $ref: '#/components/schemas/ForumSort'
        default_notification_level:
          $ref: '#/components/schemas/NotificationLevel'
          description: Default for people without an own setting (13, 3.1); absent means none.
        read_only:
          type: boolean
          description: Write-protected for every principal including owners; only a running import writes (05, rule 1.8).
        main_thread_id:
          $ref: '#/components/schemas/Uuid'
          description: Thread with `kind = main` of text and DM channels.
        dm_participant_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        archived_at:
          $ref: '#/components/schemas/Timestamp'
        version:
          type: integer
          format: int64
        settings_version:
          type: integer
          format: int64
        created_at:
          $ref: '#/components/schemas/Timestamp'
    ChannelCreate:
      type: object
      additionalProperties: false
      required: [kind, name]
      properties:
        kind:
          type: string
          enum: [text, forum]
        name:
          type: string
          minLength: 1
          examples: [tickets]
        topic:
          type: string
          description: At most `channel.topic_max_chars` characters, no control or bidi characters.
        forum_default_sort:
          $ref: '#/components/schemas/ForumSort'
        after_channel_id:
          $ref: '#/components/schemas/Uuid'
    ChannelUpdate:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          minLength: 1
          examples: [tickets]
        topic:
          type: string
          description: At most `channel.topic_max_chars` characters, no control or bidi characters.
        forum_default_sort:
          $ref: '#/components/schemas/ForumSort'
        default_notification_level:
          $ref: '#/components/schemas/NotificationLevel'
        clear_default_notification_level:
          type: boolean
          description: Removes the channel default; there is no JSON null (R-API-03).
    ChannelReadOnlySet:
      type: object
      additionalProperties: false
      required: [read_only, reason]
      properties:
        read_only:
          type: boolean
        reason:
          type: string
          maxLength: 500
          examples: [Channel is mirrored from the old system]
    ChannelView:
      type: object
      required: [channel, level, capabilities]
      properties:
        channel:
          $ref: '#/components/schemas/Channel'
        level:
          $ref: '#/components/schemas/Level'
        capabilities:
          type: array
          items:
            $ref: '#/components/schemas/Capability'
    ChannelBehavior:
      type: object
      description: Behavior of the operator in a channel. Readable only with `manage`; never in events.
      required:
      - require_mention
      - thread_require_mention
      - free_response
      - auto_thread
      - allow_mentioned_agents
      - history_backfill_limit
      - prompt
      - skills
      - settings_version
      properties:
        require_mention:
          type: boolean
        thread_require_mention:
          type: boolean
        free_response:
          type: boolean
        auto_thread:
          type: boolean
        allow_mentioned_agents:
          type: boolean
        history_backfill_limit:
          type: integer
          examples: [20]
        prompt:
          type: string
          examples: [Answer support tickets politely. Ask before closing a ticket.]
        skills:
          type: array
          description: Ordered skill references the adapter understands; the server does not interpret them.
          items:
            type: string
            examples: [support-triage]
        project_binding:
          $ref: '#/components/schemas/ProjectBinding'
        settings_version:
          type: integer
          format: int64
    ChannelBehaviorPut:
      type: object
      additionalProperties: false
      required:
      - require_mention
      - thread_require_mention
      - free_response
      - auto_thread
      - allow_mentioned_agents
      - history_backfill_limit
      - prompt
      - skills
      properties:
        require_mention:
          type: boolean
        thread_require_mention:
          type: boolean
        free_response:
          type: boolean
        auto_thread:
          type: boolean
        allow_mentioned_agents:
          type: boolean
        history_backfill_limit:
          type: integer
          minimum: 0
          examples: [20]
        prompt:
          type: string
          examples: [Answer support tickets politely.]
        skills:
          type: array
          items:
            type: string
            examples: [support-triage]
    ProjectBinding:
      type: object
      required: [workspace_ref, runner_config, ready, version]
      properties:
        workspace_ref: {type: string, description: Opaque reference the runner resolves; never a server path., examples: [website]}
        repository_url: {type: string, description: Display only; the server never fetches this URL., examples: ['https://git.example.com/team/website']}
        default_branch: {type: string, examples: [main]}
        runner_config: {$ref: '#/components/schemas/ProjectRunnerConfig'}
        ready: {type: boolean}
        version: {type: integer, format: int64}
    ProjectBindingPut:
      type: object
      additionalProperties: false
      required: [workspace_ref, reason]
      properties:
        workspace_ref: {type: string, examples: [website]}
        repository_url: {type: string, examples: ['https://git.example.com/team/website']}
        default_branch: {type: string, examples: [main]}
        runner_config: {$ref: '#/components/schemas/ProjectRunnerConfig'}
        reason: {type: string, minLength: 1, examples: [Website project moves to this forum]}
    ProjectRunnerConfig:
      type: object
      additionalProperties: false
      description: Schema `project_runner_config.v1`. No secrets.
      properties:
        model: {type: string, minLength: 1}
        allowed_tools: {type: array, uniqueItems: true, items: {type: string, minLength: 1}}
    AgentChannelConfig:
      type: object
      description: Configuration an operator agent loads for a channel it serves.
      required: [channel_id, behavior]
      properties:
        channel_id:
          $ref: '#/components/schemas/Uuid'
        space_id:
          $ref: '#/components/schemas/Uuid'
        behavior:
          $ref: '#/components/schemas/ChannelBehavior'
    DmCreate:
      type: object
      additionalProperties: false
      required: [participant_ids]
      properties:
        participant_ids:
          type: array
          description: Other people in the conversation; the caller is added.
          minItems: 1
          items:
            $ref: '#/components/schemas/Uuid'
    SidebarState:
      type: object
      required: [space_id, collapsed, version]
      properties:
        space_id:
          $ref: '#/components/schemas/Uuid'
        collapsed:
          type: boolean
        sort_key:
          type: string
          description: Personal order; absent uses the global order.
          examples: [a0V]
        version:
          type: integer
          format: int64
        updated_by_device_id:
          $ref: '#/components/schemas/Uuid'
    SidebarStatePut:
      type: object
      additionalProperties: false
      required: [collapsed]
      properties:
        collapsed:
          type: boolean
        sort_key:
          type: string
        use_global_order:
          type: boolean
          description: True removes the personal order. Replaces sending null.
    SidebarStateList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/SidebarState'
    ForumTag:
      type: object
      required: [id, channel_id, name, sort_key, restricted]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [bug]
        color:
          type: string
          description: Key of a category color of the design system, never a hex value.
          examples: [rowan]
        sort_key:
          type: string
          examples: [a0]
        restricted:
          type: boolean
    ForumTagCreate:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name:
          type: string
          examples: [bug]
        color:
          type: string
          description: Key of a category color of the design system, never a hex value.
          examples: [rowan]
        restricted:
          type: boolean
        after_tag_id:
          $ref: '#/components/schemas/Uuid'
    ForumTagUpdate:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
          examples: [bug]
        color:
          type: string
          description: Key of a category color of the design system, never a hex value.
        clear_color:
          type: boolean
          description: Removes the color; not together with `color`. There is no JSON null (R-API-03).
        restricted:
          type: boolean
    ForumTagList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ForumTag'
    Thread:
      type: object
      required:
      - id
      - channel_id
      - kind
      - title
      - title_source
      - state
      - last_message_seq
      - last_activity_at
      - message_count
      - created_by
      - created_at
      - version
      - tag_ids
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        kind:
          type: string
          enum: [main, thread]
        parent_message_id:
          $ref: '#/components/schemas/Uuid'
        title:
          type: string
          examples: [Invoice export fails]
        title_source:
          type: string
          enum: [auto, manual]
        state:
          $ref: '#/components/schemas/ThreadState'
        closed_at:
          $ref: '#/components/schemas/Timestamp'
        closed_by:
          $ref: '#/components/schemas/Uuid'
        operator_agent_id:
          $ref: '#/components/schemas/Uuid'
          description: Fixed operator of a thread of kind `thread` (R-DM-12). Absent for the main thread (`kind = main`, R-DM-21) and for threads without operator; clients then use `Channel.operator.effective_agent_id`.
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
          description: Current policy of `operator_agent_id`, for the header badge; absent exactly when `operator_agent_id` is absent. Changes arrive with `agent.updated`.
        last_message_seq:
          type: integer
          format: int64
        unread_floor_seq:
          type: integer
          format: int64
          description: Messages up to this `thread_seq` never count as unread (import); absent means 0.
        last_activity_at:
          $ref: '#/components/schemas/Timestamp'
        message_count:
          type: integer
        created_by:
          $ref: '#/components/schemas/Uuid'
        created_at:
          $ref: '#/components/schemas/Timestamp'
        moved_at:
          $ref: '#/components/schemas/Timestamp'
        version:
          type: integer
          format: int64
        tag_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
    ThreadCreate:
      type: object
      additionalProperties: false
      description: |
        Creates a forum post (with `title` and `first_message`) or a thread from a message of the
        main thread (with `parent_message_id`, optional `first_message`). `first_message.run_id` is
        never valid, because a new thread has no run: `400 validation_failed` with pointer
        `/first_message/run_id` and `errors[].code` `run_mismatch`.
      properties:
        title:
          type: string
          examples: [Invoice export fails]
        parent_message_id:
          $ref: '#/components/schemas/Uuid'
        tag_ids:
          type: array
          description: Forum posts only; at most `thread.max_tags`. Restricted tags need `manage`, as with `setThreadTags`.
          items:
            $ref: '#/components/schemas/Uuid'
        first_message:
          $ref: '#/components/schemas/MessageCreate'
    ThreadCreated:
      type: object
      required: [thread]
      properties:
        thread:
          $ref: '#/components/schemas/Thread'
        first_message:
          $ref: '#/components/schemas/Message'
    ThreadTitlePut:
      type: object
      additionalProperties: false
      required: [title]
      properties:
        title:
          type: string
          minLength: 1
          examples: [Invoice export fails for March]
    ThreadMove:
      type: object
      additionalProperties: false
      required: [target_channel_id, reason]
      properties:
        target_channel_id:
          $ref: '#/components/schemas/Uuid'
        reason:
          type: string
          examples: [Belongs to billing]
    ThreadTagsPut:
      type: object
      additionalProperties: false
      required: [tag_ids]
      properties:
        tag_ids:
          type: array
          description: Complete set, at most `thread.max_tags`, without duplicates.
          items:
            $ref: '#/components/schemas/Uuid'
    AgentSession:
      type: object
      required:
      - id
      - thread_id
      - agent_id
      - is_current
      - started
      - cost
      - created_at
      - version
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        agent_id:
          $ref: '#/components/schemas/Uuid'
        external_session_ref:
          type: string
          examples: [sess-7f3a]
        is_current:
          type: boolean
        started:
          type: boolean
        model:
          type: string
          examples: [model-large]
        cost:
          $ref: '#/components/schemas/Money'
        created_at:
          $ref: '#/components/schemas/Timestamp'
        ended_at:
          $ref: '#/components/schemas/Timestamp'
        end_reason:
          type: string
          enum: [reset, rebind, import, agent_changed]
        cost_baseline_micros:
          type: integer
          format: int64
          readOnly: true
          description: 'The cumulative amount of the external session before it was bound to this row (`agent_sessions.cost_baseline_micros`, 22 9 step 7), in micro units of `currency`; present only when above 0. An adapter whose runtime reports cumulative session values books per run only the part above it (09, 18.7; RK-75).'
          examples: [1250000]
        currency:
          type: string
          readOnly: true
          description: ISO 4217 code of `cost_baseline_micros` (`agent_sessions.currency`); present exactly when `cost_baseline_micros` is.
          examples: [USD]
        version:
          type: integer
          format: int64
    AgentSessionList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/AgentSession'
    AgentSessionBind:
      type: object
      additionalProperties: false
      required: [external_session_ref]
      properties:
        external_session_ref:
          type: string
          examples: [sess-7f3a]
        expected_current_session_id:
          $ref: '#/components/schemas/Uuid'
          description: Current session of the thread as the client saw it; absent when the thread has none. A mismatch answers 409 `conflict` with `reason` `session_changed` (09, 9.4).
        take_over:
          type: boolean
          default: false
          description: Required when the session is current in another thread; otherwise 409 `conflict` with `reason` `session_bound_elsewhere`.
    AgentSessionReport:
      type: object
      additionalProperties: false
      required: [external_session_ref]
      properties:
        external_session_ref:
          type: string
          examples: [sess-7f3a]
          description: The reference the runtime actually uses; a different one replaces the stored reference of the session (09, 9.5 item 3). At most `agent.session_ref_max_bytes` bytes of display text.
        model:
          type: string
          examples: [model-large]
          description: The pattern of `default_model` (09, 2.2), at most `agent.session_ref_max_bytes` bytes.
        started:
          type: boolean
          description: '`true` once the runtime confirmed the external session (09, 9.1); `started` only moves forward, `false` never resets it.'
        bind_failed:
          type: string
          enum: [session_missing]
          description: The bound session no longer exists in the runtime and the adapter started a new one (09, 9.5 item 4); only for a started session, otherwise 409 `conflict` with `reason` `session_not_bound`.
    MessageKind:
      type: string
      enum: [text, voice, run, card, system]
    MessageOrigin:
      type: string
      enum: [human, agent, app, import, system]
    TriggerState:
      type: string
      enum:
      - received
      - queued
      - working
      - done
      - failed
      - cancelled
      - followup_delivered
    Message:
      type: object
      required:
      - id
      - thread_id
      - channel_id
      - thread_seq
      - author
      - client_msg_id
      - origin
      - kind
      - content_md
      - content_format
      - silent
      - attachments
      - mention_ids
      - reactions
      - pinned
      - components
      - version
      - created_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        thread_seq:
          type: integer
          format: int64
          examples: [42]
        author:
          $ref: '#/components/schemas/PrincipalRef'
        client_msg_id:
          $ref: '#/components/schemas/Uuid'
        origin:
          $ref: '#/components/schemas/MessageOrigin'
        kind:
          $ref: '#/components/schemas/MessageKind'
        system_code:
          type: string
          enum: [thread.reopened, agent_session.resume_failed]
          description: Set exactly for `kind = system`; clients render the localized line from it and `author`. `content_md` is empty then. `agent_session.resume_failed` is written once when the runtime no longer knew the bound session and the agent started a new one (09 9.5, 03 R-DM-42); its `author` is the run's agent. A system line never reopens a done thread.
        content_md:
          type: string
          description: Markdown. In lists longer contents are cut at `message.list_content_max_bytes` with `content_truncated`.
          examples: ['@support-agent the invoice export fails since this morning.']
        content_format:
          type: integer
          description: Version of the Markdown dialect; `1` is CommonMark 0.31.2 without raw HTML, with mention tokens (03 `messages`).
          examples: [1]
        content_truncated:
          type: boolean
          description: Set when `content_md` was cut (events above `sync.event_payload_max_bytes`, lists above `message.list_content_max_bytes`); `getMessage` returns the full content.
        reply_to_message_id:
          $ref: '#/components/schemas/Uuid'
        run_id:
          $ref: '#/components/schemas/Uuid'
          description: 'Set only for `kind = run`: the run whose live box this anchor holds.'
        trigger_state:
          $ref: '#/components/schemas/TriggerState'
        trigger_state_at:
          $ref: '#/components/schemas/Timestamp'
        trigger_state_reason:
          type: string
          description: Reason when `trigger_state` is `failed` or `cancelled`, e.g. `billing_mode_personal_not_holder`, `run_active_other_agent`, `user_cancelled`.
        silent:
          type: boolean
        attachments:
          type: array
          description: Attachments in position order (the order of `attachment_file_ids` at sending); empty for a message without attachments and for a deleted message.
          items:
            $ref: '#/components/schemas/Attachment'
        mention_ids:
          type: array
          description: Principals resolved from the mention tokens of `content_md`, sorted. Clients render a token as a mention only for these.
          items:
            $ref: '#/components/schemas/Uuid'
        reactions:
          type: array
          items:
            $ref: '#/components/schemas/ReactionSummary'
        my_reactions:
          type: array
          description: Emoji the caller reacted with. Only in REST responses, never in events.
          items:
            type: string
        pinned:
          type: boolean
        components:
          type: array
          items:
            $ref: '#/components/schemas/Component'
        version:
          type: integer
          format: int64
        edited_at:
          $ref: '#/components/schemas/Timestamp'
        deleted_at:
          $ref: '#/components/schemas/Timestamp'
        created_at:
          $ref: '#/components/schemas/Timestamp'
    MessageCreate:
      type: object
      additionalProperties: false
      required: [client_msg_id]
      properties:
        client_msg_id:
          $ref: '#/components/schemas/Uuid'
          description: UUIDv7 created by the client before the first attempt; unique per author forever.
        kind:
          type: string
          enum: [text, voice, card]
          description: '`voice` is a voice message (12, 8.1): exactly one entry in `attachment_file_ids` (none gives `400 validation_failed` with `errors[].code` `required`, more than one `limit_reached`, pointer `/attachment_file_ids`), an MP4 audio file (`audio/mp4`, `.m4a`) for every author and also MP3 (`audio/mpeg`) for agents, and the file must be ready. Another format, an MP4 with a video track or without a duration in `mvhd` give `400 validation_failed` with `errors[].code` `voice_format_unsupported`, a duration above `voice.max_duration` `voice_too_long`, both with the pointer of the entry (`/attachment_file_ids/0`); a file whose upload or scan has not ended gives `409 file_not_ready`, a blocked or failed file `409 file_blocked`. `content_md` may be empty.'
        content_md:
          type: string
          description: Empty or white space only content is allowed only when `attachment_file_ids` lists at least one file (a picture without caption); otherwise `400 validation_failed` with `errors[].code` `empty`.
          examples: ['@support-agent the invoice export fails since this morning.']
        reply_to_message_id:
          $ref: '#/components/schemas/Uuid'
        attachment_file_ids:
          type: array
          description: Own files uploaded for the channel of this message with a completed upload session, in the order they appear in the message; at most `message.max_attachments`. Needs the right `upload` in addition to `write` (`403 forbidden` with `reason` `capability_missing`). A file that is not attachable (not the caller's, another channel, upload not completed or failed, deleted, already attached, or listed twice) answers `409 conflict` with `reason` `file_not_attachable` and the pointer of the entry in `detail`; more than `message.max_attachments` answer `400 validation_failed` with `errors[].code` `limit_reached`. Files whose scan has not ended are attachable.
          items:
            $ref: '#/components/schemas/Uuid'
        mention_ids:
          type: array
          description: Optional check. The server resolves mentions from `content_md` (03 `messages`, content format; 07 section 4.6); when present, the set must equal the resolved set, else `400 validation_failed` with code `mention_mismatch`.
          items:
            $ref: '#/components/schemas/Uuid'
        silent:
          type: boolean
        clear_draft:
          type: boolean
        run_id:
          $ref: '#/components/schemas/Uuid'
          description: 'Agents only; the run this message belongs to. It must name an active run (`queued`, `running`, `waiting_input`) of the calling agent in this thread. Never stored on the message (`Message.run_id` stays absent). A person or app, an unknown run, a run of another thread or of another agent answer alike `400 validation_failed` with `errors[].code` `run_mismatch`; an ended own run answers `409 conflict` with `reason` `run_not_active`. With `kind = card` it also binds the components of the card to this run (`Component.run_id`, 09 11.4) and is part of the canonical request of `client_msg_id` (`bothy-msg-v2`); for every other kind it is not (09 4.7).'
        components:
          type: array
          description: Requires `kind = card`, and `kind = card` requires at least one component; at most `component.max_per_message`. Agents and apps send cards (people get `invalid`). An app needs `write` and `messages:write` as for every message; without `components:write` its components stay inactive, and a use answers `409 component_used`. The whole `message.created` payload of the card must fit `sync.event_payload_max_bytes`, else `too_long` on `/components`. An agent whose owner switched `supports_components` off gets `403 feature_disabled` (09, 2.2 and 11.1); the switch does not apply to apps.
          items:
            $ref: '#/components/schemas/ComponentCreate'
    MessageUpdate:
      type: object
      additionalProperties: false
      description: The server resolves the mentions of the resulting content again; `mention_ids` is an optional check as in `MessageCreate`.
      properties:
        content_md:
          type: string
          examples: ['@support-agent the invoice export fails since 08:00.']
        mention_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
    MessagePage:
      type: object
      description: Messages in ascending `thread_seq`. Tombstones are included.
      required: [items, has_more_before, has_more_after]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Message'
        has_more_before:
          type: boolean
        has_more_after:
          type: boolean
    MessageRevision:
      type: object
      required: [revision, content_md, edited_by, created_at]
      properties:
        revision:
          type: integer
          examples: [1]
        content_md:
          type: string
          examples: ['@support-agent the invoice export fails.']
        edited_by:
          $ref: '#/components/schemas/Uuid'
        created_at:
          $ref: '#/components/schemas/Timestamp'
    MessageRevisionList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/MessageRevision'
    ReactionSummary:
      type: object
      required: [emoji, count]
      properties:
        emoji:
          type: string
          description: Exactly one Unicode grapheme cluster.
          examples: [👍]
        count:
          type: integer
          examples: [3]
    Reaction:
      type: object
      required: [emoji, principal, created_at]
      properties:
        emoji:
          type: string
          examples: [👍]
        principal:
          $ref: '#/components/schemas/PrincipalRef'
        created_at:
          $ref: '#/components/schemas/Timestamp'
    Pin:
      type: object
      required: [message_id, thread_id, pinned_by, pinned_at]
      properties:
        message_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        pinned_by:
          $ref: '#/components/schemas/Uuid'
        pinned_at:
          $ref: '#/components/schemas/Timestamp'
    MentionItem:
      type: object
      required: [message, created_at]
      properties:
        message:
          $ref: '#/components/schemas/Message'
        created_at:
          $ref: '#/components/schemas/Timestamp'
    Template:
      type: object
      required: [id, scope, name, content_md, version]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        scope:
          type: string
          enum: [personal, channel]
        channel_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Escalation]
        content_md:
          type: string
          examples: [Please escalate this ticket to second level.]
        version:
          type: integer
          format: int64
    TemplateCreate:
      type: object
      additionalProperties: false
      required: [name, content_md]
      properties:
        name:
          type: string
          examples: [Escalation]
        content_md:
          type: string
          examples: [Please escalate this ticket to second level.]
    TemplateUpdate:
      type: object
      additionalProperties: false
      properties:
        name:
          type: string
        content_md:
          type: string
    TemplateList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Template'
    Attachment:
      type: object
      required: [position, file]
      properties:
        position:
          type: integer
        file:
          $ref: '#/components/schemas/File'
    File:
      type: object
      required:
      - id
      - size_bytes
      - mime_type
      - original_name
      - uploaded_by
      - upload_state
      - scan_status
      - transcript_status
      - thumbnails
      - created_at
      - purpose
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        sha256:
          type: string
          description: Hex SHA-256 of the stored bytes.
          examples: [9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08]
        size_bytes:
          type: integer
          format: int64
          examples: [48213]
        mime_type:
          type: string
          description: Detected by the server.
          examples: [image/png]
        original_name:
          type: string
          examples: [screenshot.png]
        uploaded_by:
          $ref: '#/components/schemas/Uuid'
        purpose:
          type: string
          enum: [attachment, avatar, branding, gdpr_export]
        scope_channel_id:
          $ref: '#/components/schemas/Uuid'
          description: Channel the file was uploaded for; present only before the file is attached to a message. An attached file is read through the channel of its message.
        upload_state:
          type: string
          enum: [pending, complete, failed]
          examples: [complete]
        scan_status:
          type: string
          enum: [pending, clean, infected, error, skipped]
          examples: [clean]
        width:
          type: integer
        height:
          type: integer
        duration_ms:
          type: integer
          format: int64
        transcript_status:
          $ref: '#/components/schemas/TranscriptStatus'
        transcript_language:
          type: string
        transcript_source:
          $ref: '#/components/schemas/TranscriptSource'
        thumbnails:
          type: array
          items:
            $ref: '#/components/schemas/Thumbnail'
        created_at:
          $ref: '#/components/schemas/Timestamp'
    Thumbnail:
      type: object
      required: [variant, mime_type, width, height]
      properties:
        variant:
          type: string
          enum: [small, medium, large, poster]
        mime_type:
          type: string
          examples: [image/jpeg]
        width:
          type: integer
          examples: [320]
        height:
          type: integer
          examples: [180]
    UploadCreate:
      type: object
      additionalProperties: false
      required: [name, size_bytes, sha256]
      properties:
        name:
          type: string
          examples: [screenshot.png]
        size_bytes:
          type: integer
          format: int64
          minimum: 1
          examples: [48213]
        declared_mime_type:
          type: string
          examples: [image/png]
        sha256:
          type: string
          pattern: '^[0-9a-f]{64}$'
          description: Lowercase hex SHA-256 of the whole file; the verify pass checks it.
    UploadSession:
      type: object
      description: |
        Upload plan. In mode `direct` the part URLs point to this server; in mode `presigned` they
        point to the object store. The client uploads all parts, then calls complete.
      required: [file_id, mode, part_size_bytes, parts, expires_at]
      properties:
        file_id:
          $ref: '#/components/schemas/Uuid'
        mode:
          type: string
          enum: [direct, presigned]
        part_size_bytes:
          type: integer
          format: int64
        parts:
          type: array
          items:
            $ref: '#/components/schemas/UploadPart'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    UploadState:
      type: object
      description: State of an own upload session, for resuming after a network loss.
      required: [file_id, upload_state, part_size_bytes, completed_parts, expires_at]
      properties:
        file_id:
          $ref: '#/components/schemas/Uuid'
        upload_state:
          type: string
          enum: [pending, uploading, complete]
        part_size_bytes:
          type: integer
          format: int64
        completed_parts:
          type: array
          description: Parts the server has confirmed, with their ETag.
          items:
            type: object
            required: [part_number, etag]
            properties:
              part_number:
                type: integer
                minimum: 1
              etag:
                type: string
                examples: ['"9b2cf535f27731c974343645a3985328"']
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    FileTicketCreate:
      type: object
      additionalProperties: false
      required: [variant]
      properties:
        variant:
          type: string
          enum: [original, small, medium, large, poster]
    FileTicket:
      type: object
      description: Short-lived fetch URL for media players without DPoP (12, 9.2). Checked again on every fetch.
      required: [url, expires_at]
      properties:
        url:
          type: string
          format: uri-reference
          examples: ['/api/v1/files/0192f4a0-7b1c-75c7-88f6-199cc394b56a/content?ticket=c2lnbmVk']
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    UploadPart:
      type: object
      required: [part_number, url, headers]
      properties:
        part_number:
          type: integer
          minimum: 1
        url:
          type: string
          format: uri-reference
          examples: [/api/v1/uploads/0192f4a0-7b1c-7000-8a00-000000000501/parts/1]
        headers:
          type: array
          description: Headers the client must send with the PUT.
          items:
            $ref: '#/components/schemas/HeaderPair'
    HeaderPair:
      type: object
      required: [name, value]
      properties:
        name:
          type: string
          examples: [Content-Type]
        value:
          type: string
          examples: [application/octet-stream]
    UploadPartResult:
      type: object
      additionalProperties: false
      required: [part_number, etag]
      properties:
        part_number:
          type: integer
        etag:
          type: string
          examples: ['"3858f62230ac3c915f300c664312c11f"']
    UploadComplete:
      type: object
      additionalProperties: false
      required: [parts]
      properties:
        parts:
          type: array
          items:
            $ref: '#/components/schemas/UploadPartResult'
    TranscriptSubmit:
      type: object
      additionalProperties: false
      required: [text]
      properties:
        text:
          type: string
          description: The transcript; may be empty (a silent recording), must not contain U+0000 and is at most `message.max_content_bytes` UTF-8 bytes.
          examples: [Please call the customer back tomorrow morning.]
        language:
          type: string
          description: Optional BCP 47 tag of the spoken language, a primary subtag of two or three letters followed by subtags of two to eight letters or digits.
          pattern: ^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$
          examples: [en]
    Transcript:
      type: object
      required: [file_id, status]
      properties:
        file_id:
          $ref: '#/components/schemas/Uuid'
        status:
          $ref: '#/components/schemas/TranscriptStatus'
        text:
          type: string
          examples: [Please call the customer back tomorrow morning.]
        language:
          type: string
          examples: [en]
        source:
          $ref: '#/components/schemas/TranscriptSource'
    SearchHit:
      type: object
      description: One hit. `kind` names which of the optional objects is set.
      required: [kind, segments]
      properties:
        kind:
          $ref: '#/components/schemas/SearchHitKind'
        score:
          type: number
          format: double
          description: Only with `sort = relevance`.
        message:
          $ref: '#/components/schemas/Message'
        thread:
          $ref: '#/components/schemas/Thread'
        file:
          $ref: '#/components/schemas/File'
        thread_title:
          type: string
        channel_name:
          type: string
        space_name:
          type: string
        segments:
          type: array
          description: Excerpt split at the matches; no markup, HTML in the content stays text (14, 4.6).
          items:
            type: object
            required: [text, match]
            properties:
              text:
                type: string
              match:
                type: boolean
    ReadState:
      type: object
      required:
      - thread_id
      - channel_id
      - last_read_seq
      - mention_count
      - version
      properties:
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        last_read_seq:
          type: integer
          format: int64
          examples: [41]
        mention_count:
          type: integer
          description: Unread mentions of the caller in this thread, computed when the response or event is built (03 `read_states`); never stored.
        version:
          type: integer
          format: int64
        updated_by_device_id:
          $ref: '#/components/schemas/Uuid'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
    ReadStatePut:
      type: object
      additionalProperties: false
      description: Advances the read position. The server keeps the maximum of old and new.
      required: [last_read_seq]
      properties:
        last_read_seq:
          type: integer
          format: int64
          minimum: 0
          examples: [42]
    ReadStateList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ReadState'
    Draft:
      type: object
      required: [id, channel_id, content_md, file_ids, version]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        content_md:
          type: string
          examples: [Draft of a reply]
        reply_to_message_id:
          $ref: '#/components/schemas/Uuid'
        file_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        version:
          type: integer
          format: int64
        updated_by_device_id:
          $ref: '#/components/schemas/Uuid'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
    DraftPut:
      type: object
      additionalProperties: false
      required: [content_md]
      properties:
        content_md:
          type: string
          examples: [Draft of a reply]
        reply_to_message_id:
          $ref: '#/components/schemas/Uuid'
        file_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        base_version:
          type: integer
          format: int64
          description: Version the client edited; older than current sets `overwrote_newer`.
    DraftPutResult:
      type: object
      required: [draft, overwrote_newer]
      properties:
        draft:
          $ref: '#/components/schemas/Draft'
        overwrote_newer:
          type: boolean
    DraftList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Draft'
    UserSetting:
      type: object
      required: [key, value, version]
      properties:
        key:
          type: string
          examples: [composer.enter_sends]
        value:
          $ref: '#/components/schemas/JsonValue'
        version:
          type: integer
          format: int64
        updated_by_device_id:
          $ref: '#/components/schemas/Uuid'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
    UserSettingPut:
      type: object
      additionalProperties: false
      required: [value]
      properties:
        value:
          $ref: '#/components/schemas/JsonValue'
    UserSettingList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/UserSetting'
    NotificationLevel:
      type: string
      enum: [all, mentions, mute]
    NotificationPreference:
      type: object
      required: [target_kind, target_id, level, version]
      properties:
        target_kind:
          type: string
          enum: [space, channel, thread]
        target_id:
          $ref: '#/components/schemas/Uuid'
        level:
          $ref: '#/components/schemas/NotificationLevel'
        muted_until:
          $ref: '#/components/schemas/Timestamp'
        version:
          type: integer
          format: int64
    NotificationPreferencePut:
      type: object
      additionalProperties: false
      required: [level]
      properties:
        level:
          $ref: '#/components/schemas/NotificationLevel'
        muted_until:
          $ref: '#/components/schemas/Timestamp'
    NotificationPreferenceList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/NotificationPreference'
    QuietHoursRule:
      type: object
      required: [id, weekdays, start_local, end_local, enabled, version]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        weekdays:
          type: integer
          description: Bit mask, bit 0 = Monday.
          minimum: 1
          maximum: 127
          examples: [31]
        start_local:
          type: string
          description: Local time `HH:MM`.
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
          examples: ['22:00']
        end_local:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
          examples: ['07:00']
        time_zone:
          type: string
          description: IANA time zone name; absent means the person's time zone (users.time_zone).
          examples: [Europe/Berlin]
        enabled:
          type: boolean
        version:
          type: integer
          format: int64
    QuietHoursRuleInput:
      type: object
      additionalProperties: false
      required: [weekdays, start_local, end_local, enabled]
      properties:
        weekdays:
          type: integer
          minimum: 1
          maximum: 127
          examples: [31]
        start_local:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
          examples: ['22:00']
        end_local:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
          examples: ['07:00']
        time_zone:
          type: string
          description: IANA time zone name; absent means the person's time zone (users.time_zone).
          examples: [Europe/Berlin]
        enabled:
          type: boolean
    QuietHoursList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/QuietHoursRule'
    PushRegistration:
      type: object
      required: [id, device_id, provider, environment, app_id, created_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        device_id:
          $ref: '#/components/schemas/Uuid'
        provider:
          $ref: '#/components/schemas/PushProvider'
        environment:
          $ref: '#/components/schemas/PushEnvironment'
        app_id:
          type: string
          examples: [com.example.teamchat]
        created_at:
          $ref: '#/components/schemas/Timestamp'
        disabled_at:
          $ref: '#/components/schemas/Timestamp'
        disabled_reason:
          type: string
    PushRegistrationCreate:
      type: object
      additionalProperties: false
      required: [provider, token, app_id]
      properties:
        provider:
          $ref: '#/components/schemas/PushProvider'
        token:
          type: string
          description: Push token of the provider, at most `push.registration_field_max_bytes` bytes (`too_long` at `/token`). Never returned. An `apns` token is an even number of hexadecimal digits and is stored in lower case; an `fcm` token is printable ASCII without white space; otherwise `pattern_mismatch` at `/token`.
          examples: [EXAMPLE-device-token-a1b2]
        environment:
          $ref: '#/components/schemas/PushEnvironment'
        app_id:
          type: string
          description: Bundle ID or package of the app, at most `push.registration_field_max_bytes` bytes (`too_long` at `/app_id`). For `apns` it must be one of `push.apns.topics` (`invalid` at `/app_id`). `apns` needs a device with platform `ios` or `macos`, `fcm` one with `android` (`invalid` at `/provider`); `sandbox` exists only for `apns` (`invalid` at `/environment`); an absent `environment` is `production`.
          examples: [com.example.teamchat]
    PushRegistrationCreated:
      description: A new push registration with its preview token, which is shown exactly once and never again (13, 6.4).
      allOf:
      - $ref: '#/components/schemas/PushRegistration'
      - type: object
        required: [preview_token]
        properties:
          preview_token:
            type: string
            description: Preview token of the registration (`bothy_pvw_...`), shown exactly once. The app keeps it in the store it shares with its notification extension.
            examples: [bothy_pvw_EXAMPLEpushpreviewtoken00000000000000000000_4Z1L0A]
    UnreadThread:
      type: object
      required:
      - thread_id
      - channel_id
      - unread_count
      - mention_count
      - last_message_seq
      - last_read_seq
      properties:
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        unread_count:
          type: integer
        mention_count:
          type: integer
        last_message_seq:
          type: integer
          format: int64
        last_read_seq:
          type: integer
          format: int64
    DraftRef:
      type: object
      required: [id, channel_id, version]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        version:
          type: integer
          format: int64
    AgentPresence:
      type: object
      required: [agent_id, state]
      properties:
        agent_id:
          $ref: '#/components/schemas/Uuid'
        state:
          $ref: '#/components/schemas/AgentRuntimeState'
        reported_billing_mode:
          $ref: '#/components/schemas/BillingMode'
    Snapshot:
      type: object
      description: State without thread contents, delivered by `ready` and `GET /sync/snapshot`.
      required:
      - head_seq
      - permission_version
      - principal
      - is_owner
      - spaces
      - channels
      - sidebar
      - unread_threads
      - active_runs
      - agents
      - settings
      - drafts
      - features
      - sync_epoch
      properties:
        head_seq:
          type: integer
          format: int64
          examples: [5300]
        permission_version:
          type: integer
          format: int64
          examples: [418]
        sync_epoch:
          $ref: '#/components/schemas/Uuid'
          description: Time line of the event log (04, 7.5).
        principal:
          $ref: '#/components/schemas/PrincipalRef'
        is_owner:
          type: boolean
        spaces:
          type: array
          items:
            $ref: '#/components/schemas/SpaceView'
        channels:
          type: array
          items:
            $ref: '#/components/schemas/ChannelView'
        sidebar:
          type: array
          items:
            $ref: '#/components/schemas/SidebarState'
        unread_threads:
          type: array
          description: Only threads with unread messages or mentions.
          items:
            $ref: '#/components/schemas/UnreadThread'
        active_runs:
          type: array
          items:
            $ref: '#/components/schemas/RunSummary'
        agents:
          type: array
          items:
            $ref: '#/components/schemas/AgentPresence'
        settings:
          type: array
          items:
            $ref: '#/components/schemas/UserSetting'
        drafts:
          type: array
          items:
            $ref: '#/components/schemas/DraftRef'
        features:
          type: array
          description: Feature flags evaluated for the principal, as in `InstanceInfo.features` (04 section 6.3, step 4).
          items:
            $ref: '#/components/schemas/EvaluatedFlag'
    Agent:
      type: object
      required:
      - id
      - handle
      - display_name
      - status
      - adapter_kind
      - description
      - billing_mode
      - presence
      - operator_config
      - version
      - tool_class
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        handle:
          type: string
          examples: [support-agent]
        display_name:
          type: string
          examples: [Support Agent]
        avatar_file_id:
          $ref: '#/components/schemas/Uuid'
        status:
          type: string
          enum: [active, disabled, deleted]
        adapter_kind:
          type: string
          examples: [hermes]
        description:
          type: string
          examples: [Answers support tickets]
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
        credential_holder_id:
          $ref: '#/components/schemas/Uuid'
        billing_mode_changed_at:
          $ref: '#/components/schemas/Timestamp'
        tool_class:
          $ref: '#/components/schemas/ToolClass'
        presence:
          $ref: '#/components/schemas/AgentRuntimeStatus'
        operator_config:
          $ref: '#/components/schemas/JsonObject'
          description: Schema `agent_operator_config.v1`. No secrets.
        version:
          type: integer
          format: int64
    AgentRuntimeStatus:
      type: object
      description: Reported state of the agent runtime (heartbeat), not the configured state.
      required: [state]
      properties:
        state:
          $ref: '#/components/schemas/AgentRuntimeState'
        reported_billing_mode:
          $ref: '#/components/schemas/BillingMode'
        runtime_version:
          type: string
          examples: [2.3.1]
        last_heartbeat_at:
          $ref: '#/components/schemas/Timestamp'
        billing_modes_supported:
          type: array
          items:
            $ref: '#/components/schemas/BillingMode'
        reported_tool_class:
          $ref: '#/components/schemas/ToolClass'
        billing_mode_matches:
          type: boolean
          description: False when the reported mode differs from the configured `billing_mode`.
        rate_limit:
          $ref: '#/components/schemas/AgentRateLimitState'
        billing_evidence:
          $ref: '#/components/schemas/BillingEvidence'
        warnings:
          type: array
          description: Known values `billing_mode_mismatch` (the reported mode has differed from the configured one for longer than `billing.mismatch_alert_after`), `billing_mode_unsupported` (no reported mode and the configured one is not in `billing_modes_supported`), `billing_evidence_unknown` (`billing_evidence.key_type` is `unknown`) and `tool_class_mismatch` (the reported tool class differs from the configured one). Only while the state is `online` or `degraded`.
          items:
            type: string
    AgentCreate:
      type: object
      additionalProperties: false
      required: [handle, display_name, adapter_kind]
      properties:
        handle:
          type: string
          pattern: ^[a-z0-9][a-z0-9._-]*$
          examples: [support-agent]
        display_name:
          type: string
          examples: [Support Agent]
        adapter_kind:
          type: string
          examples: [hermes]
        description:
          type: string
          examples: [Answers support tickets]
        operator_config:
          $ref: '#/components/schemas/JsonObject'
    AgentUpdate:
      type: object
      additionalProperties: false
      properties:
        display_name:
          type: string
          examples: [Support Agent]
        description:
          type: string
        operator_config:
          $ref: '#/components/schemas/JsonObject'
    BillingModeSet:
      type: object
      additionalProperties: false
      required: [billing_mode, reason]
      properties:
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
        credential_holder_id:
          $ref: '#/components/schemas/Uuid'
          description: Required for `personal`.
        reason:
          type: string
          minLength: 1
          examples: [Team uses the agent now]
    BillingModeSetAll:
      type: object
      additionalProperties: false
      description: Switches several agents in one transaction, all or nothing (10, 9.1).
      required: [billing_mode, reason]
      properties:
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
        credential_holder_id:
          $ref: '#/components/schemas/Uuid'
          description: Required for `personal`.
        agent_ids:
          type: array
          description: Required for `personal`. Without it `organization` affects every agent not yet on `organization`.
          items:
            $ref: '#/components/schemas/Uuid'
        reason:
          type: string
          minLength: 1
          examples: [Switch all agents to the company key]
    BillingModeSetAllResult:
      type: object
      required: [changed_agent_ids]
      properties:
        changed_agent_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
    CredentialHolderSet:
      type: object
      additionalProperties: false
      required: [credential_holder_id, reason]
      properties:
        credential_holder_id:
          $ref: '#/components/schemas/Uuid'
        reason:
          type: string
          minLength: 1
          examples: [Handover to deputy]
    Heartbeat:
      type: object
      additionalProperties: false
      required: [state]
      properties:
        state:
          type: string
          enum: [online, degraded]
        runtime_version:
          type: string
          description: Version of the agent runtime (column `reported_runtime_version`); the adapter's own version is `details.adapter.version`.
          pattern: '^[0-9A-Za-z][0-9A-Za-z.+_-]*$'
          examples: [2.3.1]
        reported_billing_mode:
          $ref: '#/components/schemas/BillingMode'
          description: Absent when the adapter follows the configured mode per run; then `billing_modes_supported` names both modes and the per-run check binds (ruling L8).
        billing_modes_supported:
          type: array
          items:
            $ref: '#/components/schemas/BillingMode'
        billing_evidence:
          $ref: '#/components/schemas/BillingEvidence'
        reported_tool_class:
          $ref: '#/components/schemas/ToolClass'
        rate_limit:
          $ref: '#/components/schemas/AgentRateLimitState'
          description: Stored in `agents.reported_details.rate_limit` as exactly `state` and `resets_at`; absent clears a stored one, because every heartbeat is the full reported state. Shown by `getStatsBackends`.
        details:
          $ref: '#/components/schemas/JsonObject'
          description: |-
            Further fields of schema `agent_heartbeat.v1`, a closed object; any other key, also inside `capabilities`, `adapter` and `runtime`, is refused. Keys: `capabilities` (object: `followups` `steer` or `queue`; `components`, `voice_input`, `title`, `files_out` booleans), `active_runs` (distinct run ids), `degraded_reasons` (distinct codes), `adapter` (`name` required, `version` optional), `runtime` (`name` required), `protocol_version` (integer, at least 1). Names match `^[a-z0-9][a-z0-9._-]*$`, versions `^[0-9A-Za-z][0-9A-Za-z.+_-]*$`, codes `^[a-z0-9][a-z0-9_.:-]*$`. No secrets, no content, no paths.
        project_readiness:
          type: array
          description: Readiness per bound forum of a runner (M2-03); the channels must be distinct. Applied only when present and different from the last list this server process applied; absent means "unchanged".
          items:
            $ref: '#/components/schemas/ProjectReadiness'
    ProjectReadiness:
      type: object
      additionalProperties: false
      required: [channel_id, ready]
      properties:
        channel_id:
          $ref: '#/components/schemas/Uuid'
        ready:
          type: boolean
    HeartbeatAck:
      type: object
      required: [presence, interval_ms]
      properties:
        presence:
          $ref: '#/components/schemas/AgentRuntimeStatus'
        interval_ms:
          type: integer
          format: int64
          description: Interval the adapter should keep, `sync.heartbeat_interval`.
    ConsumerAck:
      type: object
      additionalProperties: false
      required: [seq]
      properties:
        seq:
          type: integer
          format: int64
          minimum: 0
          examples: [5301]
    ConsumerState:
      type: object
      required: [consumer, acked_seq, head_seq]
      properties:
        consumer:
          type: string
          examples: [default]
        acked_seq:
          type: integer
          format: int64
        head_seq:
          type: integer
          format: int64
    RunState:
      type: string
      enum:
      - queued
      - running
      - waiting_input
      - completed
      - failed
      - cancelled
    RunMetrics:
      type: object
      additionalProperties: false
      description: Schema `run_metrics.v1`.
      properties:
        context_pct:
          type: number
          format: double
          examples: [37.5]
        tokens_in:
          type: integer
          format: int64
        tokens_out:
          type: integer
          format: int64
        tokens_cache_read:
          type: integer
          format: int64
        tokens_cache_write:
          type: integer
          format: int64
          description: Cache writes with 5 minutes (or without a stated duration).
        tokens_cache_write_1h:
          type: integer
          format: int64
          description: Cache writes with 1 hour, counted separately.
        subagents:
          type: integer
        background_jobs:
          type: integer
        api_retries:
          type: integer
        progress_events:
          type: integer
          description: Count of progress messages of the runtime in the current turn that changed nothing visible (for example a long running tool); a patch of it counts as progress against `run.stall_after` (09 5.5, 18.4).
        last_api_retry:
          $ref: '#/components/schemas/RunApiRetry'
    RunApiRetry:
      type: object
      additionalProperties: false
      description: Last retry of a model API call, shown in the live box while set.
      required: [attempt]
      properties:
        attempt:
          type: integer
          examples: [2]
        max_retries:
          type: integer
          examples: [5]
        retry_delay_ms:
          type: integer
          format: int64
          examples: [4000]
        error:
          type: string
          examples: [overloaded]
    RunPlan:
      type: object
      description: Schema `run_plan.v1`, shown in the live box.
      required: [items]
      properties:
        items:
          type: array
          items:
            type: object
            required: [id, text, status]
            properties:
              id:
                type: string
                examples: [step-1]
              text:
                type: string
                examples: [Reproduce the export error]
              status:
                type: string
                enum: [pending, in_progress, completed]
    RunContext:
      type: object
      description: Context of an own active run, schema `run_context.v1` (09, 4.6). Built by the server because only `authz` knows how trustworthy each author is.
      required: [run_id, trigger_message, thread_context, context_floor_seq]
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        trigger_message:
          $ref: '#/components/schemas/RunContextMessage'
        thread_context:
          type: array
          items:
            $ref: '#/components/schemas/RunContextMessage'
        channel_context:
          type: array
          description: Main-thread messages before the parent message of the thread, without the parent message itself. Present (possibly empty) exactly on a run in session mode `new` or `bind` and on a guest run, in a thread with `parent_message_id` whose parent message lies in the thread's current channel; absent otherwise, also after the thread was moved to another channel.
          items:
            $ref: '#/components/schemas/RunContextMessage'
        context_floor_seq:
          type: integer
          format: int64
        truncated_before_seq:
          type: integer
          format: int64
          description: Refers to the run's thread. Set when older selectable messages of `thread_context` are missing (the `history_backfill_limit` window or the byte budget left them out); then it is the `thread_seq` of the oldest element of `thread_context`, or of the triggering message when `thread_context` is empty. Load older messages with `thread.read`.
    RunContextMessage:
      type: object
      required: [message_id, thread_seq, created_at, author, role, origin, kind, channel_id, content_md, attachments, trust]
      properties:
        message_id:
          $ref: '#/components/schemas/Uuid'
        thread_seq:
          type: integer
          format: int64
        created_at:
          $ref: '#/components/schemas/Timestamp'
        author:
          $ref: '#/components/schemas/PrincipalRef'
        role:
          type: string
          enum: [user, assistant, other]
        origin:
          $ref: '#/components/schemas/MessageOrigin'
        kind:
          $ref: '#/components/schemas/MessageKind'
        channel_id:
          $ref: '#/components/schemas/Uuid'
          description: Channel of the message (SEC-AI-1).
        content_md:
          type: string
        truncated:
          type: boolean
          description: Present and true only when `content_md` was cut at `agent.context_message_max_bytes` on a UTF-8 boundary; never on `trigger_message`.
        attachments:
          type: array
          items:
            $ref: '#/components/schemas/RunContextAttachment'
        reply_to_message_id:
          $ref: '#/components/schemas/Uuid'
        trust:
          type: string
          enum: [trigger, authorized, self, unverified, system]
          description: How the runtime must treat the content; `unverified` only as marked data, never as instruction.
        already_delivered:
          type: boolean
          description: Present and true only in session mode `resume` for a message already delivered to this session as a follow-up (`run_followups` in `delivered` or `absorbed`).
    RunContextAttachment:
      type: object
      required: [file_id, original_name, mime_type, size_bytes, scan_status]
      properties:
        file_id:
          $ref: '#/components/schemas/Uuid'
        original_name:
          type: string
          examples: [screenshot.png]
        mime_type:
          type: string
          examples: [image/png]
        size_bytes:
          type: integer
          format: int64
        scan_status:
          type: string
          examples: [clean]
        transcript:
          type: string
    AgentTriggerers:
      type: object
      description: Principals that may trigger the calling agent (allow list of the adapter).
      required: [principal_ids, permission_version]
      properties:
        principal_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        permission_version:
          type: integer
          format: int64
          examples: [418]
    RunSummary:
      type: object
      description: Summary for sidebars, "running now" and collapsed live boxes.
      required:
      - id
      - thread_id
      - channel_id
      - agent_id
      - triggered_by
      - state
      - version
      - billing_mode_expected
      - cost
      - step_count
      - queued_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        agent_id:
          $ref: '#/components/schemas/Uuid'
        triggered_by:
          $ref: '#/components/schemas/Uuid'
        state:
          $ref: '#/components/schemas/RunState'
        state_reason:
          type: string
          examples: [user_cancelled]
        version:
          type: integer
          format: int64
        billing_mode_expected:
          $ref: '#/components/schemas/BillingMode'
        billing_mode_used:
          $ref: '#/components/schemas/BillingMode'
        status_line:
          type: string
          examples: [Reading the export logs]
        summary:
          type: string
          examples: [Found the cause and proposed a fix]
        cost:
          $ref: '#/components/schemas/Money'
        step_count:
          type: integer
        duration_ms:
          type: integer
          format: int64
        queued_at:
          $ref: '#/components/schemas/Timestamp'
        started_at:
          $ref: '#/components/schemas/Timestamp'
        finished_at:
          $ref: '#/components/schemas/Timestamp'
        stall_warned_at:
          $ref: '#/components/schemas/Timestamp'
          description: Set by `runs.stall_check` when a `running` run made no progress for `run.stall_after` (notice "no movement since ..."); the next stored patch clears it (09 5.5).
    Run:
      type: object
      description: Run document (`run_document.v1`), the content of the live box.
      required:
      - id
      - thread_id
      - channel_id
      - agent_id
      - triggered_by
      - state
      - version
      - billing_mode_expected
      - metrics
      - cost
      - steps
      - followups
      - queued_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        agent_id:
          $ref: '#/components/schemas/Uuid'
        agent_session_id:
          $ref: '#/components/schemas/Uuid'
        trigger_kind:
          type: string
          enum: [message, command, interaction]
        triggering_message_id:
          $ref: '#/components/schemas/Uuid'
        triggering_command_id:
          $ref: '#/components/schemas/Uuid'
        triggering_interaction_id:
          $ref: '#/components/schemas/Uuid'
        triggered_by:
          $ref: '#/components/schemas/Uuid'
        state:
          $ref: '#/components/schemas/RunState'
        state_reason:
          type: string
        version:
          type: integer
          format: int64
        billing_mode_expected:
          $ref: '#/components/schemas/BillingMode'
        billing_mode_used:
          $ref: '#/components/schemas/BillingMode'
        model:
          type: string
          examples: [model-large]
        status_line:
          type: string
          examples: [Reading the export logs]
        last_text_excerpt:
          type: string
        metrics:
          $ref: '#/components/schemas/RunMetrics'
        cost:
          $ref: '#/components/schemas/Money'
        summary:
          type: string
        steps:
          type: object
          description: Steps keyed by step ID; order comes from `ordinal`.
          additionalProperties:
            $ref: '#/components/schemas/RunStep'
        followups:
          type: object
          description: Follow-ups keyed by message ID.
          additionalProperties:
            $ref: '#/components/schemas/RunFollowup'
        queued_at:
          $ref: '#/components/schemas/Timestamp'
        started_at:
          $ref: '#/components/schemas/Timestamp'
        finished_at:
          $ref: '#/components/schemas/Timestamp'
        duration_ms:
          type: integer
          format: int64
        plan:
          $ref: '#/components/schemas/RunPlan'
        stall_warned_at:
          $ref: '#/components/schemas/Timestamp'
          description: Set by `runs.stall_check` when a `running` run made no progress for `run.stall_after` (notice "no movement since ..."); the next stored patch clears it (09 5.5).
    RunStep:
      type: object
      required: [id, ordinal, kind, name, title, status, started_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        ordinal:
          type: integer
        parent_step_id:
          $ref: '#/components/schemas/Uuid'
        kind:
          type: string
          enum: [tool_call, status, subagent, workflow, background_job]
        name:
          type: string
          examples: [read_file]
        title:
          type: string
          examples: [Reading export.log]
        status:
          type: string
          enum: [pending, running, completed, failed, cancelled]
        detail:
          $ref: '#/components/schemas/JsonObject'
          description: Schema `run_step_detail.v1`, cut to `run.step_detail_max_bytes`. A step of kind `subagent`, `workflow` or `background_job` may carry the member `usage` (object with the optional integers `tokens`, `tool_uses`, `duration_ms`, `cost_micros` and the text `currency`, required with `cost_micros`), reported by the adapter for display only, never a booking (09 5.6).
        started_at:
          $ref: '#/components/schemas/Timestamp'
        finished_at:
          $ref: '#/components/schemas/Timestamp'
    RunStepPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/RunStep'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    RunFollowup:
      type: object
      required: [message_id, state]
      properties:
        message_id:
          $ref: '#/components/schemas/Uuid'
        state:
          type: string
          enum: [queued, delivered, absorbed, deferred, discarded]
    RunPatch:
      type: object
      additionalProperties: false
      required: [base_version, ops]
      properties:
        base_version:
          type: integer
          format: int64
          examples: [7]
        ops:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/JsonPatchOperation'
    RunPatchResult:
      type: object
      required: [version]
      properties:
        version:
          type: integer
          format: int64
          examples: [8]
    RunStart:
      type: object
      additionalProperties: false
      required: [invocation_token, billing_mode_used, tool_class_used]
      properties:
        invocation_token:
          type: string
          description: JWS from the delivered invocation (I3).
          examples: [eyJhbGciOiJFZERTQSIsImtpZCI6Imludi0yMDI2LTEwIn0.e30.sig]
        billing_mode_used:
          $ref: '#/components/schemas/BillingMode'
        tool_class_used:
          $ref: '#/components/schemas/ToolClass'
        external_session_ref:
          type: string
          description: Absent when the runtime creates the reference only during the run; then the first `reportAgentSession` creates the session row.
          examples: [sess-7f3a]
        model:
          type: string
          examples: [model-large]
        billing_evidence:
          $ref: '#/components/schemas/BillingEvidence'
    InvocationTokenRenew:
      type: object
      additionalProperties: false
      required: [invocation_token]
      properties:
        invocation_token:
          type: string
          description: The current invocation token of the run, or on a retry after a lost answer the token the last renewal replaced; it may have expired (09 section 4.5.1).
          examples: [eyJhbGciOiJFZERTQSIsImtpZCI6Imludi0yMDI2LTEwIn0.e30.sig]
    InvocationTokenGrant:
      type: object
      required: [invocation_token, expires_at]
      properties:
        invocation_token:
          type: string
          description: Fresh invocation token with a new `jti`; it replaces the current one of the run.
          examples: [eyJhbGciOiJFZERTQSIsImtpZCI6Imludi0yMDI2LTEwIn0.e30.signature]
        expires_at:
          type: string
          format: date-time
          examples: ['2026-10-10T09:31:00.000Z']
    RunAwaitInput:
      type: object
      additionalProperties: false
      required: [status_line]
      properties:
        status_line:
          type: string
          examples: [Waiting for approval]
    RunFinish:
      type: object
      additionalProperties: false
      required: [state]
      properties:
        state:
          type: string
          enum: [completed, failed, cancelled]
        reason:
          type: string
          description: Required for `failed` and `cancelled`, e.g. `billing_mode_mismatch`, `adapter_error`, `user_cancelled`.
          examples: [adapter_error]
        summary:
          type: string
          examples: [Found the cause and proposed a fix]
        metrics:
          $ref: '#/components/schemas/RunMetrics'
        costs:
          type: array
          description: 'The consumption of this run, one report per model (a second report of the same provider and model is invalid). The server books one cost entry per report at the end of the run (10, 12.1); a repeated finish books nothing. Only a run that started (`billing_mode_used` set) may report costs.'
          items:
            $ref: '#/components/schemas/CostReport'
    CostReport:
      type: object
      additionalProperties: false
      description: 'Cost of one model in this run as reported by the adapter: the consumption of this run, never a session sum; stored as estimate. The server computes the amount from the price table when the price set of provider, model and the end of the run covers every consumed unit (source `computed`, the adapter amount is ignored), otherwise it books `amount_micros` (source `adapter_reported`), otherwise 0 (source `unpriced`, "Price missing"). Token counts and `amount_micros` are not negative and below 2^53; absent token counts are 0.'
      required: [provider, model, currency]
      properties:
        provider:
          type: string
          examples: [anthropic]
        model:
          type: string
          examples: [model-large]
        tokens_in:
          type: integer
          format: int64
        tokens_out:
          type: integer
          format: int64
        tokens_cache_read:
          type: integer
          format: int64
        tokens_cache_write:
          type: integer
          format: int64
          description: Cache writes with 5 minutes (or without a stated duration).
        tokens_cache_write_1h:
          type: integer
          format: int64
          description: Cache writes with 1 hour, priced with `cache_write_1h_mtok`.
        amount_micros:
          type: integer
          format: int64
          description: Absent lets the server compute from the price table.
        currency:
          type: string
          description: ISO 4217 code.
          examples: [USD]
    RunCancel:
      type: object
      additionalProperties: false
      properties:
        reason:
          type: string
          examples: [Wrong ticket]
    CostBreakdown:
      type: object
      required: [thread_id, totals, runs]
      properties:
        thread_id:
          $ref: '#/components/schemas/Uuid'
        totals:
          type: array
          description: Valid sums per billing mode and currency, never converted; sums of mode `personal` only when `billing.show_personal_estimates` is on.
          items:
            $ref: '#/components/schemas/CostTotal'
        runs:
          type: array
          description: Runs of the thread with at least one valid cost entry, by `run_id` ascending.
          items:
            $ref: '#/components/schemas/RunCost'
    CostTotal:
      type: object
      required: [billing_mode, amount_micros, currency, is_estimate]
      properties:
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
        amount_micros:
          type: integer
          format: int64
        currency:
          type: string
          description: ISO 4217 code.
          examples: [USD]
        is_estimate:
          type: boolean
          description: True while any summed entry is an estimate.
    RunCost:
      type: object
      required: [run_id, billing_mode, tokens, unpriced]
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
        agent_session_id:
          $ref: '#/components/schemas/Uuid'
        tokens:
          $ref: '#/components/schemas/TokenCounts'
        costs:
          type: array
          description: One amount per currency; absent when amounts are hidden (mode `personal` without `billing.show_personal_estimates`).
          items:
            $ref: '#/components/schemas/Money'
        unpriced:
          type: boolean
          description: A valid entry of the run has neither a price nor an amount of the adapter ("Price missing").
    TokenCounts:
      type: object
      required: [tokens_in, tokens_out, tokens_cache_read, tokens_cache_write, tokens_cache_write_1h]
      properties:
        tokens_in:
          type: integer
          format: int64
        tokens_out:
          type: integer
          format: int64
        tokens_cache_read:
          type: integer
          format: int64
        tokens_cache_write:
          type: integer
          format: int64
        tokens_cache_write_1h:
          type: integer
          format: int64
    Component:
      type: object
      required:
      - id
      - custom_id
      - type
      - definition
      - position
      - single_use
      - restricted_to
      - state
      - version
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        custom_id:
          type: string
          examples: [approve-refund]
        type:
          $ref: '#/components/schemas/ComponentType'
        definition:
          $ref: '#/components/schemas/JsonObject'
          description: Schema `component.v1` (09, 11.1) as stored, with the defaults applied (button `style`, select `min_values` and `max_values`, field `required`).
        position:
          type: integer
        single_use:
          type: boolean
        required_capability:
          $ref: '#/components/schemas/Capability'
        restricted_to:
          $ref: '#/components/schemas/ComponentRestriction'
        run_id:
          $ref: '#/components/schemas/Uuid'
        state:
          type: string
          enum: [active, used, expired, disabled]
        expires_at:
          $ref: '#/components/schemas/Timestamp'
        used_by:
          $ref: '#/components/schemas/Uuid'
        used_at:
          $ref: '#/components/schemas/Timestamp'
        version:
          type: integer
          format: int64
    ComponentCreate:
      type: object
      additionalProperties: false
      required: [custom_id, type, definition]
      properties:
        custom_id:
          type: string
          examples: [approve-refund]
        type:
          $ref: '#/components/schemas/ComponentType'
        definition:
          $ref: '#/components/schemas/JsonObject'
          description: Schema `component.v1` (09, 11.1), a closed member set per type; `400 validation_failed` with one entry per finding below `/components/<index>/definition` and the codes `required`, `invalid` or `limit_reached`.
        single_use:
          type: boolean
        required_capability:
          $ref: '#/components/schemas/Capability'
        restricted_to:
          $ref: '#/components/schemas/ComponentRestriction'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    ComponentUpdate:
      type: object
      additionalProperties: false
      properties:
        state:
          type: string
          enum: [active, disabled]
        definition:
          $ref: '#/components/schemas/JsonObject'
          description: A new `component.v1` object for the type of the component (09, 11.1); it may change in every state. Type, `custom_id`, `single_use`, `required_capability`, `restricted_to` and `run_id` cannot change.
    InteractionCreate:
      type: object
      additionalProperties: false
      required: [client_interaction_id]
      properties:
        client_interaction_id:
          $ref: '#/components/schemas/Uuid'
        values:
          $ref: '#/components/schemas/JsonObject'
          description: 'Selected options or form values, validated against the component definition (09, 11.1); absent means `{}`. Button: `{}`. Select: `{"selected": [<option value>, ...]}` with between `min_values` and `max_values` distinct option values. Form: an object keyed by field `name` (`text` and `textarea` strings, `number` a finite number, `boolean` a boolean, `choice` one of `choices`). A finding gives `400 validation_failed` with code `interaction_values_invalid` and its pointer; values whose `interaction.created` would exceed `sync.event_payload_max_bytes` give `too_long` on `/values`.'
    Interaction:
      type: object
      required:
      - id
      - component_id
      - message_id
      - principal_id
      - client_interaction_id
      - values
      - state
      - created_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        component_id:
          $ref: '#/components/schemas/Uuid'
        custom_id:
          type: string
          examples: [approve-refund]
        message_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        run_id:
          $ref: '#/components/schemas/Uuid'
          description: Run of the agent card the component belongs to; absent for a component of an app card.
        principal_id:
          $ref: '#/components/schemas/Uuid'
        client_interaction_id:
          $ref: '#/components/schemas/Uuid'
        values:
          $ref: '#/components/schemas/JsonObject'
        state:
          type: string
          description: '`received` when stored. For a component of an agent card the delivery sets `dispatched` when it
            hands `interaction.created` to the agent, or `failed` when the re-check denies (09, 11.2). For a component of
            an app card the job `runs.app_dispatch` acts after the commit: `failed` when the app has no active
            installation; for an app whose manifest declares `delivery.interactions_url` it re-checks with the rights of
            that moment (`failed` without a request on a denial), sets `dispatched` when it hands the event to that
            address, then `acknowledged` on a `200` answer whose reply message is written in the name of the app, and
            `failed` when the app does not answer within `app.interaction_timeout` or gives any other answer; a `202`
            keeps `dispatched` until the app calls `respondToInteraction`. For an app that receives its events over the
            gateway the interaction stays `received` until `respondToInteraction`. `respondToInteraction` moves the
            state from `received` or `dispatched` to `acknowledged`, `failed` or `rejected`; every change is a
            compare-and-swap, so the first of the job and the owner wins and the job never overwrites the answer of the
            owner.'
          enum: [received, dispatched, acknowledged, failed, rejected]
        response:
          $ref: '#/components/schemas/InteractionResponse'
        created_at:
          $ref: '#/components/schemas/Timestamp'
    InteractionResponse:
      type: object
      additionalProperties: false
      properties:
        text:
          type: string
          examples: [Refund approved]
    InteractionRespond:
      type: object
      additionalProperties: false
      required: [state]
      properties:
        state:
          type: string
          enum: [acknowledged, failed, rejected]
        response:
          $ref: '#/components/schemas/InteractionResponse'
    CommandOption:
      type: object
      additionalProperties: false
      required: [name, description, type, required]
      properties:
        name:
          type: string
          examples: [ticket]
        description:
          type: string
          examples: [Ticket number]
        type:
          type: string
          enum: [string, integer, boolean, choice, user, file]
        required:
          type: boolean
        choices:
          type: array
          items:
            $ref: '#/components/schemas/CommandChoice'
        autocomplete:
          type: boolean
    CommandChoice:
      type: object
      additionalProperties: false
      required: [name, value]
      properties:
        name:
          type: string
          examples: [High priority]
        value:
          type: string
          description: Canonical string form of the value; the option type defines its syntax.
          examples: [high]
    Command:
      type: object
      required:
      - id
      - owner_principal_id
      - name
      - description
      - scope_kind
      - options
      - required_level
      - control
      - version
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        owner_principal_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [escalate]
        description:
          type: string
          examples: [Escalate the ticket to second level]
        scope_kind:
          $ref: '#/components/schemas/TargetKind'
        scope_id:
          $ref: '#/components/schemas/Uuid'
        options:
          type: array
          items:
            $ref: '#/components/schemas/CommandOption'
        required_level:
          $ref: '#/components/schemas/CommandLevel'
        control:
          type: boolean
        version:
          type: integer
          format: int64
    CommandRegister:
      type: object
      additionalProperties: false
      required: [name, description]
      properties:
        name:
          type: string
          pattern: ^[a-z0-9][a-z0-9_-]*$
          examples: [escalate]
        description:
          type: string
          examples: [Escalate the ticket to second level]
        scope_kind:
          $ref: '#/components/schemas/TargetKind'
        scope_id:
          $ref: '#/components/schemas/Uuid'
        options:
          type: array
          items:
            $ref: '#/components/schemas/CommandOption'
        required_level:
          $ref: '#/components/schemas/CommandLevel'
        control:
          type: boolean
    CommandList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Command'
    CommandOptionValue:
      type: object
      additionalProperties: false
      required: [name, value]
      properties:
        name:
          type: string
          examples: [ticket]
        value:
          type: string
          examples: ['4711']
    CommandInvoke:
      type: object
      additionalProperties: false
      properties:
        thread_id:
          $ref: '#/components/schemas/Uuid'
          description: 'Absent: in a text channel the main thread for a control command, and for a model command only when
            `auto_thread` is false; otherwise `400 validation_failed` (`/thread_id`, `required`), because a command has no
            message from which the `auto_thread` thread could start. For an app command absent means the main thread in
            a text channel and no thread in a forum channel, never `400`; `auto_thread` does not apply.'
        options:
          type: array
          items:
            $ref: '#/components/schemas/CommandOptionValue'
    CommandInvocation:
      type: object
      required: [invocation_id, command_id, channel_id, accepted]
      properties:
        invocation_id:
          $ref: '#/components/schemas/Uuid'
          description: New per accepted invocation; it appears as `invocation_id` in `command.invoked` and, for agent
            commands, as `command_invocation_id` in `agent.invocation`.
        command_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
          description: Target thread of the invocation; absent when there is none, for example for an app command in a
            forum channel without `thread_id`.
        run_id:
          $ref: '#/components/schemas/Uuid'
          description: Run created for non-control commands of agents. Always absent for app commands, because apps have
            no runs.
        accepted:
          type: boolean
          description: Always true, also for app commands; a refusal is an error response.
    AutocompleteQuery:
      type: object
      additionalProperties: false
      required: [option, value]
      properties:
        thread_id:
          $ref: '#/components/schemas/Uuid'
        option:
          type: string
          examples: [ticket]
        value:
          type: string
          examples: ['47']
        options:
          type: array
          description: Values of the other options entered so far.
          items:
            $ref: '#/components/schemas/CommandOptionValue'
    AutocompleteResult:
      type: object
      required: [choices]
      properties:
        choices:
          type: array
          maxItems: 25
          description: At most `command.autocomplete_max_choices`.
          items:
            $ref: '#/components/schemas/CommandChoice'
    AutocompleteAnswer:
      type: object
      additionalProperties: false
      required: [choices]
      properties:
        choices:
          type: array
          maxItems: 25
          description: At most `command.autocomplete_max_choices`.
          items:
            $ref: '#/components/schemas/CommandChoice'
    AppManifest:
      type: object
      additionalProperties: false
      description: 'Schema `app_manifest.v1` (11, 3.1). Unknown members are an error. Beyond the schema the server applies the check rules of 11, 3.2: scopes from the catalogue, only subscribable events with the scope of their receive action and `events:subscribe`, `https` delivery addresses whose host is in `webhook.allowed_hosts`, `requested_grants` at most `write` and at most the capability `upload` and never on a host, commands without reserved names and at most `command.max_per_principal`, and at most `app.manifest_max_bytes`.'
      required: [schema_version, slug, name, description, publisher, version, min_api_version, scopes]
      properties:
        schema_version:
          type: integer
          enum: [1]
          examples: [1]
        slug:
          type: string
          pattern: ^[a-z0-9][a-z0-9-]*$
          description: Unique in the instance; becomes the handle of the app principal.
          examples: [ticket-sync]
        name:
          type: string
          minLength: 1
          description: Becomes the display name of the app principal.
          examples: [Ticket Sync]
        description:
          type: string
          minLength: 1
          examples: [Mirrors tickets of a help desk into a forum.]
        publisher:
          type: string
          minLength: 1
          examples: [Example Ltd]
        version:
          type: string
          description: SemVer 2.0.0 of the manifest (`apps.manifest_version`).
          examples: [1.2.0]
        homepage_url:
          type: string
          description: Absolute `https` address for display only; the server never loads it.
          examples: ['https://apps.example.com/ticket-sync']
        min_api_version:
          type: string
          pattern: ^[0-9]+\.[0-9]+$
          description: Lowest API version (major.minor) the app needs; not above the version of this server.
          examples: ['1.0']
        scopes:
          type: object
          additionalProperties: false
          required: [required, optional]
          properties:
            required:
              type: array
              items:
                $ref: '#/components/schemas/AppScope'
            optional:
              type: array
              items:
                $ref: '#/components/schemas/AppScope'
        events:
          type: array
          description: Durable event types (05, 6) the app may subscribe to; each needs the scope of its receive action and `events:subscribe`.
          items:
            type: string
            examples: [message.created]
        delivery:
          type: object
          additionalProperties: false
          description: Required with `events`, `commands` or `components`.
          required: [mode]
          properties:
            mode:
              type: string
              enum: [gateway, webhook]
            events_url:
              type: string
              description: Required for `webhook`, not allowed for `gateway`; `https`, host in `webhook.allowed_hosts`.
              examples: ['https://apps.example.com/ticket-sync/events']
            interactions_url:
              type: string
              description: Required for commands or components without the gateway; `https`, host in `webhook.allowed_hosts`.
              examples: ['https://apps.example.com/ticket-sync/interactions']
        commands:
          type: array
          description: Commands after 09, 10, without reserved names and without a target.
          items:
            $ref: '#/components/schemas/CommandRegister'
        components:
          type: array
          description: Component types the app uses; only shown for the consent.
          items:
            type: string
            enum: [button, select, form]
        requested_grants:
          type: array
          description: Grants the app asks for; the owner creates them per target with `createGrant`. Only `read` or `write`, at most the capability `upload`, never on a host.
          items:
            type: object
            additionalProperties: false
            required: [target_kind, level, purpose]
            properties:
              target_kind:
                $ref: '#/components/schemas/TargetKind'
              level:
                $ref: '#/components/schemas/Level'
              capabilities:
                type: array
                items:
                  $ref: '#/components/schemas/Capability'
              purpose:
                type: string
                minLength: 1
                examples: [Posts per ticket]
        incoming_webhooks:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [name, purpose]
            properties:
              name:
                type: string
                minLength: 1
                examples: [alerts]
              purpose:
                type: string
                minLength: 1
                examples: [Alerts of the help desk]
        data_egress:
          type: string
          description: Plain text on which data leave the instance and where to; required with `events` or `interactions_url`.
          examples: [Messages of the granted channels go to apps.example.com.]
    App:
      type: object
      required:
      - id
      - slug
      - display_name
      - manifest
      - manifest_version
      - installation
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        slug:
          type: string
          examples: [ticket-sync]
        display_name:
          type: string
          examples: [Ticket Sync]
        manifest:
          $ref: '#/components/schemas/AppManifest'
        manifest_version:
          type: string
          examples: [1.2.0]
        installation:
          $ref: '#/components/schemas/AppInstallation'
    AppInstallation:
      type: object
      required:
      - id
      - granted_scopes
      - event_subscriptions
      - state
      - installed_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        granted_scopes:
          type: array
          items:
            $ref: '#/components/schemas/AppScope'
        event_subscriptions:
          type: array
          items:
            type: string
            examples: [message.created]
        state:
          type: string
          enum: [active, suspended, uninstalled]
        installed_by:
          $ref: '#/components/schemas/Uuid'
        installed_at:
          $ref: '#/components/schemas/Timestamp'
    AppInstall:
      type: object
      additionalProperties: false
      required: [manifest, granted_scopes, reason]
      properties:
        manifest:
          $ref: '#/components/schemas/AppManifest'
        granted_scopes:
          type: array
          items:
            $ref: '#/components/schemas/AppScope'
        event_subscriptions:
          type: array
          items:
            type: string
            examples: [message.created]
        reason:
          type: string
          examples: [Sync tickets with the help desk]
    AppScopesPut:
      type: object
      additionalProperties: false
      required: [granted_scopes, reason]
      properties:
        granted_scopes:
          type: array
          items:
            $ref: '#/components/schemas/AppScope'
        event_subscriptions:
          type: array
          items:
            type: string
        reason:
          type: string
          examples: [App no longer needs files]
    ManifestValidation:
      type: object
      required: [valid, errors]
      properties:
        valid:
          type: boolean
        errors:
          type: array
          items:
            $ref: '#/components/schemas/FieldError'
    WebhookSubscription:
      type: object
      required:
      - id
      - url
      - event_types
      - state
      - consecutive_failures
      - signature_schemes
      - signing_key_kid
      - created_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        app_id:
          $ref: '#/components/schemas/Uuid'
          description: Set for subscriptions of an app.
        agent_id:
          $ref: '#/components/schemas/Uuid'
          description: Set for subscriptions of an agent (agent transport, 04 15.6); exactly one of `app_id` and `agent_id`.
        url:
          type: string
          examples: ['https://hooks.example.com/chat']
        event_types:
          type: array
          items:
            type: string
            examples: [message.created]
        state:
          type: string
          enum: [active, paused, disabled]
        consecutive_failures:
          type: integer
        signature_schemes:
          type: array
          items:
            type: string
            enum: [v1a, v1]
        signing_key_kid:
          type: string
          examples: [whk-2026-10]
        created_at:
          $ref: '#/components/schemas/Timestamp'
        disabled_at:
          $ref: '#/components/schemas/Timestamp'
    WebhookSubscriptionCreate:
      type: object
      additionalProperties: false
      required: [url, event_types]
      properties:
        url:
          type: string
          description: HTTPS URL; the host must be in `webhook.allowed_hosts`.
          examples: ['https://hooks.example.com/chat']
        event_types:
          type: array
          items:
            type: string
            examples: [message.created]
        enable_hmac:
          type: boolean
          description: Also sign with Standard Webhooks `v1` (HMAC); the secret is returned once.
    WebhookSubscriptionUpdate:
      type: object
      additionalProperties: false
      properties:
        url:
          type: string
        event_types:
          type: array
          items:
            type: string
        state:
          type: string
          enum: [active, paused]
        rotate_hmac_secret:
          type: boolean
    WebhookSubscriptionCreated:
      type: object
      required: [subscription]
      properties:
        subscription:
          $ref: '#/components/schemas/WebhookSubscription'
        hmac_secret:
          type: string
          description: Standard Webhooks secret with prefix `whsec_`. Shown once.
          examples: [whsec_EXAMPLE]
    WebhookSubscriptionList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/WebhookSubscription'
    WebhookDelivery:
      type: object
      required:
      - id
      - subscription_id
      - event_id
      - event_seq
      - attempt
      - status
      - created_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        subscription_id:
          $ref: '#/components/schemas/Uuid'
        event_id:
          $ref: '#/components/schemas/Uuid'
        event_seq:
          type: integer
          format: int64
        event_type:
          type: string
          description: Type of the event, read from the event log; absent once the event left the event log (`sync.event_retention`).
          examples: [message.created]
        attempt:
          type: integer
        status:
          $ref: '#/components/schemas/WebhookDeliveryStatus'
        response_code:
          type: integer
        last_error:
          type: string
          examples: [timeout]
        next_attempt_at:
          $ref: '#/components/schemas/Timestamp'
        created_at:
          $ref: '#/components/schemas/Timestamp'
        delivered_at:
          $ref: '#/components/schemas/Timestamp'
    IncomingWebhook:
      type: object
      required: [id, app_id, channel_id, name, created_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        app_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Monitoring alerts]
        created_at:
          $ref: '#/components/schemas/Timestamp'
        last_used_at:
          $ref: '#/components/schemas/Timestamp'
        revoked_at:
          $ref: '#/components/schemas/Timestamp'
    IncomingWebhookCreate:
      type: object
      additionalProperties: false
      description: |-
        Body of `createIncomingWebhook`. `app_id` names an app principal whose installation is
        `active` (else `400 validation_failed` with `errors[].code` `app_not_installed`) and that may
        write in the channel (else `app_cannot_write`); `thread_id`, when present, is a thread of this
        channel (else `not_in_channel`, an unknown ID too). `name` is not empty and has no control or
        bidi characters.
      required: [app_id, name]
      properties:
        app_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Monitoring alerts]
        thread_id:
          $ref: '#/components/schemas/Uuid'
    IncomingWebhookCreated:
      type: object
      required: [webhook, url]
      properties:
        webhook:
          $ref: '#/components/schemas/IncomingWebhook'
        url:
          type: string
          description: |-
            Absolute URL from `server.public_url` including the token, shown once. Senders that set a
            header drop the last segment and send the token as `Authorization: Bearer <token>`.
          examples:
          - https://chat.example.com/api/v1/hooks/0192f4a0-7b1c-7000-8a00-000000000901/bothy_whi_EXAMPLE000000000000000000000000000000000000_2WwD3e
    IncomingWebhookList:
      type: object
      description: The active incoming webhooks of a channel in creation order; revoked ones are not listed.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/IncomingWebhook'
    IncomingWebhookPost:
      type: object
      additionalProperties: true
      description: |-
        Body of an incoming webhook call in one of two shapes, recognised per request (11 8.2,
        docs/DECISIONS.md D-M2-27-5, D-M7-20-5). A body with at least one Alertmanager member
        (`version`, `groupKey`, `truncatedAlerts`, `status`, `receiver`, `groupLabels`,
        `commonLabels`, `commonAnnotations`, `externalURL`, `alerts`) is the unchanged webhook
        payload of Prometheus Alertmanager (format `alertmanager`) and must contain no native
        member; other members of such a payload, at the top and in each alert, are ignored, because
        Alertmanager adds members in new versions and drops a notification answered with `400`.
        Every other body is `native`, needs `content_md` and refuses every other member with
        `400 validation_failed` and `errors[].code` `unknown_member`.
      properties:
        content_md:
          type: string
          description: Native. Markdown content, at most `message.max_content_bytes` UTF-8 bytes.
          examples: [Disk usage above threshold on build host.]
        client_msg_id:
          $ref: '#/components/schemas/Uuid'
          description: Native. Optional UUIDv7; makes retries of the sender idempotent.
        title:
          type: string
          description: Native. Title of a new thread when the webhook has no fixed thread; required in a forum.
          examples: [Disk alert]
        thread_id:
          $ref: '#/components/schemas/Uuid'
          description: Native. Thread of the webhook's channel to write into when the webhook has no fixed thread.
        version:
          type: string
          enum: ['4']
          description: Alertmanager. Version of the webhook payload.
        groupKey:
          type: string
          description: Alertmanager. Key of the alert group.
        truncatedAlerts:
          type: integer
          format: int64
          minimum: 0
          description: Alertmanager. Number of alerts left out of `alerts`.
        status:
          type: string
          enum: [firing, resolved]
          description: Alertmanager. State of the group.
        receiver:
          type: string
          description: Alertmanager. Name of the receiver.
        groupLabels:
          $ref: '#/components/schemas/JsonObject'
          description: Alertmanager. Labels of the group; every value is a string.
        commonLabels:
          $ref: '#/components/schemas/JsonObject'
          description: Alertmanager. Labels all alerts share; every value is a string.
        commonAnnotations:
          $ref: '#/components/schemas/JsonObject'
          description: Alertmanager. Annotations all alerts share; every value is a string.
        externalURL:
          type: string
          description: Alertmanager. Address of the sending Alertmanager; written as text, never fetched.
        alerts:
          type: array
          minItems: 1
          description: Alertmanager. The alerts of the notification.
          items:
            $ref: '#/components/schemas/AlertmanagerAlert'
    AlertmanagerAlert:
      type: object
      description: One alert of an Alertmanager webhook payload; `status` and `labels` are required (checked with `errors[].code` `required`); other members are ignored (D-M7-20-5).
      properties:
        status:
          type: string
          enum: [firing, resolved]
        labels:
          $ref: '#/components/schemas/JsonObject'
          description: Labels of the alert; every value is a string.
        annotations:
          $ref: '#/components/schemas/JsonObject'
          description: Annotations of the alert; every value is a string.
        startsAt:
          type: string
          format: date-time
        endsAt:
          type: string
          format: date-time
        generatorURL:
          type: string
          description: Address of the rule's source; written as text, never fetched.
        fingerprint:
          type: string
    AuditEvent:
      type: object
      required:
      - seq
      - id
      - occurred_at
      - action
      - decision
      - details
      - prev_hash
      - hash
      properties:
        seq:
          type: integer
          format: int64
        id:
          $ref: '#/components/schemas/Uuid'
        occurred_at:
          $ref: '#/components/schemas/Timestamp'
        actor_principal_id:
          $ref: '#/components/schemas/Uuid'
        actor_device_id:
          $ref: '#/components/schemas/Uuid'
        actor_session_id:
          $ref: '#/components/schemas/Uuid'
        action:
          type: string
          examples: [grant.create]
        decision:
          $ref: '#/components/schemas/AuditDecision'
        deny_reason:
          type: string
        resource_kind:
          type: string
          examples: [grant]
        resource_id:
          $ref: '#/components/schemas/Uuid'
        details:
          $ref: '#/components/schemas/JsonObject'
        correlation_id:
          $ref: '#/components/schemas/CorrelationId'
        prev_hash:
          type: string
          description: Hex.
        hash:
          type: string
          description: Hex.
    AuditVerifyRequest:
      type: object
      additionalProperties: false
      properties:
        from_seq:
          type: integer
          format: int64
        to_seq:
          type: integer
          format: int64
    AuditVerifyResult:
      type: object
      required: [ok, checked_from_seq, checked_to_seq, checkpoints_compared, warnings]
      properties:
        ok:
          type: boolean
        checked_from_seq:
          type: integer
          format: int64
        checked_to_seq:
          type: integer
          format: int64
        first_broken_seq:
          type: integer
          format: int64
        failure_reason:
          type: string
          enum: [hash_mismatch, link_mismatch, seq_gap, checkpoint_mismatch, checkpoint_invalid]
        checkpoints_compared:
          type: integer
          format: int64
        last_checkpoint_seq:
          type: integer
          format: int64
        warnings:
          type: array
          description: Known value `checkpoint_sink_not_configured`.
          items:
            type: string
    StatsBucket:
      type: object
      description: One time bucket of one group. `group_key` is set when the query groups (space, agent, tool).
      required: [start, metrics]
      properties:
        start:
          description: Start of the bucket as a UTC instant (local midnight of `stats.time_zone` for `day`, `week` and `month`).
          $ref: '#/components/schemas/Timestamp'
        group_key:
          type: string
          description: Space ID, agent ID or tool name, as the operation names it; never a person.
          examples: [0192f4a0-7b1c-7000-8a00-000000000101]
        metrics:
          type: object
          description: Metric name to value. Names per endpoint are listed in its description; a metric without a value is absent.
          additionalProperties:
            type: number
            format: double
        below_min_group:
          type: array
          description: Metrics that count people and were withheld because their value is below `privacy.stats_min_group_size`; the client shows "fewer than" the minimum group.
          items:
            type: string
            examples: [active_people]
    StatsSeries:
      type: object
      required: [granularity, buckets]
      properties:
        granularity:
          $ref: '#/components/schemas/StatsGranularity'
        buckets:
          type: array
          items:
            $ref: '#/components/schemas/StatsBucket'
    CostSeries:
      type: object
      required: [granularity, currency, buckets]
      properties:
        granularity:
          $ref: '#/components/schemas/StatsGranularity'
        currency:
          type: string
          examples: [USD]
        buckets:
          type: array
          items:
            $ref: '#/components/schemas/CostBucket'
    CostBucket:
      type: object
      required: [start, billing_mode, amount_micros, is_estimate]
      properties:
        start:
          $ref: '#/components/schemas/Timestamp'
        group_key:
          type: string
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
        amount_micros:
          type: integer
          format: int64
        is_estimate:
          type: boolean
    Price:
      type: object
      required:
      - id
      - provider
      - model
      - unit
      - price_micros
      - currency
      - valid_from
      - source_url
      - retrieved_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        provider:
          type: string
          examples: [anthropic]
        model:
          type: string
          examples: [model-large]
        unit:
          $ref: '#/components/schemas/PriceUnit'
        price_micros:
          type: integer
          format: int64
        currency:
          type: string
          examples: [USD]
        valid_from:
          $ref: '#/components/schemas/Timestamp'
        valid_to:
          $ref: '#/components/schemas/Timestamp'
        source_url:
          type: string
          examples: ['https://provider.example.com/pricing']
        retrieved_at:
          $ref: '#/components/schemas/Timestamp'
    PriceCreate:
      type: object
      additionalProperties: false
      description: 'A price per unit (per million tokens for the `*_mtok` units) with its public source. `valid_to`, when present, lies after `valid_from`; `valid_from` may lie in the past. `source_url` is an absolute https URL without user info and fragment; `retrieved_at` is not in the future.'
      required:
      - provider
      - model
      - unit
      - price_micros
      - currency
      - valid_from
      - source_url
      - retrieved_at
      properties:
        provider:
          type: string
          examples: [anthropic]
        model:
          type: string
          examples: [model-large]
        unit:
          $ref: '#/components/schemas/PriceUnit'
        price_micros:
          type: integer
          format: int64
        currency:
          type: string
          examples: [USD]
        valid_from:
          $ref: '#/components/schemas/Timestamp'
        valid_to:
          $ref: '#/components/schemas/Timestamp'
        source_url:
          type: string
          examples: ['https://provider.example.com/pricing']
        retrieved_at:
          $ref: '#/components/schemas/Timestamp'
    PriceClose:
      type: object
      additionalProperties: false
      description: The new end of the validity, after `valid_from`.
      required: [valid_to]
      properties:
        valid_to:
          $ref: '#/components/schemas/Timestamp'
    PriceList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Price'
    CostReconcile:
      type: object
      additionalProperties: false
      required: [provider, period_start, period_end, entries]
      properties:
        provider:
          type: string
          minLength: 1
          examples: [anthropic]
        period_start:
          $ref: '#/components/schemas/Timestamp'
        period_end:
          $ref: '#/components/schemas/Timestamp'
        entries:
          type: array
          items:
            $ref: '#/components/schemas/ReconcileEntry'
    ReconcileEntry:
      type: object
      additionalProperties: false
      required: [run_id, amount_micros, currency]
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        amount_micros:
          type: integer
          format: int64
          minimum: 0
        currency:
          type: string
          pattern: '^[A-Z]{3}$'
          description: ISO 4217 code.
          examples: [USD]
    CostReconcileResult:
      type: object
      required: [reconciled, unmatched_run_ids]
      properties:
        reconciled:
          type: integer
        unmatched_run_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
    CostEstimate:
      type: object
      description: Estimate of one run in the requested range for the reconciliation (10, 12.4; 05, R-AZ-28). Names no channel, thread or content.
      required: [run_id, agent_id, provider, currency, estimate_micros, reconciled]
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        agent_id:
          $ref: '#/components/schemas/Uuid'
        provider:
          type: string
          examples: [anthropic]
        currency:
          type: string
          description: ISO 4217 code.
          examples: [USD]
        estimate_micros:
          type: integer
          format: int64
          description: Sum in micro units of the run's estimate entries in the range whose successor, if any, is not an estimate; the estimate before any reconciliation.
          examples: [1250000]
        reconciled:
          type: boolean
          description: True when every valid entry of the run in the range has `source = reconciled`.
    CostEstimatePage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CostEstimate'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    ClientMetricsBatch:
      type: object
      additionalProperties: false
      description: Anonymous client measurements (15, section 17). No contents, no IDs of threads, channels or people.
      required: [platform, app_version, metrics]
      properties:
        platform:
          type: string
          enum: [ios, android, macos, windows, web]
        app_version:
          type: string
          examples: [1.4.0]
        density:
          type: string
          enum: [comfortable, compact, touch]
          description: Mobile apps always send `touch` (01, section 11).
        theme:
          type: string
          enum: [light, dark]
        metrics:
          type: array
          maxItems: 200
          items:
            $ref: '#/components/schemas/ClientMetric'
    ClientMetric:
      type: object
      additionalProperties: false
      required: [name, kind]
      properties:
        name:
          type: string
          description: |-
            One name of the closed list in docs/observability/signals.md section 4.6a: `client.cold_start.duration`, `client.thread_open.duration`, `client.send_local.duration`, `client.frames`, `client.frames.over_budget`, `client.catchup.duration`, `client.catchup.events`, `client.resync`, `client.reconnects`, `client.outbox.depth`, `client.push_display.duration`, `client.push.placeholder`. Not an enum on purpose: the server drops an entry with an unknown name, or with a `kind` that does not match the type of its name, one by one and still accepts the rest of the batch, so a newer app does not lose its batch against an older server.
          pattern: '^client\.[a-z_.]+$'
          maxLength: 64
          examples: [client.thread_open.duration]
        kind:
          type: string
          enum: [histogram, counter, gauge]
        close_code:
          type: integer
          description: Only for `client.reconnects`; a close code of 04 section 6.4.
          examples: [4009]
        value:
          type: number
          format: double
          description: Counter increment or gauge value.
        buckets:
          type: array
          description: Histogram buckets (upper bound and count).
          items:
            type: object
            additionalProperties: false
            required: [le, count]
            properties:
              le:
                type: number
                format: double
              count:
                type: integer
                format: int64
    StatusComponentState:
      type: string
      enum:
      - operational
      - degraded
      - partial_outage
      - major_outage
      - maintenance
    StatusComponent:
      type: object
      required: [id, name, description, sort_key, state, updated_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Messages]
        description:
          type: string
        sort_key:
          type: string
          examples: [a0]
        state:
          $ref: '#/components/schemas/StatusComponentState'
        health_source:
          type: string
          examples: [db]
        agent_id:
          $ref: '#/components/schemas/Uuid'
        updated_at:
          $ref: '#/components/schemas/Timestamp'
    StatusComponentInput:
      type: object
      additionalProperties: false
      required: [name]
      properties:
        name:
          type: string
          examples: [Messages]
        description:
          type: string
        state:
          $ref: '#/components/schemas/StatusComponentState'
        health_source:
          type: string
        agent_id:
          $ref: '#/components/schemas/Uuid'
        after_component_id:
          $ref: '#/components/schemas/Uuid'
    StatusIncidentState:
      type: string
      enum: [scheduled, investigating, identified, monitoring, resolved]
    StatusIncident:
      type: object
      required:
      - id
      - title
      - kind
      - impact
      - state
      - started_at
      - component_ids
      - updates
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        title:
          type: string
          examples: [Push notifications delayed]
        kind:
          $ref: '#/components/schemas/StatusIncidentKind'
        impact:
          $ref: '#/components/schemas/StatusIncidentImpact'
        state:
          $ref: '#/components/schemas/StatusIncidentState'
        started_at:
          $ref: '#/components/schemas/Timestamp'
        resolved_at:
          $ref: '#/components/schemas/Timestamp'
        component_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        updates:
          type: array
          items:
            $ref: '#/components/schemas/StatusIncidentUpdate'
    StatusIncidentUpdate:
      type: object
      required: [id, state, body_md, created_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        state:
          $ref: '#/components/schemas/StatusIncidentState'
        body_md:
          type: string
          examples: [The push provider reports delays. We are monitoring.]
        created_at:
          $ref: '#/components/schemas/Timestamp'
    StatusIncidentCreate:
      type: object
      additionalProperties: false
      required: [title, kind, impact, started_at, component_ids, body_md]
      properties:
        title:
          type: string
          examples: [Push notifications delayed]
        kind:
          $ref: '#/components/schemas/StatusIncidentKind'
        impact:
          $ref: '#/components/schemas/StatusIncidentImpact'
        state:
          $ref: '#/components/schemas/StatusIncidentState'
        started_at:
          $ref: '#/components/schemas/Timestamp'
        component_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        body_md:
          type: string
          examples: [The push provider reports delays.]
    StatusIncidentUpdateCreate:
      type: object
      additionalProperties: false
      required: [state, body_md]
      properties:
        state:
          $ref: '#/components/schemas/StatusIncidentState'
        body_md:
          type: string
          examples: [Delays are resolved.]
    StatusPage:
      type: object
      required: [overall_state, components, incidents]
      properties:
        overall_state:
          $ref: '#/components/schemas/StatusComponentState'
        components:
          type: array
          items:
            $ref: '#/components/schemas/StatusComponent'
        incidents:
          type: array
          description: Open incidents and recent ones within `status.history_window`.
          items:
            $ref: '#/components/schemas/StatusIncident'
    ImportSource:
      type: object
      required: [id, kind, name, config, state, stats, created_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        kind:
          $ref: '#/components/schemas/ImportKind'
        name:
          type: string
          examples: [Old community server]
        config:
          $ref: '#/components/schemas/JsonObject'
          description: Schema `import_source.<kind>.v1`; credentials only as `secret_ref` names.
        state:
          type: string
          enum: [draft, running, paused, completed, failed]
          description: A dry run writes nothing and keeps the state.
        stats:
          $ref: '#/components/schemas/JsonObject'
          description: Counters without content. The server's counters per item kind and state are under `server` (`server.<item_kind>.<state>`, `server.source_changed`, `server.cancelled`); the counters of the import tool from `setImportCursor` are under `tool`.
        cursor:
          $ref: '#/components/schemas/JsonObject'
          description: The object last stored by `setImportCursor` (progress and the run lease of the import tool, 22 7.3 and 7.4); `{}` before the first one. The server never interprets it.
        created_at:
          $ref: '#/components/schemas/Timestamp'
        started_at:
          $ref: '#/components/schemas/Timestamp'
        finished_at:
          $ref: '#/components/schemas/Timestamp'
    ImportSourceCreate:
      type: object
      additionalProperties: false
      required: [kind, name, config]
      properties:
        kind:
          $ref: '#/components/schemas/ImportKind'
        name:
          type: string
          examples: [Old community server]
        config:
          $ref: '#/components/schemas/JsonObject'
          description: For `kind = discord` the mapping of schema `import_source.discord.v1`, see `ImportSourceDiscordConfig` (22, 5.2); every violation is one error of `400 validation_failed` with its JSON pointer, all reported at once. `kind = archive` is `400 validation_failed` with the code `not_enabled`.
    ImportItem:
      type: object
      required: [id, origin_key, item_kind, state, created_at]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        origin_key:
          type: string
          examples: ['message:1029384756:0']
        item_kind:
          type: string
          enum:
          - space
          - channel
          - forum_tag
          - thread
          - message
          - file
          - user
          - reaction
          - pin
        target_kind:
          type: string
        target_id:
          $ref: '#/components/schemas/Uuid'
        state:
          $ref: '#/components/schemas/ImportItemState'
        error:
          type: string
        source_changed:
          type: boolean
          description: Present and `true` when a repeated key came with a `content_sha256` different from the stored one; nothing was changed (22, 7.2 item 4).
        session:
          type: string
          enum: [taken_over, already_present, dropped_late]
          description: 'Present on a `thread` item that carried `earlier_session` and has a thread (22, 9 step 1): `taken_over` when the earlier `agent_sessions` row was written by this batch, `already_present` when a row of the same thread, agent and reference with `end_reason = import` existed, `dropped_late` when the thread has had a current session since the import and nothing was written (22, 9 step 6).'
        created_at:
          $ref: '#/components/schemas/Timestamp'
    GdprRequest:
      type: object
      required:
      - id
      - subject_user_id
      - kind
      - state
      - requested_by
      - reason
      - due_at
      - created_at
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        subject_user_id:
          $ref: '#/components/schemas/Uuid'
        kind:
          $ref: '#/components/schemas/GdprRequestKind'
        state:
          $ref: '#/components/schemas/GdprRequestState'
        requested_by:
          $ref: '#/components/schemas/Uuid'
        approved_by:
          $ref: '#/components/schemas/Uuid'
        reason:
          type: string
          examples: [Request by the person]
        due_at:
          $ref: '#/components/schemas/Timestamp'
        result_expires_at:
          $ref: '#/components/schemas/Timestamp'
        progress:
          $ref: '#/components/schemas/JsonObject'
        error:
          type: string
        created_at:
          $ref: '#/components/schemas/Timestamp'
        completed_at:
          $ref: '#/components/schemas/Timestamp'
    GdprRequestCreate:
      type: object
      additionalProperties: false
      required: [kind, reason]
      properties:
        subject_user_id:
          $ref: '#/components/schemas/Uuid'
          description: Absent means the caller. Others only for owners.
        kind:
          $ref: '#/components/schemas/GdprRequestKind'
        reason:
          type: string
          examples: [Request by the person]
    GdprDecision:
      type: object
      additionalProperties: false
      required: [decision, reason]
      properties:
        decision:
          type: string
          enum: [approve, reject]
        reason:
          type: string
          examples: [Identity verified]
    ViewAsSnapshot:
      type: object
      description: Read-only view of what a person can see, for owners (07, 9.2). Direct messages never appear.
      required: [user, spaces, channels, expires_at]
      properties:
        user:
          $ref: '#/components/schemas/PrincipalRef'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
          description: End of the view, `authz.view_as_max_duration` after its start.
        spaces:
          type: array
          items:
            $ref: '#/components/schemas/SpaceView'
        channels:
          type: array
          items:
            $ref: '#/components/schemas/ChannelView'
    EventScope:
      type: object
      description: Location of the event. Members are absent where not applicable.
      properties:
        space_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
    EventResource:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          examples: [message]
        id:
          $ref: '#/components/schemas/Uuid'
        version:
          type: integer
          format: int64
    EventActor:
      type: object
      required: [kind]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        kind:
          $ref: '#/components/schemas/ActorKind'
    EventEnvelope:
      type: object
      description: |
        Common envelope of every event. Each event schema adds `type` and its `payload`.
        Durable events carry `seq` and `permission_version`; ephemeral events carry neither. Delivery-only members (`invocation_token`, `redelivered`,
        `sync_epoch`) are set per delivery and never stored in the event log.
      required: [id, schema_version, occurred_at, scope, resource]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        seq:
          type: integer
          format: int64
          description: Global commit-ordered sequence; absent for ephemeral events.
          examples: [5301]
        schema_version:
          type: integer
          examples: [1]
        occurred_at:
          $ref: '#/components/schemas/Timestamp'
        actor:
          $ref: '#/components/schemas/EventActor'
        scope:
          $ref: '#/components/schemas/EventScope'
        resource:
          $ref: '#/components/schemas/EventResource'
        audience:
          type: string
          enum: [self]
        redacted:
          type: boolean
        truncated:
          type: boolean
          description: The payload was cut to `sync.event_payload_max_bytes`; load the resource via REST.
        correlation_id:
          $ref: '#/components/schemas/CorrelationId'
        trace_context:
          type: string
          description: W3C `traceparent` of the triggering request; only in deliveries to agent adapters and only if the request was recorded.
          examples: [00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01]
        client_msg_id:
          $ref: '#/components/schemas/Uuid'
        permission_version:
          type: integer
          format: int64
        invocation_token:
          type: string
          description: Fresh invocation token (JWS, I3); only on agent-addressed invocation events.
        redelivered:
          type: boolean
          description: Set on redelivery to an agent consumer after `agent.ack_timeout`.
        sync_epoch:
          $ref: '#/components/schemas/Uuid'
          description: Delivery-only member of outgoing webhooks; the time line of the event log (04, 7.5; ADR 0059). A receiver that sees a new epoch resets its `seq` state.
    MessagePayload:
      type: object
      required: [message]
      properties:
        message:
          $ref: '#/components/schemas/Message'
    MessageDeletedPayload:
      type: object
      required: [message_id, thread_id, thread_seq, deleted_at]
      properties:
        message_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        thread_seq:
          type: integer
          format: int64
        deleted_at:
          $ref: '#/components/schemas/Timestamp'
        deleted_by:
          $ref: '#/components/schemas/Uuid'
    ReactionPayload:
      type: object
      required: [message_id, thread_id, emoji, principal_id, count]
      properties:
        message_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        emoji:
          type: string
          examples: [👍]
        principal_id:
          $ref: '#/components/schemas/Uuid'
        count:
          type: integer
          description: Count for this emoji after the change.
    PinPayload:
      type: object
      required: [pin]
      properties:
        pin:
          $ref: '#/components/schemas/Pin'
    ThreadPayload:
      type: object
      required: [thread]
      properties:
        thread:
          $ref: '#/components/schemas/Thread'
    ThreadMovedPayload:
      type: object
      required: [thread, from_channel_id]
      properties:
        thread:
          $ref: '#/components/schemas/Thread'
        from_channel_id:
          $ref: '#/components/schemas/Uuid'
    ThreadMovedOutPayload:
      type: object
      description: Sent in the source channel. Does not name the target channel.
      required: [thread_id, channel_id]
      properties:
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
    AgentSessionChangedPayload:
      type: object
      required: [thread_id, agent_session]
      properties:
        thread_id:
          $ref: '#/components/schemas/Uuid'
        agent_session:
          $ref: '#/components/schemas/AgentSession'
    RunStateChangedPayload:
      type: object
      required: [run]
      properties:
        run:
          $ref: '#/components/schemas/RunSummary'
        triggering_message_id:
          $ref: '#/components/schemas/Uuid'
        trigger_state:
          $ref: '#/components/schemas/TriggerState'
    SpacePayload:
      type: object
      required: [space]
      properties:
        space:
          $ref: '#/components/schemas/Space'
    ChannelPayload:
      type: object
      required: [channel]
      properties:
        channel:
          $ref: '#/components/schemas/Channel'
    ChannelSettingsChangedPayload:
      type: object
      required: [channel_id, settings_version]
      properties:
        channel_id:
          $ref: '#/components/schemas/Uuid'
        settings_version:
          type: integer
          format: int64
    CommandChangedPayload:
      type: object
      required: [command, change]
      properties:
        command:
          $ref: '#/components/schemas/Command'
        change:
          type: string
          enum: [registered, updated, removed]
    FilePayload:
      type: object
      required: [file]
      properties:
        file:
          $ref: '#/components/schemas/File'
    UserPayload:
      type: object
      required: [user]
      properties:
        user:
          $ref: '#/components/schemas/User'
    AgentPayload:
      type: object
      required: [agent]
      properties:
        agent:
          $ref: '#/components/schemas/Agent'
    InstanceUpdatedPayload:
      type: object
      required: [changed_keys]
      properties:
        changed_keys:
          type: array
          description: Setting or flag keys that changed; values are loaded via REST.
          items:
            type: string
            examples: [branding.product_name]
    StatusUpdatedPayload:
      type: object
      required: [overall_state]
      properties:
        overall_state:
          $ref: '#/components/schemas/StatusComponentState'
        changed_component_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
        changed_incident_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
    GrantChangedPayload:
      type: object
      required: [grant, change]
      properties:
        grant:
          $ref: '#/components/schemas/Grant'
        change:
          type: string
          enum: [created, revoked, expired]
    GroupChangedPayload:
      type: object
      required: [group, change]
      properties:
        group:
          $ref: '#/components/schemas/Group'
        change:
          type: string
          description: '`member_expired`: a membership ended by expiry, reported once by the job `authz.expiry_sweep`.'
          enum: [created, updated, deleted, member_added, member_removed, member_expired]
        user_id:
          $ref: '#/components/schemas/Uuid'
    AccessChangedPayload:
      type: object
      required: [permission_version, added, removed, changed]
      properties:
        permission_version:
          type: integer
          format: int64
          examples: [418]
        added:
          type: array
          items:
            $ref: '#/components/schemas/AccessTarget'
        removed:
          type: array
          items:
            $ref: '#/components/schemas/AccessTarget'
        changed:
          type: array
          items:
            $ref: '#/components/schemas/AccessTarget'
    AccessExpiringPayload:
      type: object
      description: A grant or group membership ends within `authz.expiry_reminder_lead`.
      required: [kind, subject_id, expires_at]
      properties:
        kind:
          type: string
          enum: [grant, group_membership]
        grant_id:
          $ref: '#/components/schemas/Uuid'
        group_id:
          $ref: '#/components/schemas/Uuid'
        subject_id:
          $ref: '#/components/schemas/Uuid'
        target:
          $ref: '#/components/schemas/AccessTarget'
          description: Absent for a grant on the instance.
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    DevicePendingPayload:
      type: object
      description: A new device of the human waits for approval (R-AZ-31). Carries only IDs and the platform.
      required: [device_id, platform]
      properties:
        device_id:
          $ref: '#/components/schemas/Uuid'
        platform:
          $ref: '#/components/schemas/DevicePlatform'
    GdprRequestDuePayload:
      type: object
      description: An open privacy request reaches the reminder lead before its statutory deadline (R-AZ-35). Carries only the ID, the kind and the deadline.
      required: [request_id, kind, due_at]
      properties:
        request_id:
          $ref: '#/components/schemas/Uuid'
        kind:
          $ref: '#/components/schemas/GdprRequestKind'
        due_at:
          $ref: '#/components/schemas/Timestamp'
    SessionRevokedPayload:
      type: object
      required: [session_id, reason]
      properties:
        session_id:
          $ref: '#/components/schemas/Uuid'
        reason:
          type: string
          enum:
          - logout
          - admin
          - device_revoked
          - refresh_reuse
          - user_disabled
          - expired
          - inactive
          - step_up_failed
          - trusted_header_mismatch
    ReadStatePayload:
      type: object
      required: [read_state]
      properties:
        read_state:
          $ref: '#/components/schemas/ReadState'
    DraftPayload:
      type: object
      required: [draft]
      properties:
        draft:
          $ref: '#/components/schemas/Draft'
    DraftDeletedPayload:
      type: object
      required: [draft_id, channel_id]
      properties:
        draft_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
    SettingsPayload:
      type: object
      required: [setting]
      properties:
        setting:
          $ref: '#/components/schemas/UserSetting'
        deleted:
          type: boolean
    SidebarPayload:
      type: object
      required: [state]
      properties:
        state:
          $ref: '#/components/schemas/SidebarState'
    NotificationPrefPayload:
      type: object
      required: [preference]
      properties:
        preference:
          $ref: '#/components/schemas/NotificationPreference'
        deleted:
          type: boolean
    QuietHoursPayload:
      type: object
      required: [rules]
      properties:
        rules:
          type: array
          description: Complete list after the change, ascending by id.
          items:
            $ref: '#/components/schemas/QuietHoursRule'
    GdprRequestPayload:
      type: object
      required: [request]
      properties:
        request:
          $ref: '#/components/schemas/GdprRequest'
    ImportProgressPayload:
      type: object
      required: [import]
      properties:
        import:
          $ref: '#/components/schemas/ImportSource'
    AgentInvocationPayload:
      type: object
      description: Sent to the agent; the envelope carries a fresh `invocation_token` per delivery.
      required:
      - run_id
      - thread_id
      - channel_id
      - trigger_kind
      - billing_mode
      - triggered_by
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        space_id:
          $ref: '#/components/schemas/Uuid'
        trigger_kind:
          type: string
          enum: [message, interaction, command]
        message_id:
          $ref: '#/components/schemas/Uuid'
          description: Triggering message; the adapter reads its content with `run.context.read` (09, 4.6). Events outside resource `message` never carry message content (04, 2.2).
        interaction_id:
          $ref: '#/components/schemas/Uuid'
        command_invocation_id:
          $ref: '#/components/schemas/Uuid'
        command_id:
          $ref: '#/components/schemas/Uuid'
          description: Triggering command; set exactly for `trigger_kind = command`.
        command_name:
          type: string
          description: Name of the triggering command; set exactly for `trigger_kind = command`.
          examples: [escalate]
        options:
          type: array
          description: Parsed option values in the order of the command definition; set exactly for `trigger_kind = command`.
          items:
            $ref: '#/components/schemas/CommandOptionValue'
        triggered_by:
          $ref: '#/components/schemas/PrincipalRef'
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
        agent_session:
          $ref: '#/components/schemas/AgentSession'
          description: The session row of the run (09, 9.1); absent for `session_mode = new` without a row and for a guest run. With `started = false` the adapter reuses its `external_session_ref`; its `version` is the `If-Match` of the first `reportAgentSession`.
        session_mode:
          type: string
          enum: [new, resume, bind]
          description: '`session.mode` of 09, 9.1: `new` creates the external session (with the reference of `agent_session` when present), `resume` continues it, `bind` takes over a session that no run used yet (rebinding, import; 9.5). Present exactly for a run of the thread''s operator; a guest run has neither `session_mode` nor `agent_session` (09, 3.6).'
        settings_version:
          type: integer
          format: int64
          description: Channel settings version; reload `agent.config.read` when newer than cached.
    AgentFollowupPayload:
      type: object
      required: [run_id, thread_id, message_id]
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        message_id:
          $ref: '#/components/schemas/Uuid'
          description: Follow-up message; the adapter reads its content with `getMessage` (`message.read`); its author is `sub` of the invocation token of this delivery.
    RunCancelRequestedPayload:
      type: object
      required: [run_id, thread_id, reason]
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        reason:
          type: string
          enum: [user_cancelled, access_revoked, agent_disabled, user_disabled, channel_read_only]
        requested_by:
          $ref: '#/components/schemas/Uuid'
    InteractionCreatedPayload:
      type: object
      required: [interaction]
      properties:
        interaction:
          $ref: '#/components/schemas/Interaction'
    CommandInvokedPayload:
      type: object
      required:
      - invocation_id
      - command_id
      - command_name
      - control
      - channel_id
      - invoked_by
      - options
      properties:
        invocation_id:
          $ref: '#/components/schemas/Uuid'
        command_id:
          $ref: '#/components/schemas/Uuid'
        command_name:
          type: string
          examples: [escalate]
        control:
          type: boolean
        channel_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        run_id:
          $ref: '#/components/schemas/Uuid'
          description: Active run of the command owner in the thread at execution time; absent when the owner has none
            (R-AZ-25). Never the run of another agent. Always absent for app commands, because apps have no runs.
        invoked_by:
          $ref: '#/components/schemas/PrincipalRef'
        options:
          type: array
          items:
            $ref: '#/components/schemas/CommandOptionValue'
    AgentConfigChangedPayload:
      type: object
      required: [channel_id, settings_version]
      properties:
        channel_id:
          $ref: '#/components/schemas/Uuid'
        settings_version:
          type: integer
          format: int64
    TypingStartedPayload:
      type: object
      required: [thread_id, principal_id, expires_at]
      properties:
        thread_id:
          $ref: '#/components/schemas/Uuid'
        principal_id:
          $ref: '#/components/schemas/Uuid'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    RunUpdatedPayload:
      type: object
      description: Coalesced live-box patch. Either `ops` (from `base_version` to `version`) or a full `snapshot`.
      required: [run_id, version]
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        base_version:
          type: integer
          format: int64
        version:
          type: integer
          format: int64
        ops:
          type: array
          items:
            $ref: '#/components/schemas/JsonPatchOperation'
        snapshot:
          $ref: '#/components/schemas/Run'
    AgentPresencePayload:
      type: object
      required: [agent_id, state]
      properties:
        agent_id:
          $ref: '#/components/schemas/Uuid'
        state:
          $ref: '#/components/schemas/AgentRuntimeState'
        reported_billing_mode:
          $ref: '#/components/schemas/BillingMode'
    CommandAutocompleteRequestedPayload:
      type: object
      description: Sent to one gateway connection of the command owner (04, 15.7); agents get an invocation token with `trigger_kind = autocomplete` in the delivery fields.
      required:
      - request_id
      - command_id
      - channel_id
      - user_id
      - option
      - value
      - expires_at
      properties:
        request_id:
          $ref: '#/components/schemas/Uuid'
        command_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        user_id:
          $ref: '#/components/schemas/Uuid'
        option:
          type: string
          examples: [ticket]
        value:
          type: string
          examples: ['47']
        options:
          type: array
          items:
            $ref: '#/components/schemas/CommandOptionValue'
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    EventPage:
      type: object
      required: [events, next_cursor, head_seq, sync_epoch]
      properties:
        events:
          type: array
          items:
            $ref: '#/components/schemas/Event'
        next_cursor:
          type: integer
          format: int64
          description: Highest checked `seq`; use as `after` for the next call even if `events` is empty.
        head_seq:
          type: integer
          format: int64
        sync_epoch:
          $ref: '#/components/schemas/Uuid'
          description: Time line of the event log; a client stores it with its cursor (04, 7.5).
    GatewayHello:
      type: object
      required:
      - heartbeat_interval_ms
      - resume_window_ms
      - protocol_versions
      - dpop_nonce
      - server_time
      - sync_epoch
      properties:
        heartbeat_interval_ms:
          type: integer
          format: int64
          examples: [30000]
        resume_window_ms:
          type: integer
          format: int64
          examples: [300000]
        protocol_versions:
          type: array
          items:
            type: integer
            examples: [1]
        dpop_nonce:
          type: string
          examples: [eyJ7S_zG.eyJH0-Z.HX4w-7v]
        server_time:
          $ref: '#/components/schemas/Timestamp'
        sync_epoch:
          $ref: '#/components/schemas/Uuid'
          description: Time line of the event log; a client stores it with its cursor (04, 7.5).
    GatewayClientInfo:
      type: object
      additionalProperties: false
      required: [platform, version]
      properties:
        platform:
          $ref: '#/components/schemas/DevicePlatform'
        version:
          type: string
          examples: [1.4.0]
    GatewayIdentify:
      type: object
      additionalProperties: false
      description: |
        People send `access_token`, `dpop`, `device_id`; agents and apps send `api_token`, `consumer`
        and `dpop` when the token is DPoP-bound.
      required: [protocol_version]
      properties:
        protocol_version:
          type: integer
          examples: [1]
        access_token:
          type: string
        api_token:
          type: string
        dpop:
          type: string
          description: DPoP proof with `htm = GET`, `htu` = HTTPS form of the gateway URL, `nonce` from `hello`, `ath`.
        device_id:
          $ref: '#/components/schemas/Uuid'
        consumer:
          type: string
          pattern: ^[a-z0-9][a-z0-9_-]*$
          examples: [default]
        cursor:
          type: integer
          format: int64
          description: Last processed `seq`; absent on first connect.
        sync_epoch:
          $ref: '#/components/schemas/Uuid'
          description: Epoch stored with the cursor; absent on first connect. A mismatch gives `resync_required` with `epoch_changed`.
        client:
          $ref: '#/components/schemas/GatewayClientInfo'
    GatewayReady:
      type: object
      required:
      - gateway_session_id
      - principal
      - permission_version
      - head_seq
      - snapshot
      - sync_epoch
      properties:
        gateway_session_id:
          $ref: '#/components/schemas/Uuid'
        principal:
          $ref: '#/components/schemas/PrincipalRef'
        device_id:
          $ref: '#/components/schemas/Uuid'
        permission_version:
          type: integer
          format: int64
        head_seq:
          type: integer
          format: int64
        snapshot:
          $ref: '#/components/schemas/Snapshot'
        sync_epoch:
          $ref: '#/components/schemas/Uuid'
          description: Time line of the event log; a client stores it with its cursor (04, 7.5).
    GatewayResume:
      type: object
      additionalProperties: false
      required: [gateway_session_id, cursor]
      properties:
        gateway_session_id:
          $ref: '#/components/schemas/Uuid'
        access_token:
          type: string
        api_token:
          type: string
        dpop:
          type: string
        cursor:
          type: integer
          format: int64
        sync_epoch:
          $ref: '#/components/schemas/Uuid'
    GatewayResumed:
      type: object
      required: [gateway_session_id, head_seq, sync_epoch]
      properties:
        gateway_session_id:
          $ref: '#/components/schemas/Uuid'
        head_seq:
          type: integer
          format: int64
        sync_epoch:
          $ref: '#/components/schemas/Uuid'
          description: Time line of the event log; a client stores it with its cursor (04, 7.5).
    GatewayHeartbeat:
      type: object
      additionalProperties: false
      required: [last_seq]
      properties:
        last_seq:
          type: integer
          format: int64
        agent:
          $ref: '#/components/schemas/Heartbeat'
          description: Agents only; equivalent to `POST /agents/me/heartbeat`. The liveness rules of the op run first (`heartbeat_ack`); then the agent part passes `rl.agent_runtime` and `agent.heartbeat`. Its errors (`forbidden`, `validation_failed`, `rate_limited`, `internal_error`) answer `error` and keep the connection. A heartbeat of an agent without this member changes no presence.
    GatewayHeartbeatAck:
      type: object
      required: [server_time]
      properties:
        server_time:
          $ref: '#/components/schemas/Timestamp'
    GatewayReauth:
      type: object
      additionalProperties: false
      required: [access_token, dpop]
      properties:
        access_token:
          type: string
        dpop:
          type: string
    GatewayReauthRequired:
      type: object
      required: [expires_at, dpop_nonce]
      properties:
        expires_at:
          $ref: '#/components/schemas/Timestamp'
        dpop_nonce:
          type: string
    GatewayFocus:
      type: object
      additionalProperties: false
      required: [thread_ids]
      properties:
        thread_ids:
          type: array
          description: Complete set of focused threads; replaces the previous set. More than `sync.max_focused_threads` ids answer `error` with `validation_failed` and keep the previous set.
          items:
            $ref: '#/components/schemas/Uuid'
        idle:
          type: boolean
          description: True when the window is open but unused; live-box patches continue, but the person does not count as watching (13, 4.4).
        background_thread_ids:
          type: array
          description: Threads whose runs the app shows without their messages (control room cards, a run graph opened from a card). They get the run document and `run.updated` but no typing, never count as "Active in the thread" and are not part of the presence focus (13 4.4). Unreadable threads are ignored as in `thread_ids`; a thread in both lists counts as in `thread_ids`; both lists together hold at most `sync.max_focused_threads` distinct threads (04 6.2, R-SY-27).
          items:
            $ref: '#/components/schemas/Uuid'
    GatewayTyping:
      type: object
      additionalProperties: false
      required: [thread_id]
      properties:
        thread_id:
          $ref: '#/components/schemas/Uuid'
    GatewayAck:
      type: object
      additionalProperties: false
      required: [seq]
      properties:
        seq:
          type: integer
          format: int64
          minimum: 0
    GatewayRunUpdate:
      type: object
      additionalProperties: false
      required: [run_id, base_version, ops]
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        base_version:
          type: integer
          format: int64
        ops:
          type: array
          items:
            $ref: '#/components/schemas/JsonPatchOperation'
        ref:
          type: string
          description: Client reference echoed in `error` frames for this op.
    GatewayAutocompleteResult:
      type: object
      additionalProperties: false
      description: Answer of the command owner to `command.autocomplete_requested` (action `command.autocomplete.respond`).
      required: [request_id, choices]
      properties:
        request_id:
          $ref: '#/components/schemas/Uuid'
        choices:
          type: array
          maxItems: 25
          description: At most `command.autocomplete_max_choices`.
          items:
            $ref: '#/components/schemas/CommandChoice'
        ref:
          type: string
          description: Client reference echoed in `error` frames for this op.
    GatewayCursor:
      type: object
      required: [seq]
      properties:
        seq:
          type: integer
          format: int64
    GatewayCatchupComplete:
      type: object
      required: [head_seq]
      properties:
        head_seq:
          type: integer
          format: int64
    GatewayResyncRequired:
      type: object
      required: [reason, head_seq, sync_epoch]
      properties:
        reason:
          type: string
          enum: [epoch_changed, retention, too_far_behind, cursor_ahead]
        head_seq:
          type: integer
          format: int64
        sync_epoch:
          $ref: '#/components/schemas/Uuid'
          description: Time line of the event log; a client stores it with its cursor (04, 7.5).
    GatewayError:
      type: object
      required: [op, code]
      properties:
        op:
          type: string
          examples: [focus]
        code:
          type: string
          description: Same codes as REST problem types.
          examples: [rate_limited]
        detail:
          type: string
        ref:
          type: string
    PageLimit:
      type: integer
      minimum: 1
      description: Page size. Default `api.page_size_default`; larger values than `api.page_size_max` are capped.
    ImageUpload:
      type: object
      additionalProperties: false
      required: [file]
      description: Image as the one multipart part `file` (a further part is rejected with 400 `validation_failed`). The server detects the type from the bytes; it accepts only JPEG and PNG, whose metadata it removes losslessly, and still WebP, which it converts to PNG or JPEG without metadata. Thumbnails are re-encoded. Any other detected type ends the file `failed` with `file_type_denied`.
      properties:
        file:
          type: string
          format: binary
    IncomingWebhookResult:
      type: object
      required: [message_id, thread_id]
      properties:
        message_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
          description: |-
            The thread the message landed in; with a `title` in a text channel the new thread that
            starts from the message, which itself stands in the main thread.
    StatsMetric:
      type: string
      description: Metric of the own measurement in `stats_minutes` (docs/spec/25-visualization.md, section 3.2).
      enum:
      - messages_created
      - threads_created
      - runs_started
      - runs_finished
      - tool_calls
      - approval_cards
      - cost
      - invocations_denied
      - billing_mode_switches
      - catchups
      - resyncs
      - slow_consumer_closes
      - sli.api_availability
      - sli.delivery_latency
      - sli.reconnect
      - sli.push_acceptance
      - sli.run_start
      - sli.turn_duration
    StatsScope:
      type: object
      required: [kind]
      properties:
        kind:
          type: string
          enum: [instance, space]
        space_id:
          $ref: '#/components/schemas/Uuid'
    AgentRateLimitState:
      type: object
      additionalProperties: false
      description: Rate-limit state of the agent runtime at its provider as reported by the adapter (`agent_heartbeat.v1`). Never quotas of an account, keys or subscription names.
      required: [state]
      properties:
        state:
          type: string
          enum: [ok, near_limit, limited, unknown]
        resets_at:
          $ref: '#/components/schemas/Timestamp'
    StatsLiveStep:
      type: object
      description: Current step of a run; only kind and name (for tools the tool name), never title or detail.
      required: [kind, name]
      properties:
        kind:
          type: string
          enum: [tool_call, status, subagent, workflow, background_job]
        name:
          type: string
          examples: [Bash]
    StatsLiveRun:
      type: object
      required: [run_id, thread_id, channel_id, space_id, agent_id, state, queued_at, billing_mode, approval_pending]
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        space_id:
          $ref: '#/components/schemas/Uuid'
        agent_id:
          $ref: '#/components/schemas/Uuid'
        state:
          type: string
          enum: [queued, running, waiting_input]
        queued_at:
          $ref: '#/components/schemas/Timestamp'
        started_at:
          $ref: '#/components/schemas/Timestamp'
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
          description: Mode used by the run, else the configured mode.
        step:
          $ref: '#/components/schemas/StatsLiveStep'
        approval_pending:
          type: boolean
          description: An approval card of this run waits for a decision.
        cost_micros:
          type: integer
          format: int64
          description: Cost so far; only with `cost.read.aggregate`.
        currency:
          type: string
          examples: [USD]
        cost_is_estimate:
          type: boolean
    StatsLiveAgent:
      type: object
      required: [agent_id, presence, billing_mode]
      properties:
        agent_id:
          $ref: '#/components/schemas/Uuid'
        presence:
          $ref: '#/components/schemas/AgentRuntimeState'
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
        reported_billing_mode:
          $ref: '#/components/schemas/BillingMode'
          description: Absent when the adapter follows the configured mode per run.
        rate_limit:
          $ref: '#/components/schemas/AgentRateLimitState'
    StatsLiveRate:
      type: object
      description: Sum of `count` (and `sum` where meaningful) of one metric over the last full minute and the current minute up to `at`.
      required: [metric, last_minute, current_minute]
      properties:
        metric:
          $ref: '#/components/schemas/StatsMetric'
        dimension:
          type: string
          description: Dimension code; absent means all dimensions summed.
        last_minute:
          type: integer
          format: int64
        current_minute:
          type: integer
          format: int64
        last_minute_sum:
          type: integer
          format: int64
          description: Sum of values (milliseconds or micro units) of the last full minute.
        currency:
          type: string
          description: Only for metric `cost`.
    StatsLiveSli:
      type: object
      required: [metric, total, good]
      properties:
        metric:
          $ref: '#/components/schemas/StatsMetric'
        total:
          type: integer
          format: int64
        good:
          type: integer
          format: int64
        ratio:
          type: number
          format: double
          description: '`good / total`; absent when `total` is 0.'
    StatsLiveConnections:
      type: object
      description: Gateway connections of this node; only with `cluster.mode = single`.
      required: [user, agent, app]
      properties:
        user:
          type: integer
        agent:
          type: integer
        app:
          type: integer
    StatsLiveFrame:
      type: object
      description: Live frame (docs/spec/25-visualization.md, section 5.2). No person values, no content.
      required: [at, interval_ms, scope, runs, runs_truncated, agents, rates]
      properties:
        at:
          $ref: '#/components/schemas/Timestamp'
        interval_ms:
          type: integer
          examples: [1000]
        scope:
          $ref: '#/components/schemas/StatsScope'
        runs:
          type: array
          items:
            $ref: '#/components/schemas/StatsLiveRun'
        runs_truncated:
          type: boolean
        agents:
          type: array
          items:
            $ref: '#/components/schemas/StatsLiveAgent'
        rates:
          type: array
          items:
            $ref: '#/components/schemas/StatsLiveRate'
        sli:
          type: array
          description: Only for the instance scope.
          items:
            $ref: '#/components/schemas/StatsLiveSli'
        connections:
          $ref: '#/components/schemas/StatsLiveConnections'
    StatsTimelineBucket:
      type: object
      description: One row of `stats_minutes`.
      required: [start, metric, dimension, count, sum, good]
      properties:
        start:
          $ref: '#/components/schemas/Timestamp'
        metric:
          $ref: '#/components/schemas/StatsMetric'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        agent_id:
          $ref: '#/components/schemas/Uuid'
        dimension:
          type: string
          description: Code from the closed list per metric; empty string when the metric has none.
        currency:
          type: string
        count:
          type: integer
          format: int64
        sum:
          type: integer
          format: int64
        good:
          type: integer
          format: int64
    StatsTimelineRun:
      type: object
      required: [run_id, channel_id, agent_id, queued_at, state]
      properties:
        run_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        agent_id:
          $ref: '#/components/schemas/Uuid'
        queued_at:
          $ref: '#/components/schemas/Timestamp'
        started_at:
          $ref: '#/components/schemas/Timestamp'
        finished_at:
          $ref: '#/components/schemas/Timestamp'
        state:
          $ref: '#/components/schemas/RunState'
    StatsTimeline:
      type: object
      required: [from, to, buckets, runs, runs_truncated]
      properties:
        from:
          $ref: '#/components/schemas/Timestamp'
        to:
          $ref: '#/components/schemas/Timestamp'
        buckets:
          type: array
          items:
            $ref: '#/components/schemas/StatsTimelineBucket'
        runs:
          type: array
          description: Runs of the visible channels whose interval from `queued_at` to `finished_at` (now for an unfinished run) intersects the range, ordered by `queued_at`, then `run_id`; at most `stats.timeline_max_runs`.
          items:
            $ref: '#/components/schemas/StatsTimelineRun'
        runs_truncated:
          type: boolean
          description: True when more runs intersect the range than `runs` holds.
    StatsSloState:
      type: string
      enum: [green, amber, red, insufficient_data]
      description: Traffic light of one SLO (docs/spec/25-visualization.md, section 6.4).
    StatsSloEntry:
      type: object
      required: [slo, metric, objective, period_days, total, good, state]
      properties:
        slo:
          type: string
          description: '`service/name` of the Sloth file.'
          examples: [bothy-sync/delivery-latency]
        metric:
          $ref: '#/components/schemas/StatsMetric'
        objective:
          type: number
          format: double
          description: Objective in percent from the Sloth file.
        threshold_ms:
          type: integer
          description: Threshold of a latency SLO; absent for ratio SLOs.
        period_days:
          type: integer
        total:
          type: integer
          format: int64
        good:
          type: integer
          format: int64
        ratio:
          type: number
          format: double
        budget_remaining:
          type: number
          format: double
          description: Remaining error budget, `1 - (1 - ratio) / (1 - objective/100)`; may be negative.
        state:
          $ref: '#/components/schemas/StatsSloState'
    StatsSloReport:
      type: object
      required: [at, measured_by, items]
      properties:
        at:
          $ref: '#/components/schemas/Timestamp'
        measured_by:
          type: string
          enum: [own]
          description: Always `own`; the values come from the own measurement, not from Prometheus.
        items:
          type: array
          items:
            $ref: '#/components/schemas/StatsSloEntry'
    StatsBackendCost:
      type: object
      required: [billing_mode, currency, amount_micros, is_estimate]
      properties:
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
        currency:
          type: string
        amount_micros:
          type: integer
          format: int64
        is_estimate:
          type: boolean
    StatsBackendDenials:
      type: object
      required: [i2_denied, billing_mode_mismatch]
      properties:
        i2_denied:
          type: integer
          format: int64
        billing_mode_mismatch:
          type: integer
          format: int64
    StatsBackendAgent:
      type: object
      required: [agent_id, billing_mode, billing_modes_supported, switches, invocations_denied]
      properties:
        agent_id:
          $ref: '#/components/schemas/Uuid'
        billing_mode:
          $ref: '#/components/schemas/BillingMode'
        reported_billing_mode:
          $ref: '#/components/schemas/BillingMode'
        billing_modes_supported:
          type: array
          items:
            $ref: '#/components/schemas/BillingMode'
        switches:
          type: integer
          format: int64
          description: Billing mode switches in the range.
        invocations_denied:
          $ref: '#/components/schemas/StatsBackendDenials'
        costs:
          type: array
          description: Only with `cost.read.aggregate`.
          items:
            $ref: '#/components/schemas/StatsBackendCost'
        rate_limit:
          $ref: '#/components/schemas/AgentRateLimitState'
    StatsBackendReport:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/StatsBackendAgent'
    GatewayStatsSubscribe:
      type: object
      additionalProperties: false
      description: Subscribe to `stats.live`; without `space_id` the instance (owner). Replaces an earlier subscription of the connection.
      properties:
        space_id:
          $ref: '#/components/schemas/Uuid'
    GatewayStatsUnsubscribe:
      type: object
      additionalProperties: false
      description: End the `stats.live` subscription of the connection.
    StatsLiveEvent:
      description: Ephemeral event `stats.live` (no `seq`, at most once), only to connections with a subscription.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [stats.live]
          payload:
            $ref: '#/components/schemas/StatsLiveFrame'
    StatsGranularity:
      type: string
      enum: [hour, day, week, month]
      description: Size of a statistics bucket.
    IdentityProviderKind:
      type: string
      enum: [oidc, trusted_header, local]
      description: Kind of identity provider (B9).
    ForumSort:
      type: string
      enum: [last_activity, created]
      description: Sort order of forum posts.
    AgentRuntimeState:
      type: string
      enum: [unknown, online, degraded, offline]
      description: State derived from heartbeats.
    SigningKeyPurpose:
      type: string
      description: Purpose of a server signing key (03 `server_signing_keys.purpose`). `terminal` signs terminal session tokens (24 section 5.3); `audit_checkpoint` signs audit checkpoints (19 SEC-AUD-7).
      enum: [access_token, invocation, terminal, webhook_out, audit_checkpoint]
    UserStatus:
      type: string
      enum: [pending, active, disabled, deleted]
    GroupSource:
      type: string
      enum: [local, oidc_claim]
    LevelOrNone:
      type: string
      enum: [none, read, write, trigger]
      description: Level including `none` for no access.
    AccessTargetKind:
      type: string
      description: '`host` appears only in `access.changed` and `access.expiring` (terminal capability, no level).'
      enum: [space, channel, host]
    OperatorMode:
      type: string
      enum: [inherit, none, agent]
      description: '`inherit` takes the default operator of the space (channels only).'
    ThreadState:
      type: string
      enum: [open, closed]
    TranscriptStatus:
      type: string
      enum: [none, pending, done, failed]
    TranscriptSource:
      type: string
      enum: [agent, service]
    SearchHitKind:
      type: string
      enum: [message, thread, file]
    PushProvider:
      type: string
      enum: [apns, fcm, unifiedpush, wns]
    PushEnvironment:
      type: string
      enum: [production, sandbox]
    ComponentType:
      type: string
      enum: [button, select, form]
    ComponentRestriction:
      type: string
      enum: [any, run_starter, owner]
      description: '`run_starter`: only the person who started the run, or an owner. `owner`: only owners.'
    CommandLevel:
      type: string
      enum: [write, trigger]
    WebhookDeliveryStatus:
      type: string
      enum: [pending, succeeded, failed, dead]
    AuditDecision:
      type: string
      enum: [allow, deny, error]
    PriceUnit:
      type: string
      enum:
      - input_mtok
      - output_mtok
      - cache_read_mtok
      - cache_write_mtok
      - cache_write_1h_mtok
      - request
    StatusIncidentKind:
      type: string
      enum: [incident, maintenance]
    StatusIncidentImpact:
      type: string
      enum: [none, minor, major, critical]
    ImportKind:
      type: string
      enum: [discord, archive]
    ImportItemState:
      type: string
      enum: [pending, posted, skipped, failed]
    ImportObject:
      type: object
      additionalProperties: false
      required: [origin_key, item_kind, data]
      properties:
        origin_key:
          type: string
          examples: ['message:1029384756']
        item_kind:
          type: string
          enum: [space, channel, forum_tag, thread, message, user, pin]
        content_sha256:
          type: string
          description: Hex SHA-256 of the converted content; a different value on a later run is counted as changed at the source.
        data:
          $ref: '#/components/schemas/JsonObject'
          description: Fields of the target object, schema `import_object.<item_kind>.v1` (22, sections 5 and 6). Structure items use `ImportSpaceData` (`space`), `ImportChannelData` (`channel`), `ImportForumTagData` (`forum_tag`) and `ImportUserData` (`user`); content items `ImportThreadData` (`thread`), `ImportMessageData` (`message`) and `ImportPinData` (`pin`).
    ImportBatch:
      type: object
      additionalProperties: false
      description: One transaction. Objects of one thread, in source order; structure objects (space, channel, forum tag, person) come in batches without `thread_origin_key`.
      required: [items]
      properties:
        thread_origin_key:
          type: string
          description: '`thread:<id>` for a thread or forum post, `channel:<id>` for the main thread of a text channel; absent for a structure batch.'
          examples: ['thread:1029384700']
        items:
          type: array
          minItems: 1
          maxItems: 100
          description: At most `import.batch_max_items`.
          items:
            $ref: '#/components/schemas/ImportObject'
    ImportBatchResult:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ImportItem'
        unread_floor_seq:
          type: integer
          format: int64
          description: New `threads.unread_floor_seq` of the thread of this batch.
    ImportFileCreate:
      type: object
      additionalProperties: false
      required: [origin_key, channel_id, author_id, name, size_bytes, sha256]
      properties:
        origin_key:
          type: string
          examples: ['file:1029384799']
        channel_id:
          $ref: '#/components/schemas/Uuid'
        author_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [report.pdf]
        size_bytes:
          type: integer
          format: int64
          minimum: 1
        sha256:
          type: string
          pattern: '^[0-9a-f]{64}$'
          description: Lowercase hex SHA-256 of the bytes (`file_upload_sessions.declared_sha256`); the scan checks the bytes against it.
        declared_mime_type:
          type: string
          examples: [application/pdf]
    ImportFileSlot:
      type: object
      description: The `pending` item reserves the file ID; a repeated call with the same key returns the same ID (22, 7.2).
      required: [item]
      properties:
        item:
          $ref: '#/components/schemas/ImportItem'
        upload:
          $ref: '#/components/schemas/UploadSession'
          description: Absent when the file is already `posted`. Its part URLs name `uploadImportFilePart`.
    ImportThreadData:
      type: object
      additionalProperties: false
      description: Data of a `thread` item (22, 5.1.1). The title is `name` cut to `thread.title_max_chars`, `title_source` is `auto` for a placeholder name and `manual` otherwise; the operator is the planned operator of the channel in the mapping; `parent_message_id` is the starter message only when its item is posted in the main thread of the same channel; the tags are the posted `forum_tag` items of the same channel. A posted thread only follows the mirror rules of 22 12.2 step 3.
      required: [channel_origin_key, name, created_at, last_activity_at, closed]
      properties:
        channel_origin_key:
          type: string
          examples: ['channel:1029384600']
        name:
          type: string
          examples: [Login fails on mobile]
        created_at:
          $ref: '#/components/schemas/Timestamp'
        last_activity_at:
          $ref: '#/components/schemas/Timestamp'
        closed:
          type: boolean
          description: The archive rule of the tool (22 5.1.1, R-IM-11).
        closed_at:
          $ref: '#/components/schemas/Timestamp'
          description: Required exactly when `closed` is true.
        starter_message_origin_key:
          type: string
          examples: ['message:1029384700']
        tag_origin_keys:
          type: array
          items:
            type: string
            examples: ['forum_tag:1029384600:77']
        earlier_session:
          $ref: '#/components/schemas/ImportEarlierSession'
    ImportEarlierSession:
      type: object
      additionalProperties: false
      description: 'The agent session the thread had before the migration (22, 9 step 1; `--sessions` of the tool). The server writes one earlier `agent_sessions` row (`is_current = false`, `started = true`, `ended_at` = the time of the batch, `end_reason = import`, the cost baseline) exactly once per thread, agent and reference, also for a thread posted by an earlier run, and never after the thread has had a current session; the item member `session` names the outcome. `agent_id` must be the operator of the thread (`threads.operator_agent_id`), else `400 validation_failed` at `/items/<i>/data/earlier_session/agent_id`. The reference is opaque, at most `agent.session_ref_max_bytes` bytes, and is never logged.'
      required: [agent_id, external_session_ref]
      properties:
        agent_id:
          $ref: '#/components/schemas/Uuid'
        external_session_ref:
          type: string
          minLength: 1
          examples: [sess-7f3a]
        cost_baseline_micros:
          type: integer
          format: int64
          minimum: 0
          maximum: 9007199254740991
          description: The cumulative amount of the external session before the binding, in micro units of `currency` (22, 9 step 7); absent means 0.
          examples: [1250000]
        currency:
          type: string
          pattern: '^[A-Z]{3}$'
          description: ISO 4217 code of `cost_baseline_micros`; required when it is above 0.
          examples: [USD]
    ImportMessageData:
      type: object
      additionalProperties: false
      description: Data of a `message` item (22, 6.2). The message gets `origin = import`, `kind = text`, `silent`, no mentions and no `trigger_state`; `content_md` is stored as given (at most `message.max_content_bytes`, never split); the reply target must be a posted message of the same thread; the files must be posted file items of this source uploaded for the channel of the thread by the author, at most `message.max_attachments`.
      required: [author_origin_key, content_md, created_at]
      properties:
        author_origin_key:
          type: string
          examples: ['user:1029384000']
        content_md:
          type: string
          examples: [The invoice export fails.]
        created_at:
          $ref: '#/components/schemas/Timestamp'
        edited_at:
          $ref: '#/components/schemas/Timestamp'
        reply_to_origin_key:
          type: string
          examples: ['message:1029384701']
        attachment_file_ids:
          type: array
          items:
            $ref: '#/components/schemas/Uuid'
    ImportPinData:
      type: object
      additionalProperties: false
      description: Data of a `pin` item (22, 6.6); `pinned_by` is the importing owner.
      required: [message_origin_key]
      properties:
        message_origin_key:
          type: string
          examples: ['message:1029384756']
    ImportCursorSet:
      type: object
      additionalProperties: false
      required: [cursor]
      properties:
        cursor:
          $ref: '#/components/schemas/JsonObject'
          description: Progress per source channel and thread; only after a successful batch.
        stats:
          $ref: '#/components/schemas/JsonObject'
    ImportLeadSet:
      type: object
      additionalProperties: false
      required: [source_channel, lead]
      properties:
        source_channel:
          type: string
          description: ID of the channel in the source.
          examples: ['1029384000']
        lead:
          type: string
          enum: [discord, bothy]
          description: '`discord`: the source leads, the channel here is read-only and mirrored; `bothy`: this server leads (22, 12.1).'
    ImportSourceDiscordConfig:
      type: object
      additionalProperties: false
      description: Mapping file of schema `import_source.discord.v1` as JSON (22, 5.2); the web console converts the YAML of the owner. It holds no secret.
      required: [version, guild_id, defaults]
      properties:
        version:
          type: integer
          enum: [1]
        guild_id:
          $ref: '#/components/schemas/ImportDiscordId'
        defaults:
          type: object
          additionalProperties: false
          required: [unmapped_human, unmapped_bot, uncategorized_space]
          properties:
            unmapped_human:
              type: string
              enum: [placeholder, skip]
            unmapped_bot:
              type: string
              enum: [placeholder, skip]
            uncategorized_space:
              type: string
              description: '`skip` or the `discord_category` of an entry in `spaces`.'
              examples: [skip]
        spaces:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [discord_category, target]
            properties:
              discord_category:
                $ref: '#/components/schemas/ImportDiscordId'
              target:
                $ref: '#/components/schemas/ImportMappingTarget'
        channels:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [discord_channel, target]
            properties:
              discord_channel:
                $ref: '#/components/schemas/ImportDiscordId'
              target:
                $ref: '#/components/schemas/ImportMappingTarget'
              planned_operator:
                type: string
                description: '`agent:<handle>` of an active agent or `none` (default).'
                examples: ['agent:lotse']
              lead:
                type: string
                enum: [discord, bothy]
                default: discord
        users:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [discord_user, target]
            properties:
              discord_user:
                $ref: '#/components/schemas/ImportDiscordId'
              target:
                type: string
                description: '`user:<handle>`, `agent:<handle>` or `placeholder`.'
                examples: ['user:alex']
    ImportDiscordId:
      type: string
      pattern: '^[0-9]+$'
      description: A Discord snowflake as a string of digits.
      examples: ['1029384000']
    ImportMappingTarget:
      description: 'Exactly one of the string `skip`, `{create: {name}}` or `{existing: <uuid>}` (22, 5.2). The server checks the form and reports a violation at the pointer of the member.'
    ImportSpaceData:
      type: object
      additionalProperties: false
      description: '`data` of a `space` item (origin key `space:<category_id>`). Exactly one of `name` (a new space as the mapping says `create`) and `existing_space_id` (equal to the mapping''s `existing`).'
      required: [sort_key]
      properties:
        name:
          type: string
        sort_key:
          type: string
          description: Fractional index key of the space order, derived by the tool from the Discord `position`.
          examples: [a0]
        existing_space_id:
          $ref: '#/components/schemas/Uuid'
    ImportChannelData:
      type: object
      additionalProperties: false
      description: '`data` of a `channel` item (origin key `channel:<channel_id>`). The space is the target of the posted item `space_origin_key` or, without it, of the category named in `defaults.uncategorized_space`; `existing_space_id` and `existing_channel_id` must equal the stored mapping. `read_only` is not a member: the server derives it from the stored `lead`.'
      required: [kind, sort_key]
      properties:
        space_origin_key:
          type: string
          examples: ['space:1029384001']
        existing_space_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
        kind:
          type: string
          enum: [text, forum]
        topic:
          type: string
        sort_key:
          type: string
          description: Fractional index key of the order in the space.
          examples: [a0]
        existing_channel_id:
          $ref: '#/components/schemas/Uuid'
    ImportForumTagData:
      type: object
      additionalProperties: false
      description: '`data` of a `forum_tag` item (origin key `forum_tag:<channel_id>:<tag_id>`). A tag with the same name, without regard to case, is adopted (22, 6.6). `emoji` is accepted and not stored (forum tags have no emoji).'
      required: [channel_origin_key, name]
      properties:
        channel_origin_key:
          type: string
          examples: ['channel:1029384000']
        name:
          type: string
        emoji:
          type: string
    ImportUserData:
      type: object
      additionalProperties: false
      description: '`data` of a `user` item (origin key `user:<user_id>`). The target comes from the stored mapping and its `defaults`; a webhook author is always a placeholder (22, 5.3).'
      required: [discord_kind, username]
      properties:
        discord_kind:
          type: string
          enum: [user, bot, webhook]
        username:
          type: string
        global_name:
          type: string
    GdprRequestKind:
      type: string
      enum: [export, deletion, rectification]
    GdprRequestState:
      type: string
      enum:
      - requested
      - approved
      - running
      - completed
      - failed
      - rejected
      - cancelled
    MessageCreatedEvent:
      description: Durable event `message.created`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [message.created]
          payload:
            $ref: '#/components/schemas/MessagePayload'
    MessageUpdatedEvent:
      description: Durable event `message.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [message.updated]
          payload:
            $ref: '#/components/schemas/MessagePayload'
    MessageDeletedEvent:
      description: Durable event `message.deleted`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [message.deleted]
          payload:
            $ref: '#/components/schemas/MessageDeletedPayload'
    ReactionAddedEvent:
      description: Durable event `reaction.added`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [reaction.added]
          payload:
            $ref: '#/components/schemas/ReactionPayload'
    ReactionRemovedEvent:
      description: Durable event `reaction.removed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [reaction.removed]
          payload:
            $ref: '#/components/schemas/ReactionPayload'
    PinAddedEvent:
      description: Durable event `pin.added`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [pin.added]
          payload:
            $ref: '#/components/schemas/PinPayload'
    PinRemovedEvent:
      description: Durable event `pin.removed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [pin.removed]
          payload:
            $ref: '#/components/schemas/PinPayload'
    ThreadCreatedEvent:
      description: Durable event `thread.created`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [thread.created]
          payload:
            $ref: '#/components/schemas/ThreadPayload'
    ThreadUpdatedEvent:
      description: Durable event `thread.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [thread.updated]
          payload:
            $ref: '#/components/schemas/ThreadPayload'
    ThreadClosedEvent:
      description: Durable event `thread.closed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [thread.closed]
          payload:
            $ref: '#/components/schemas/ThreadPayload'
    ThreadReopenedEvent:
      description: Durable event `thread.reopened`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [thread.reopened]
          payload:
            $ref: '#/components/schemas/ThreadPayload'
    ThreadMovedEvent:
      description: Durable event `thread.moved`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [thread.moved]
          payload:
            $ref: '#/components/schemas/ThreadMovedPayload'
    ThreadMovedOutEvent:
      description: Durable event `thread.moved_out`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [thread.moved_out]
          payload:
            $ref: '#/components/schemas/ThreadMovedOutPayload'
    AgentSessionChangedEvent:
      description: Durable event `agent_session.changed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [agent_session.changed]
          payload:
            $ref: '#/components/schemas/AgentSessionChangedPayload'
    RunStateChangedEvent:
      description: Durable event `run.state_changed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [run.state_changed]
          payload:
            $ref: '#/components/schemas/RunStateChangedPayload'
    SpaceCreatedEvent:
      description: Durable event `space.created`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [space.created]
          payload:
            $ref: '#/components/schemas/SpacePayload'
    SpaceUpdatedEvent:
      description: Durable event `space.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [space.updated]
          payload:
            $ref: '#/components/schemas/SpacePayload'
    SpaceArchivedEvent:
      description: Durable event `space.archived`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [space.archived]
          payload:
            $ref: '#/components/schemas/SpacePayload'
    ChannelCreatedEvent:
      description: Durable event `channel.created`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [channel.created]
          payload:
            $ref: '#/components/schemas/ChannelPayload'
    ChannelUpdatedEvent:
      description: Durable event `channel.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [channel.updated]
          payload:
            $ref: '#/components/schemas/ChannelPayload'
    ChannelSettingsChangedEvent:
      description: Durable event `channel.settings_changed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [channel.settings_changed]
          payload:
            $ref: '#/components/schemas/ChannelSettingsChangedPayload'
    ChannelArchivedEvent:
      description: Durable event `channel.archived`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [channel.archived]
          payload:
            $ref: '#/components/schemas/ChannelPayload'
    CommandChangedEvent:
      description: Durable event `command.changed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [command.changed]
          payload:
            $ref: '#/components/schemas/CommandChangedPayload'
    FileUpdatedEvent:
      description: Durable event `file.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [file.updated]
          payload:
            $ref: '#/components/schemas/FilePayload'
    UserUpdatedEvent:
      description: Durable event `user.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [user.updated]
          payload:
            $ref: '#/components/schemas/UserPayload'
    AgentUpdatedEvent:
      description: Durable event `agent.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [agent.updated]
          payload:
            $ref: '#/components/schemas/AgentPayload'
    InstanceUpdatedEvent:
      description: Durable event `instance.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [instance.updated]
          payload:
            $ref: '#/components/schemas/InstanceUpdatedPayload'
    StatusUpdatedEvent:
      description: Durable event `status.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [status.updated]
          payload:
            $ref: '#/components/schemas/StatusUpdatedPayload'
    GrantChangedEvent:
      description: Durable event `grant.changed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [grant.changed]
          payload:
            $ref: '#/components/schemas/GrantChangedPayload'
    GroupChangedEvent:
      description: Durable event `group.changed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [group.changed]
          payload:
            $ref: '#/components/schemas/GroupChangedPayload'
    AccessChangedEvent:
      description: Durable event `access.changed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [access.changed]
          payload:
            $ref: '#/components/schemas/AccessChangedPayload'
    AccessExpiringEvent:
      description: Durable event `access.expiring`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [access.expiring]
          payload:
            $ref: '#/components/schemas/AccessExpiringPayload'
    DevicePendingEvent:
      description: Durable event `device.pending`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [device.pending]
          payload:
            $ref: '#/components/schemas/DevicePendingPayload'
    SessionRevokedEvent:
      description: Durable event `session.revoked`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [session.revoked]
          payload:
            $ref: '#/components/schemas/SessionRevokedPayload'
    ReadStateUpdatedEvent:
      description: Durable event `read_state.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [read_state.updated]
          payload:
            $ref: '#/components/schemas/ReadStatePayload'
    DraftUpdatedEvent:
      description: Durable event `draft.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [draft.updated]
          payload:
            $ref: '#/components/schemas/DraftPayload'
    DraftDeletedEvent:
      description: Durable event `draft.deleted`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [draft.deleted]
          payload:
            $ref: '#/components/schemas/DraftDeletedPayload'
    SettingsUpdatedEvent:
      description: Durable event `settings.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [settings.updated]
          payload:
            $ref: '#/components/schemas/SettingsPayload'
    SidebarUpdatedEvent:
      description: Durable event `sidebar.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [sidebar.updated]
          payload:
            $ref: '#/components/schemas/SidebarPayload'
    NotificationPrefUpdatedEvent:
      description: Durable event `notification_pref.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [notification_pref.updated]
          payload:
            $ref: '#/components/schemas/NotificationPrefPayload'
    QuietHoursUpdatedEvent:
      description: Durable event `quiet_hours.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [quiet_hours.updated]
          payload:
            $ref: '#/components/schemas/QuietHoursPayload'
    GdprRequestUpdatedEvent:
      description: Durable event `gdpr.request_updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [gdpr.request_updated]
          payload:
            $ref: '#/components/schemas/GdprRequestPayload'
    GdprRequestDueEvent:
      description: Durable event `gdpr.request_due`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [gdpr.request_due]
          payload:
            $ref: '#/components/schemas/GdprRequestDuePayload'
    ImportProgressEvent:
      description: Durable event `import.progress`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [import.progress]
          payload:
            $ref: '#/components/schemas/ImportProgressPayload'
    AgentInvocationEvent:
      description: Durable event `agent.invocation`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [agent.invocation]
          payload:
            $ref: '#/components/schemas/AgentInvocationPayload'
    AgentFollowupEvent:
      description: Durable event `agent.followup`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [agent.followup]
          payload:
            $ref: '#/components/schemas/AgentFollowupPayload'
    RunCancelRequestedEvent:
      description: Durable event `run.cancel_requested`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [run.cancel_requested]
          payload:
            $ref: '#/components/schemas/RunCancelRequestedPayload'
    InteractionCreatedEvent:
      description: Durable event `interaction.created`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [interaction.created]
          payload:
            $ref: '#/components/schemas/InteractionCreatedPayload'
    CommandInvokedEvent:
      description: Durable event `command.invoked`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [command.invoked]
          payload:
            $ref: '#/components/schemas/CommandInvokedPayload'
    AgentConfigChangedEvent:
      description: Durable event `agent.config_changed`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [agent.config_changed]
          payload:
            $ref: '#/components/schemas/AgentConfigChangedPayload'
    TypingStartedEvent:
      description: Ephemeral event `typing.started` (no `seq`, at most once).
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [typing.started]
          payload:
            $ref: '#/components/schemas/TypingStartedPayload'
    RunUpdatedEvent:
      description: Ephemeral event `run.updated` (no `seq`, at most once).
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [run.updated]
          payload:
            $ref: '#/components/schemas/RunUpdatedPayload'
    AgentPresenceChangedEvent:
      description: Ephemeral event `agent.presence_changed` (no `seq`, at most once).
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [agent.presence_changed]
          payload:
            $ref: '#/components/schemas/AgentPresencePayload'
    CommandAutocompleteRequestedEvent:
      description: Ephemeral event `command.autocomplete_requested` (no `seq`, at most once).
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [command.autocomplete_requested]
          payload:
            $ref: '#/components/schemas/CommandAutocompleteRequestedPayload'
    NotificationCreatedEvent:
      description: Ephemeral event `notification.created` (no `seq`, at most once), only to connections of the recipient instead of a push (13, 6.3).
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [notification.created]
          payload:
            $ref: '#/components/schemas/NotificationCreatedPayload'
    NotificationCreatedPayload:
      type: object
      required: [nid, kind, quiet, count]
      properties:
        nid:
          $ref: '#/components/schemas/Uuid'
        kind:
          $ref: '#/components/schemas/NotificationKind'
        thread_id:
          $ref: '#/components/schemas/Uuid'
          description: Thread of the hint; absent for hints without a thread.
        message_id:
          $ref: '#/components/schemas/Uuid'
          description: Message the hint points at; absent when the hint points at no message.
        target_id:
          $ref: '#/components/schemas/Uuid'
          description: 'Target of a hint without a thread: the waiting device for `security`, the grant or group of the reminder for `admin` (R-AZ-30).'
        quiet:
          type: boolean
          description: Inside quiet hours; show without sound.
        count:
          type: integer
          description: 1, or the unread count of a bundled hint (13 4.5).
        space_name:
          type: string
        channel_name:
          type: string
        thread_title:
          type: string
        sender_display_name:
          type: string
        excerpt:
          type: string
          description: Plain text, at most `notify.preview_max_chars`; only with `notify.preview_mode = full`.
        attachment_count:
          type: integer
        image_url:
          type: string
          format: uri-reference
          description: Fetch ticket for the `small` variant of the first image.
    NotificationKind:
      type: string
      enum: [message, mention, dm, run_done, run_failed, input_needed, security, admin]
    PushPreview:
      type: object
      description: Preview for the notification extension, fields depend on `notify.preview_mode` (13, 6.4).
      required: [kind, badge]
      properties:
        kind:
          $ref: '#/components/schemas/NotificationKind'
        thread_id:
          $ref: '#/components/schemas/Uuid'
        message_id:
          $ref: '#/components/schemas/Uuid'
        badge:
          type: integer
        space_name:
          type: string
        channel_name:
          type: string
        thread_title:
          type: string
        sender_display_name:
          type: string
        excerpt:
          type: string
        attachment_count:
          type: integer
        image_url:
          type: string
          format: uri-reference
          description: Fetch ticket for the `small` variant of the first image.
    Event:
      description: Any durable event, discriminated by `type`. Clients skip unknown types and still advance their cursor.
      oneOf:
      - $ref: '#/components/schemas/MessageCreatedEvent'
      - $ref: '#/components/schemas/MessageUpdatedEvent'
      - $ref: '#/components/schemas/MessageDeletedEvent'
      - $ref: '#/components/schemas/ReactionAddedEvent'
      - $ref: '#/components/schemas/ReactionRemovedEvent'
      - $ref: '#/components/schemas/PinAddedEvent'
      - $ref: '#/components/schemas/PinRemovedEvent'
      - $ref: '#/components/schemas/ThreadCreatedEvent'
      - $ref: '#/components/schemas/ThreadUpdatedEvent'
      - $ref: '#/components/schemas/ThreadClosedEvent'
      - $ref: '#/components/schemas/ThreadReopenedEvent'
      - $ref: '#/components/schemas/ThreadMovedEvent'
      - $ref: '#/components/schemas/ThreadMovedOutEvent'
      - $ref: '#/components/schemas/AgentSessionChangedEvent'
      - $ref: '#/components/schemas/RunStateChangedEvent'
      - $ref: '#/components/schemas/SpaceCreatedEvent'
      - $ref: '#/components/schemas/SpaceUpdatedEvent'
      - $ref: '#/components/schemas/SpaceArchivedEvent'
      - $ref: '#/components/schemas/ChannelCreatedEvent'
      - $ref: '#/components/schemas/ChannelUpdatedEvent'
      - $ref: '#/components/schemas/ChannelSettingsChangedEvent'
      - $ref: '#/components/schemas/ChannelArchivedEvent'
      - $ref: '#/components/schemas/CommandChangedEvent'
      - $ref: '#/components/schemas/FileUpdatedEvent'
      - $ref: '#/components/schemas/UserUpdatedEvent'
      - $ref: '#/components/schemas/AgentUpdatedEvent'
      - $ref: '#/components/schemas/InstanceUpdatedEvent'
      - $ref: '#/components/schemas/StatusUpdatedEvent'
      - $ref: '#/components/schemas/GrantChangedEvent'
      - $ref: '#/components/schemas/GroupChangedEvent'
      - $ref: '#/components/schemas/AccessChangedEvent'
      - $ref: '#/components/schemas/AccessExpiringEvent'
      - $ref: '#/components/schemas/DevicePendingEvent'
      - $ref: '#/components/schemas/SessionRevokedEvent'
      - $ref: '#/components/schemas/ReadStateUpdatedEvent'
      - $ref: '#/components/schemas/DraftUpdatedEvent'
      - $ref: '#/components/schemas/DraftDeletedEvent'
      - $ref: '#/components/schemas/SettingsUpdatedEvent'
      - $ref: '#/components/schemas/SidebarUpdatedEvent'
      - $ref: '#/components/schemas/NotificationPrefUpdatedEvent'
      - $ref: '#/components/schemas/QuietHoursUpdatedEvent'
      - $ref: '#/components/schemas/GdprRequestUpdatedEvent'
      - $ref: '#/components/schemas/GdprRequestDueEvent'
      - $ref: '#/components/schemas/ImportProgressEvent'
      - $ref: '#/components/schemas/AgentInvocationEvent'
      - $ref: '#/components/schemas/AgentFollowupEvent'
      - $ref: '#/components/schemas/RunCancelRequestedEvent'
      - $ref: '#/components/schemas/InteractionCreatedEvent'
      - $ref: '#/components/schemas/CommandInvokedEvent'
      - $ref: '#/components/schemas/AgentConfigChangedEvent'
      - $ref: '#/components/schemas/HostUpdatedEvent'
      - $ref: '#/components/schemas/TerminalSessionChangedEvent'
      discriminator:
        propertyName: type
        mapping:
          message.created: '#/components/schemas/MessageCreatedEvent'
          message.updated: '#/components/schemas/MessageUpdatedEvent'
          message.deleted: '#/components/schemas/MessageDeletedEvent'
          reaction.added: '#/components/schemas/ReactionAddedEvent'
          reaction.removed: '#/components/schemas/ReactionRemovedEvent'
          pin.added: '#/components/schemas/PinAddedEvent'
          pin.removed: '#/components/schemas/PinRemovedEvent'
          thread.created: '#/components/schemas/ThreadCreatedEvent'
          thread.updated: '#/components/schemas/ThreadUpdatedEvent'
          thread.closed: '#/components/schemas/ThreadClosedEvent'
          thread.reopened: '#/components/schemas/ThreadReopenedEvent'
          thread.moved: '#/components/schemas/ThreadMovedEvent'
          thread.moved_out: '#/components/schemas/ThreadMovedOutEvent'
          agent_session.changed: '#/components/schemas/AgentSessionChangedEvent'
          run.state_changed: '#/components/schemas/RunStateChangedEvent'
          space.created: '#/components/schemas/SpaceCreatedEvent'
          space.updated: '#/components/schemas/SpaceUpdatedEvent'
          space.archived: '#/components/schemas/SpaceArchivedEvent'
          channel.created: '#/components/schemas/ChannelCreatedEvent'
          channel.updated: '#/components/schemas/ChannelUpdatedEvent'
          channel.settings_changed: '#/components/schemas/ChannelSettingsChangedEvent'
          channel.archived: '#/components/schemas/ChannelArchivedEvent'
          command.changed: '#/components/schemas/CommandChangedEvent'
          file.updated: '#/components/schemas/FileUpdatedEvent'
          user.updated: '#/components/schemas/UserUpdatedEvent'
          agent.updated: '#/components/schemas/AgentUpdatedEvent'
          instance.updated: '#/components/schemas/InstanceUpdatedEvent'
          status.updated: '#/components/schemas/StatusUpdatedEvent'
          grant.changed: '#/components/schemas/GrantChangedEvent'
          group.changed: '#/components/schemas/GroupChangedEvent'
          access.changed: '#/components/schemas/AccessChangedEvent'
          access.expiring: '#/components/schemas/AccessExpiringEvent'
          device.pending: '#/components/schemas/DevicePendingEvent'
          session.revoked: '#/components/schemas/SessionRevokedEvent'
          read_state.updated: '#/components/schemas/ReadStateUpdatedEvent'
          draft.updated: '#/components/schemas/DraftUpdatedEvent'
          draft.deleted: '#/components/schemas/DraftDeletedEvent'
          settings.updated: '#/components/schemas/SettingsUpdatedEvent'
          sidebar.updated: '#/components/schemas/SidebarUpdatedEvent'
          notification_pref.updated: '#/components/schemas/NotificationPrefUpdatedEvent'
          quiet_hours.updated: '#/components/schemas/QuietHoursUpdatedEvent'
          gdpr.request_updated: '#/components/schemas/GdprRequestUpdatedEvent'
          gdpr.request_due: '#/components/schemas/GdprRequestDueEvent'
          import.progress: '#/components/schemas/ImportProgressEvent'
          agent.invocation: '#/components/schemas/AgentInvocationEvent'
          agent.followup: '#/components/schemas/AgentFollowupEvent'
          run.cancel_requested: '#/components/schemas/RunCancelRequestedEvent'
          interaction.created: '#/components/schemas/InteractionCreatedEvent'
          command.invoked: '#/components/schemas/CommandInvokedEvent'
          agent.config_changed: '#/components/schemas/AgentConfigChangedEvent'
          host.updated: '#/components/schemas/HostUpdatedEvent'
          terminal_session.changed: '#/components/schemas/TerminalSessionChangedEvent'
    MentionItemPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/MentionItem'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    UserPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/User'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    DevicePage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Device'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    SessionPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Session'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    ApiTokenPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ApiToken'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    GroupPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Group'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    GroupMemberPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/GroupMember'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    PermissionHintKind:
      type: string
      description: Kind of a permission hint (07 section 9.5), in the order of the list.
      enum: [h1_read_horizon, grant_to_disabled, trigger_blocked, agent_personal]
    PermissionHint:
      type: object
      description: 'One permission hint. `h1_read_horizon` names `agent_id`, `person_id`, `space_id` and `channel_id` (the person can learn content of the channel through the agent, 07 section 6). `grant_to_disabled` and `trigger_blocked` carry the `grant`; `trigger_blocked` names the blocking `rules` (SR2 tool class, SR3 dev isolation). `agent_personal` names `agent_id`, `credential_holder_id` (absent when the agent has none) and the `trigger_set` (I1 asks for exactly the holder).'
      required: [kind]
      properties:
        kind:
          $ref: '#/components/schemas/PermissionHintKind'
        agent_id:
          $ref: '#/components/schemas/Uuid'
        person_id:
          $ref: '#/components/schemas/Uuid'
        space_id:
          $ref: '#/components/schemas/Uuid'
        channel_id:
          $ref: '#/components/schemas/Uuid'
        grant:
          $ref: '#/components/schemas/Grant'
        rules:
          type: array
          items:
            type: string
            enum: [sr2, sr3]
        credential_holder_id:
          $ref: '#/components/schemas/Uuid'
        trigger_set:
          type: array
          description: The people with effective `trigger` on a target the agent serves, ascending.
          items:
            $ref: '#/components/schemas/Uuid'
    AccessLabel:
      type: object
      description: Name of a group or space named on a page.
      required: [id, name]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Support]
    ChannelLabel:
      type: object
      description: Name and space of a channel named on a page.
      required: [id, space_id, name]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        space_id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Intern]
    AccessLabels:
      type: object
      description: The names of the principals, groups, spaces and channels a page of hints or expiring access names, as the administration lists show them; never content.
      required: [principals, groups, spaces, channels]
      properties:
        principals:
          type: array
          items:
            $ref: '#/components/schemas/PrincipalRef'
        groups:
          type: array
          items:
            $ref: '#/components/schemas/AccessLabel'
        spaces:
          type: array
          items:
            $ref: '#/components/schemas/AccessLabel'
        channels:
          type: array
          items:
            $ref: '#/components/schemas/ChannelLabel'
    PermissionHintPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items, labels]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/PermissionHint'
        labels:
          $ref: '#/components/schemas/AccessLabels'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    ExpiringAccessItem:
      type: object
      description: 'One access that expires within `authz.expiry_reminder_lead`. `grant` carries the `grant`; `membership` names `membership_id`, `group_id` and `user_id`; `token` names `token_id` and the holder `principal_id` (an agent or an app), never the token value.'
      required: [kind, expires_at]
      properties:
        kind:
          type: string
          enum: [grant, membership, token]
        expires_at:
          $ref: '#/components/schemas/Timestamp'
        grant:
          $ref: '#/components/schemas/Grant'
        membership_id:
          $ref: '#/components/schemas/Uuid'
        group_id:
          $ref: '#/components/schemas/Uuid'
        user_id:
          $ref: '#/components/schemas/Uuid'
        token_id:
          $ref: '#/components/schemas/Uuid'
        principal_id:
          $ref: '#/components/schemas/Uuid'
    ExpiringAccessPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items, labels]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ExpiringAccessItem'
        labels:
          $ref: '#/components/schemas/AccessLabels'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    GrantPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Grant'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    SpaceViewList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/SpaceView'
    ChannelViewList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ChannelView'
    ThreadPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Thread'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    RunSummaryPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/RunSummary'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    PinList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Pin'
    ReactionPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Reaction'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    SearchHitPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/SearchHit'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
        candidates_capped:
          type: boolean
          description: The candidate limit `search.max_candidates` was reached; ask for a narrower query.
    ThreadTitleHitList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            type: object
            required: [thread_id, channel_id, title, last_activity_at]
            properties:
              thread_id:
                $ref: '#/components/schemas/Uuid'
              channel_id:
                $ref: '#/components/schemas/Uuid'
              title:
                type: string
                examples: [Invoice export fails]
              last_activity_at:
                $ref: '#/components/schemas/Timestamp'
    AgentPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Agent'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    AgentChannelConfigList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/AgentChannelConfig'
    AppPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/App'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    WebhookDeliveryPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDelivery'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    AuditEventPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/AuditEvent'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    ImportSourcePage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ImportSource'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    ImportItemPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/ImportItem'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    GdprRequestPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/GdprRequest'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    HostState:
      type: string
      description: Life cycle of a host (24, 3.1). Only `active` allows sessions.
      enum: [pairing, confirming, active, disabled, retired]
    LlmCredentials:
      type: string
      description: Owner declaration of the LLM login on a host (24, 7.2). `personal` needs `credential_holder_id` (I6).
      enum: [none, personal, organization]
    HostPlatform:
      type: string
      enum: [macos, linux, windows]
    HostProfile:
      type: object
      additionalProperties: false
      description: Profile reported by the host service (`host_profiles.v1`). Never a command line, path or environment.
      required: [name, display_name, kind]
      properties:
        name:
          type: string
          pattern: ^[a-z0-9][a-z0-9_-]*$
          examples: [shell]
        display_name:
          type: string
          examples: [Shell]
        kind:
          type: string
          enum: [shell, claude, command]
    HostDoctorCheck:
      type: object
      description: Result of one self-check of the host service (`host_doctor.v1`, checks HD1 to HD11 in 24, 2.2). Codes and, following the rule for names (24, 2.2), the name of each account a person with a terminal can cause the finding in; never content, paths or values. When HD6 or HD11 fails, the list has one entry per affected session user; when HD10 fails with a wrong peer, one entry per session user of that channel; when HD9 fails, one entry per former session user whose home can be listed, plus one entry without a name for any other user; `check` and `session_user` together are unique in the list.
      required: [check, ok]
      properties:
        check:
          type: string
          enum: [hd1_user, hd2_groups, hd3_sudo, hd4_state_dir, hd5_server_host, hd6_llm_login, hd7_profiles, hd8_separation, hd9_foreign_homes, hd10_helper_integrity, hd11_shared_locations]
        ok:
          type: boolean
        code:
          type: string
          pattern: ^[a-z0-9_]+$
          examples: [subscription_login_detected]
        session_user:
          type: string
          description: 'Only when not passed: the account the rule for names gives (see the schema; 24, 2.2 and 7.4). A name, never a path.'
          pattern: ^[A-Za-z0-9_][A-Za-z0-9_.-]*$
          examples: [bothy-s-robin]
    HostSessionUserAssign:
      type: object
      additionalProperties: false
      description: Assigns a person their own session user on a host (ruling T12, 24 section 2.2). The name must be one the host reported (`reported_session_users`); any other name gives `400 validation_failed` with `errors[].code` `session_user_not_on_host`, and nothing is stored.
      required: [user_id, os_user]
      properties:
        user_id:
          $ref: '#/components/schemas/Uuid'
        os_user:
          type: string
          description: Operating system user name from `host_service.session_users`; at most `host.os_user_max_chars` characters; never a path.
          pattern: ^[A-Za-z0-9_][A-Za-z0-9_.-]*$
          examples: [bothy-s-robin]
    HostSessionUser:
      type: object
      description: A valid assignment of a person to their own session user on the host (ruling T12, 24 section 2.2).
      required: [user_id, os_user, created_at]
      properties:
        user_id:
          $ref: '#/components/schemas/Uuid'
        os_user:
          type: string
          examples: [bothy-s-robin]
        created_at:
          $ref: '#/components/schemas/Timestamp'
    HostReportedSessionUser:
      type: object
      additionalProperties: false
      description: A session user the host service reports (`host_reported_session_users.v1`, 24 section 2.2), with the display names of its own working directories of the `claude` profile (ruling T13). Names only, never paths.
      required: [os_user, workdirs]
      properties:
        os_user:
          type: string
          description: Operating system user name from `host_service.session_users`.
          pattern: ^[A-Za-z0-9_][A-Za-z0-9_.-]*$
          examples: [bothy-s-robin]
        workdirs:
          type: array
          description: Display names of the working directories of this session user in `host_service.claude_workdirs` after check HD7, in their order (a directory listed under two session users is dropped from both lists); empty means no `claude` profile for this session user. A client sends the index into its own list, never a path; the host service resolves the index in exactly this list (24, 2.2 and 5.3).
          items:
            type: string
            examples: [Project A]
    Host:
      type: object
      description: A connected host. Keys are public; the client pins `noise_public_key` per device (24, 5.5).
      required: [id, name, state, llm_credentials, declared_server_host, key_generation, profiles, doctor, connected, created_at, version]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        name:
          type: string
          examples: [Build server]
        state:
          $ref: '#/components/schemas/HostState'
        disabled_reason:
          type: string
          description: Present exactly with `state = disabled`. `duplicate_connection` means a second live control connection with the same keys; only re-pairing helps (24, 3.4).
          enum: [owner, duplicate_connection]
        platform:
          $ref: '#/components/schemas/HostPlatform'
        assigned_user_id:
          $ref: '#/components/schemas/Uuid'
          description: Person the host is assigned to (SR10); absent means owners only.
        llm_credentials:
          $ref: '#/components/schemas/LlmCredentials'
        credential_holder_id:
          $ref: '#/components/schemas/Uuid'
          description: Present exactly with `llm_credentials = personal`.
        declared_server_host:
          type: boolean
        reported_is_server_host:
          type: boolean
          description: Report of the host service (24, 2.3); absent before the first report.
        noise_public_key:
          type: string
          description: Static X25519 key of the host, 32 bytes base64url without padding; absent while the host has no keys (before pairing and after a re-pairing or the fallback of `confirmHost`).
          examples: [Zm9vYmFyZm9vYmFyZm9vYmFyZm9vYmFyZm9vYmFyMTI]
        key_fingerprint:
          type: string
          description: Fingerprint of the stored keys (24, 4.3), 13 groups of four Crockford base32 characters; absent while the host has no keys.
          examples: [7K2Q-M9XD-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000-0000]
        key_generation:
          type: integer
          examples: [1]
        profiles:
          type: array
          items:
            $ref: '#/components/schemas/HostProfile'
        doctor:
          type: array
          description: One entry per check HD1 to HD11; a failed HD6, HD9, HD10 or HD11 has one entry per account named by the rule for names (24, 2.2).
          items:
            $ref: '#/components/schemas/HostDoctorCheck'
        session_users:
          type: array
          description: Valid session user assignments (ruling T12), visible like every other member to everyone with `host.read`. Absent when there is none.
          items:
            $ref: '#/components/schemas/HostSessionUser'
        reported_session_users:
          type: array
          description: Session users the host service reported at pairing and in its last `report` (24 section 2.2). An assignment works only while its name is in this list (SR12); `confirmHost` and `updateHost` accept only these names. Absent before pairing.
          items:
            $ref: '#/components/schemas/HostReportedSessionUser'
        require_local_confirm:
          type: boolean
          description: Report whether `host_service.require_local_confirm` is on.
        connected:
          type: boolean
          description: Usable for sessions, from the first `report` after `ready` to the end of the control connection (24, 3.5).
        host_version:
          type: string
          examples: [1.0.0]
        os_version:
          type: string
          examples: [Debian 13]
        last_seen_at:
          $ref: '#/components/schemas/Timestamp'
        created_at:
          $ref: '#/components/schemas/Timestamp'
        version:
          type: integer
          format: int64
    HostPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/Host'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    HostCreate:
      type: object
      additionalProperties: false
      required: [name, reason]
      properties:
        name:
          type: string
          examples: [Build server]
        assigned_user_id:
          $ref: '#/components/schemas/Uuid'
        reason:
          type: string
          examples: [Shared build machine for Alex]
    HostPairingCode:
      type: object
      description: The pairing code is shown exactly once; the server stores only its HMAC.
      required: [host_id, pairing_code, expires_at]
      properties:
        host_id:
          $ref: '#/components/schemas/Uuid'
        pairing_code:
          type: string
          pattern: ^bothy_hpc_[0-9A-Za-z]{43}_[0-9A-Za-z]{6}$
          examples: [bothy_hpc_EXAMPLE000000000000000000000000000000000000_EXAMPL]
        expires_at:
          $ref: '#/components/schemas/Timestamp'
    HostPairRequest:
      type: object
      additionalProperties: false
      required: [pairing_code, noise_public_key, auth_public_jwk, platform, host_version, profiles, session_users]
      properties:
        pairing_code:
          type: string
          pattern: ^bothy_hpc_[0-9A-Za-z]{43}_[0-9A-Za-z]{6}$
        noise_public_key:
          type: string
          description: Static X25519 public key, 32 bytes base64url without padding.
        auth_public_jwk:
          $ref: '#/components/schemas/JsonObject'
          description: Ed25519 public key as JWK (`kty = OKP`, `crv = Ed25519`); a private member is rejected.
        platform:
          $ref: '#/components/schemas/HostPlatform'
        os_version:
          type: string
        host_version:
          type: string
          examples: [1.0.0]
        profiles:
          type: array
          items:
            $ref: '#/components/schemas/HostProfile'
        session_users:
          type: array
          description: The session users of `host_service.session_users` with the display names of their working directories (24 section 2.2).
          items:
            $ref: '#/components/schemas/HostReportedSessionUser'
    HostPairResult:
      type: object
      required: [host_id, key_fingerprint, instance_id]
      properties:
        host_id:
          $ref: '#/components/schemas/Uuid'
        key_fingerprint:
          type: string
        instance_id:
          $ref: '#/components/schemas/Uuid'
    HostUpdate:
      type: object
      additionalProperties: false
      description: Members that are absent stay unchanged. To remove the assignee send `clear_assigned_user` (no JSON null, ADR 0053).
      required: [reason]
      properties:
        name:
          type: string
        assigned_user_id:
          $ref: '#/components/schemas/Uuid'
        clear_assigned_user:
          type: boolean
        add_session_users:
          type: array
          description: New session user assignments (ruling T12).
          items:
            $ref: '#/components/schemas/HostSessionUserAssign'
        revoke_session_users:
          type: array
          description: People whose session user assignment ends; their names stay taken on this host.
          items:
            $ref: '#/components/schemas/Uuid'
        reason:
          type: string
    HostConfirm:
      type: object
      additionalProperties: false
      required: [fingerprint_prefix, llm_credentials, declared_server_host, reason]
      properties:
        fingerprint_prefix:
          type: string
          description: First `terminal.fingerprint_confirm_chars` characters of the fingerprint without separators, case-insensitive.
          examples: [7K2QM9XD]
        llm_credentials:
          $ref: '#/components/schemas/LlmCredentials'
        credential_holder_id:
          $ref: '#/components/schemas/Uuid'
        declared_server_host:
          type: boolean
        session_users:
          type: array
          description: Session user per person (ruling T12); a person without one gets no terminal on the host (SR12).
          items:
            $ref: '#/components/schemas/HostSessionUserAssign'
        reason:
          type: string
    HostLlmCredentialsSet:
      type: object
      additionalProperties: false
      required: [llm_credentials, reason]
      properties:
        llm_credentials:
          $ref: '#/components/schemas/LlmCredentials'
        credential_holder_id:
          $ref: '#/components/schemas/Uuid'
          description: Required exactly with `personal`. `errors[].code` `required` when missing with `personal`, `not_allowed` when sent with `none` or `organization`, `unknown_user` when it names no person or a deleted one.
        reason:
          type: string
    HostStateSet:
      type: object
      additionalProperties: false
      required: [state, reason]
      properties:
        state:
          type: string
          enum: [active, disabled, retired]
        reason:
          type: string
    TerminalSessionState:
      type: string
      enum: [opening, active, detached, closed]
    TerminalCloseReason:
      type: string
      description: Why a session ended (24, 10.3).
      enum: [client_closed, closed_by_owner, process_exited, idle_timeout, max_duration, detach_expired, access_revoked, i6_violation, host_disabled, host_retired, host_rekeyed, host_restarted, user_disabled, device_revoked, session_revoked, logout, open_failed, host_policy_violation]
    TerminalCloseDetail:
      type: string
      enum: [token_invalid, token_expired, token_replayed, handshake_failed, profile_unknown, session_user_unknown, session_user_mismatch, server_host_owner_only, local_confirm_denied, subscription_login_detected, helper_untrusted, shared_location, pty_failed, open_timeout]
    TerminalSession:
      type: object
      description: Metadata of a terminal session. Never content.
      required: [id, host_id, user_id, device_id, profile, state, opened_at, bytes_in, bytes_out, version]
      properties:
        id:
          $ref: '#/components/schemas/Uuid'
        host_id:
          $ref: '#/components/schemas/Uuid'
        user_id:
          $ref: '#/components/schemas/Uuid'
        device_id:
          $ref: '#/components/schemas/Uuid'
        profile:
          type: string
          examples: [shell]
        state:
          $ref: '#/components/schemas/TerminalSessionState'
        opened_at:
          $ref: '#/components/schemas/Timestamp'
        started_at:
          $ref: '#/components/schemas/Timestamp'
        detached_at:
          $ref: '#/components/schemas/Timestamp'
        closed_at:
          $ref: '#/components/schemas/Timestamp'
        close_reason:
          $ref: '#/components/schemas/TerminalCloseReason'
        close_detail:
          $ref: '#/components/schemas/TerminalCloseDetail'
        exit_code:
          type: integer
        bytes_in:
          type: integer
          format: int64
        bytes_out:
          type: integer
          format: int64
        version:
          type: integer
          format: int64
    TerminalSessionPage:
      type: object
      description: One page. `next_cursor` is absent on the last page.
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/TerminalSession'
        next_cursor:
          $ref: '#/components/schemas/Cursor'
    TerminalSessionOpen:
      type: object
      additionalProperties: false
      required: [profile, cols, rows, client_ephemeral_key_hash]
      properties:
        profile:
          type: string
          examples: [shell]
        workdir_index:
          type: integer
          minimum: 0
          description: 'Required exactly with a `claude` profile: index into `workdirs` of the caller''s own entry in `reported_session_users` (ruling T13). A `claude` profile without a valid index, or an index with another profile, gives 403 `terminal_denied` with `reason: profile_unknown`; other profiles omit it. The token carries it as `wdi` (24, 5.3, 11.1).'
        cols:
          type: integer
          minimum: 1
          maximum: 65535
        rows:
          type: integer
          minimum: 1
          maximum: 65535
        client_ephemeral_key_hash:
          type: string
          pattern: '^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$'
          description: SHA-256 of the ephemeral X25519 public key the device sends in Noise message 1, base64url without padding; becomes the claim `cek`.
        pin_changed:
          type: boolean
          default: false
          description: '`true` when the person confirmed a host key that differs from the key this device pinned earlier (docs/spec/24-terminal.md section 5.5, docs/spec/15-client-architecture.md section 23.3); recorded in the audit entry of `terminal.open`.'
    TerminalSessionAttach:
      type: object
      additionalProperties: false
      required: [cols, rows, client_ephemeral_key_hash]
      properties:
        cols:
          type: integer
          minimum: 1
          maximum: 65535
        rows:
          type: integer
          minimum: 1
          maximum: 65535
        client_ephemeral_key_hash:
          type: string
          pattern: '^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$'
        pin_changed:
          type: boolean
          default: false
          description: Same as in `TerminalSessionOpen`; recorded in the audit entry of `terminal.attach`.
    TerminalSessionGrant:
      type: object
      description: Everything the device needs for the Noise handshake (24, 5). The token is valid once and for `terminal.session_token_ttl`.
      required: [session, session_token, session_token_expires_at, stream_path, noise_public_key, key_fingerprint, key_generation, noise_protocol, prologue_version]
      properties:
        session:
          $ref: '#/components/schemas/TerminalSession'
        session_token:
          type: string
          description: Compact JWS, `typ = terminal-session+jwt`, `alg = EdDSA`.
        session_token_expires_at:
          $ref: '#/components/schemas/Timestamp'
        stream_path:
          type: string
          examples: [/api/v1/terminal/sessions/0192f4a0-7b1c-7b22-9d4f-5a6b7c8d9e0f/stream]
        noise_public_key:
          type: string
        key_fingerprint:
          type: string
        key_generation:
          type: integer
        noise_protocol:
          type: string
          enum: [Noise_NK_25519_ChaChaPoly_SHA256]
        prologue_version:
          type: string
          enum: [bothy-terminal/1]
    HostUpdatedPayload:
      type: object
      required: [host]
      properties:
        host:
          $ref: '#/components/schemas/Host'
        key_changed:
          type: boolean
          description: True after a re-pairing (24, 3.3).
    HostUpdatedEvent:
      description: Durable event `host.updated`.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [host.updated]
          payload:
            $ref: '#/components/schemas/HostUpdatedPayload'
    TerminalSessionChangedEvent:
      description: Durable personal event `terminal_session.changed` to the person of the session.
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [terminal_session.changed]
          payload:
            $ref: '#/components/schemas/TerminalSession'
    HostPresencePayload:
      type: object
      required: [host_id, connected]
      properties:
        host_id:
          $ref: '#/components/schemas/Uuid'
        connected:
          type: boolean
    HostPresenceChangedEvent:
      description: Ephemeral event `host.presence_changed` (no `seq`, at most once).
      allOf:
      - $ref: '#/components/schemas/EventEnvelope'
      - type: object
        required: [type, payload]
        properties:
          type:
            type: string
            enum: [host.presence_changed]
          payload:
            $ref: '#/components/schemas/HostPresencePayload'
    TerminalStreamHello:
      type: object
      description: First text frame of the device stream; the binary Noise frames follow `identify`.
      required: [protocol_versions, dpop_nonce, server_time]
      properties:
        protocol_versions:
          type: array
          items:
            type: integer
            examples: [1]
        dpop_nonce:
          type: string
        server_time:
          $ref: '#/components/schemas/Timestamp'
    TerminalStreamIdentify:
      type: object
      description: 'Native apps send an access token with DPoP proof (`htm = GET`, `htu` = HTTPS form of the stream URL, `nonce` from `hello`, `ath`). The web console sends neither: its stream authenticates with the console session cookie of the upgrade request and the same-origin check of the upgrade (docs/spec/08-identity.md section 7.5, docs/spec/15-client-architecture.md section 23.2); the server rejects an identify without token from any other session. Only the device of the current session token passes (`terminal.stream`).'
      required: [protocol_version, device_id]
      properties:
        protocol_version:
          type: integer
          examples: [1]
        access_token:
          type: string
        dpop:
          type: string
        device_id:
          $ref: '#/components/schemas/Uuid'
    HostChannelHello:
      type: object
      description: First text frame on the control and data connections of a host service. It carries server values the host service applies; the values of the `hello` of a data connection apply to the session of that connection (24, 5.3, 6.2 and 6.3).
      required: [protocol_versions, challenge, server_time, heartbeat_interval_ms, heartbeat_miss_limit, clock_leeway_ms, idle_timeout_ms, detach_grace_ms, jwks]
      properties:
        protocol_versions:
          type: array
          items:
            type: integer
            examples: [1]
        challenge:
          type: string
          description: Random value; the host signs it with its Ed25519 key in `identify`.
        server_time:
          $ref: '#/components/schemas/Timestamp'
        heartbeat_interval_ms:
          type: integer
          format: int64
          examples: [20000]
        heartbeat_miss_limit:
          type: integer
          minimum: 1
        clock_leeway_ms:
          type: integer
          format: int64
          minimum: 0
          description: '`auth.clock_leeway` in milliseconds. The host service applies it to the session of this data connection: the tolerance on `exp` of the session token and how long it remembers the `jti` after `exp` (24, 5.3).'
        idle_timeout_ms:
          type: integer
          format: int64
          minimum: 1
          description: '`terminal.idle_timeout` in milliseconds. The host service applies it to the idle timer of the session of this data connection (24, 6.3).'
        detach_grace_ms:
          type: integer
          format: int64
          minimum: 1
          description: '`terminal.detach_grace` in milliseconds. The host service applies it to the detach timer of the session of this data connection (24, 6.2).'
        jwks:
          $ref: '#/components/schemas/Jwks'
          description: Public keys with purpose `terminal` for checking session tokens.
    HostIdentify:
      type: object
      description: Login of a host service. `proof` is a compact JWS (`alg = EdDSA`, `typ = host-auth+jwt`, `kid`) over exactly `{challenge, host_id, iat}` with the registered Ed25519 key (24, 3.5).
      required: [protocol_version, host_id, proof, host_version]
      properties:
        protocol_version:
          type: integer
          examples: [1]
        host_id:
          $ref: '#/components/schemas/Uuid'
        proof:
          type: string
        host_version:
          type: string
          pattern: ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$
        session_id:
          $ref: '#/components/schemas/Uuid'
          description: Only on a data connection; the session the connection belongs to.
        live_sessions:
          type: array
          description: Only on the control connection, always present there (empty when none). The sessions the host service knows, those whose PTY still runs and those whose end it could not report yet (it reports that end with `session.ended` after `ready`). After a restart of the host service the list is empty. The server ends every other session of the host in state `active` or `detached` with `host_restarted` (24, 6.2).
          items:
            $ref: '#/components/schemas/Uuid'
    HostReady:
      type: object
      description: Sent after login and again on every change of `llm_credentials`; the host service treats each `ready` as the current declaration (24, 2.2 and 7.4).
      required: [host_id, state, llm_credentials]
      properties:
        host_id:
          $ref: '#/components/schemas/Uuid'
        state:
          $ref: '#/components/schemas/HostState'
        llm_credentials:
          $ref: '#/components/schemas/LlmCredentials'
    HostHeartbeat:
      type: object
      required: [active_sessions]
      properties:
        active_sessions:
          type: integer
          minimum: 0
    HostHeartbeatAck:
      type: object
      properties:
        server_time:
          $ref: '#/components/schemas/Timestamp'
        jwks:
          $ref: '#/components/schemas/Jwks'
    HostReport:
      type: object
      description: Report of profiles, session users and self-checks; never content, paths or values. Sent after every `ready` before the host service accepts `session.open`, and on every change. When `doctor` refuses the start, the host service still logs in, sends one `report` and closes the connection without accepting `session.open` (24 section 2.2).
      required: [profiles, session_users, doctor, is_server_host, require_local_confirm]
      properties:
        profiles:
          type: array
          items:
            $ref: '#/components/schemas/HostProfile'
        session_users:
          type: array
          items:
            $ref: '#/components/schemas/HostReportedSessionUser'
        doctor:
          type: array
          description: One entry per check HD1 to HD11; a failed HD6, HD9, HD10 or HD11 has one entry per account named by the rule for names (24, 2.2).
          items:
            $ref: '#/components/schemas/HostDoctorCheck'
        is_server_host:
          type: boolean
        require_local_confirm:
          type: boolean
        os_version:
          type: string
    HostSessionOpen:
      type: object
      description: The server asks the host service to open the data connection of a session.
      required: [session_id, data_path, profile, expires_at]
      properties:
        session_id:
          $ref: '#/components/schemas/Uuid'
        data_path:
          type: string
          examples: [/api/v1/hosts/sessions/0192f4a0-7b1c-7b22-9d4f-5a6b7c8d9e0f/stream]
        profile:
          type: string
        expires_at:
          $ref: '#/components/schemas/Timestamp'
          description: Without a valid handshake until then the host service drops the request (`terminal.open_timeout`).
    HostSessionClose:
      type: object
      required: [session_id, close_reason]
      properties:
        session_id:
          $ref: '#/components/schemas/Uuid'
        close_reason:
          $ref: '#/components/schemas/TerminalCloseReason'
    HostSessionStarted:
      type: object
      required: [session_id]
      properties:
        session_id:
          $ref: '#/components/schemas/Uuid'
    HostSessionEnded:
      type: object
      required: [session_id, close_reason]
      properties:
        session_id:
          $ref: '#/components/schemas/Uuid'
        close_reason:
          $ref: '#/components/schemas/TerminalCloseReason'
        close_detail:
          $ref: '#/components/schemas/TerminalCloseDetail'
        exit_code:
          type: integer
    NoiseFrame:
      type: string
      format: binary
      description: One binary WebSocket message carrying exactly one Noise message (at most 65535 bytes). After the handshake each transport message holds one frame `type (1 byte) || body` (24, 5.4). The relay never decrypts it.
