openapi: 3.1.0
info:
  title: Nexio API
  version: '1.0'
  description: |
    The Nexio public API. Submit runs to configured engines and read their
    results, hold conversations with configured assistants, make authorized
    reads over the planes and families of a connected system of record, and
    manage webhooks and environments.

    Six engine types are registered: comparison, matching, entity_analysis,
    diligence, triage, and opportunity.

    Every route under /api/v1/ requires `Authorization: Bearer <key>`, except
    inbound event ingest (`POST /api/v1/events/ingest/{source_key}`), which
    authenticates each request against its ingest source instead: an
    HMAC-SHA256 signature for `nexio` and `github` sources, or a shared
    authentication code for an `ams360_ons` source. Error
    bodies share the `Error` schema. Call the API from servers only: it sends
    no CORS headers.
  contact:
    email: support@usenexio.com
    url: https://docs.usenexio.com

servers:
  - url: https://api.usenexio.com
    description: Nexio API. Sandbox or live is chosen by the API key's environment, not by the host.

security:
  - BearerAuth: []

tags:
  - name: EngineManagement
    description: Create, configure, version and manage engines. An engine is a configuration of one registered engine type.
  - name: Engines
    description: Engine-scoped endpoints for submitting runs and retrieving results.
  - name: Runs
    description: Retrieve and reconcile submitted runs.
  - name: Records
    description: 'Typed, authorized reads over the registered planes and families of a connected system of record, and governed writes to the action ledger. The generic family read is the core contract.'
  - name: Graph
    description: 'The organization''s data graph: a read-only map of connections and the derivations scheduled on them. Node kinds are a closed registry of 16; the API serves 14.'
  - name: Environments
    description: The live environment and up to 5 sandbox environments in your org. Organization keys only.
  - name: Webhooks
    description: Manage webhook endpoints that receive terminal run events (run.completed, run.failed, run.cancelled) and run corrections (run.superseded).
  - name: Events
    description: Send inbound events to your organization's event log through an ingest source. Source kinds are nexio, github and ams360_ons; a nexio source sends event types from your own vocabulary.
  - name: Converse
    description: The raw stateless conversational model turn (caller-owned state).
  - name: Conversations
    description: Conversation instances (orchestrators over engines) and their managed conversations, turns, annotations, evals, and exports.

  - name: Platform
    description: Service health. No key needed.
paths:
  # ── Engine Management ─────────────────────────────────────────────

  /api/v1/engines:
    get:
      operationId: listEngines
      summary: List Engines
      description: |
        With the organization API key, returns every engine in the
        organization. With a scoped key that holds `engines:read`, returns
        only the engines the key is bound to; a scoped key bound to none gets
        an empty list.

        Engine metadata only: configs are not included. Use
        [Get Engine Config](/api-reference/engines/get-engine-config) to
        retrieve the full configuration for a specific engine.
      tags: [EngineManagement]
      responses:
        '200':
          description: List of engines.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EngineListResponse'
              example:
                engines:
                  - id: 550e8400-e29b-41d4-a716-446655440000
                    slug: vendor-review
                    label: Vendor review
                    description: Checks a supplier profile against vendor requirements.
                    engine_type: entity_analysis
                    status: active
                    group_key: supplier-risk
                    created_at: '2026-04-01T10:00:00Z'
                    updated_at: '2026-04-01T10:00:00Z'
                  - id: 660e8400-e29b-41d4-a716-446655440001
                    slug: vendor-intake
                    label: Vendor intake
                    description: Records a new supplier against a declared contract.
                    engine_type: entity_analysis
                    status: active
                    group_key: ''
                    created_at: '2026-04-02T14:30:00Z'
                    updated_at: '2026-04-03T09:15:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`insufficient_capability`: a scoped key without `engines:read`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: insufficient_capability
                message: API key does not have permission for this action
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

    post:
      operationId: createEngine
      summary: Create Engine
      description: |
        Create a new engine with its type's default configuration saved as the
        draft. The engine starts in `active` status with no released version:
        publish one with `POST /api/v1/engines/{engine_slug}/versions` before
        runs that resolve a release can execute. Unknown request fields are
        ignored. The request body is at most 256 KiB.

        The slug must be unique within your organization and is used in
        all engine-scoped endpoint URLs.

        ### Slug rules

        - 3 to 50 characters
        - Lowercase alphanumeric and hyphens only
        - Must start and end with an alphanumeric character

        Accepts only the organization API key. Every scoped key receives
        `403 insufficient_capability`.
      tags: [EngineManagement]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateEngineRequest'
            example:
              slug: vendor-review
              label: Vendor review
              description: Checks a supplier profile against vendor requirements.
              engine_type: entity_analysis
      responses:
        '201':
          description: Engine created with default configuration.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EngineMetadata'
              example:
                id: 550e8400-e29b-41d4-a716-446655440000
                slug: vendor-review
                label: Vendor review
                description: Checks a supplier profile against vendor requirements.
                engine_type: entity_analysis
                status: active
                group_key: ''
                created_at: '2026-04-01T10:00:00Z'
                updated_at: '2026-04-01T10:00:00Z'
        '400':
          description: Malformed JSON or a body over 256 KiB, or a slug, label or description outside its limits (`invalid_request`), or an unsupported `engine_type` (`invalid_engine_type`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalidSlug:
                  summary: Invalid slug format
                  value:
                    code: invalid_request
                    message: Slug must be 3-50 lowercase alphanumeric characters with hyphens, starting and ending with alphanumeric
                invalidEngineType:
                  summary: Unsupported engine type
                  value:
                    code: invalid_engine_type
                    message: 'engine_type must be one of: comparison, diligence, entity_analysis, matching, opportunity, triage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OrganizationKeyRequired'
        '409':
          description: Engine slug already exists.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_slug_conflict
                message: An engine with this slug already exists
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/engines/{engine_slug}:
    get:
      operationId: getEngine
      summary: Get Engine
      description: |
        Retrieve metadata for a single engine by slug.
      tags: [EngineManagement]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
      responses:
        '200':
          description: Engine metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EngineMetadata'
              example:
                id: 550e8400-e29b-41d4-a716-446655440000
                slug: vendor-review
                label: Vendor review
                description: Checks a supplier profile against vendor requirements.
                engine_type: entity_analysis
                status: active
                group_key: supplier-risk
                created_at: '2026-04-01T10:00:00Z'
                updated_at: '2026-04-01T10:00:00Z'
        '400':
          description: '`invalid_request`: the slug segment of the path is empty.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`insufficient_capability` (a scoped key without `engines:read`) or `engine_binding_forbidden` (the key is not bound to this engine).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/EngineNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

    patch:
      operationId: updateEngine
      summary: Update Engine
      description: |
        Update one or more metadata fields on an existing engine.

        At least one of `label`, `description`, `status` or `group_key` must be
        present with a non-null value; `null` counts as absent and unknown fields
        are ignored. The request body is at most 256 KiB. Setting `status` to
        `archived` makes run submission, configuration saves, publishing and the
        version reads answer `403 engine_archived`.

        Accepts only the organization API key. Every scoped key receives
        `403 insufficient_capability`.
      tags: [EngineManagement]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateEngineRequest'
            example:
              label: Vendor review (2026)
              status: active
      responses:
        '200':
          description: Updated engine metadata.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EngineMetadata'
              example:
                id: 550e8400-e29b-41d4-a716-446655440000
                slug: vendor-review
                label: Vendor review (2026)
                description: Checks a supplier profile against vendor requirements.
                engine_type: entity_analysis
                status: active
                group_key: ''
                created_at: '2026-04-01T10:00:00Z'
                updated_at: '2026-04-05T11:30:00Z'
        '400':
          description: Validation failed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                noFields:
                  summary: No fields provided
                  value:
                    code: invalid_request
                    message: At least one field must be provided
                invalidStatus:
                  summary: Invalid status value
                  value:
                    code: invalid_request
                    message: 'Status must be "active" or "archived"'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OrganizationKeyRequired'
        '404':
          $ref: '#/components/responses/EngineNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/engines/{engine_slug}/config:
    get:
      operationId: getEngineConfig
      summary: Get Engine Config
      description: |
        The draft configuration. Saving it does not change released versions:
        a run that resolves a release executes that release, and only a
        sandbox key's run pinned to `engine_version: draft` executes the saved
        draft. Publish with
        `POST /api/v1/engines/{engine_slug}/versions`. `expose_warnings`,
        `notify_on_supersede` and `quotas.requests_per_minute` are the
        exceptions: Nexio reads them from the saved configuration. See
        [Settings read from the saved configuration](/engines/configuration#settings-read-from-the-saved-configuration).

        Retrieve the full configuration for an engine, along with metadata
        about when and by whom it was last updated.

        The `config` object structure depends on `engine_type`. The Contract
        page on platform.usenexio.com (per-engine "Copy for agents" button)
        is the integration guide: it renders the typed
        request schema directly from the engine's runtime input struct.

        The `preset` field contains the default configuration template for
        the engine type: useful as a starting point when building a new
        config from scratch.
      tags: [EngineManagement]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
      responses:
        '200':
          description: Engine configuration.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EngineConfigResponse'
              example:
                config:
                  schema_version: 1
                  egress_manifest_version: ee5f5d33e35b99102c9a8f9ae29ced43b3b7dfcbc59d34d8600383d267b7069f
                  pack_key: harbor_vendor_review
                  pack_version: 1
                  privacy_policy:
                    mode: default_deny
                  domain_key: vendor
                  response_type: VENDOR_REVIEW
                  analysis_instructions: Review the supplier profile against each enabled dimension. Report every gap with a severity (HIGH, MEDIUM, LOW), a category, a title, a description and a recommendation.
                  analysis_dimensions:
                  - key: security
                    label: Security
                    enabled: true
                  - key: financial
                    label: Financial health
                    enabled: true
                  profile_extraction_rules: []
                  summary_counter_rules: []
                  extra_result_sections: []
                  deterministic_checks: []
                  overlay_categories: []
                  requirement_rules: []
                  deterministic_gap_rules: []
                  knowledge_overlay: []
                config_updated_at: '2026-04-03T09:15:00Z'
                config_updated_by: key_abc123
                engine_type: entity_analysis
                preset:
                  schema_version: 1
                  egress_manifest_version: ee5f5d33e35b99102c9a8f9ae29ced43b3b7dfcbc59d34d8600383d267b7069f
                  pack_key: default_entity_analysis_v1
                  pack_version: 1
                  privacy_policy:
                    mode: default_deny
                  domain_key: default
                  response_type: ENTITY_ANALYSIS
                  analysis_instructions: You are an entity analysis engine. Analyze the entity's profile and identify gaps, risks, or areas for improvement across the configured dimensions. For each gap, provide a severity (HIGH, MEDIUM, LOW), category, title, description, and recommendation. Be specific about which data points drove each finding.
                  analysis_dimensions:
                  - key: general
                    label: General Analysis
                    enabled: true
                  profile_extraction_rules: []
                  summary_counter_rules: []
                  extra_result_sections: []
                  deterministic_checks: []
                  overlay_categories: []
                  requirement_rules: []
                  deterministic_gap_rules: []
                  knowledge_overlay: []
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/EngineNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          description: '`insufficient_capability` (a scoped key without `engines:read`) or `engine_binding_forbidden` (the key is not bound to this engine).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

    put:
      operationId: updateEngineConfig
      summary: Update Engine Config
      description: |
        The draft configuration. Saving it does not change released versions:
        a run that resolves a release executes that release, and only a
        sandbox key's run pinned to `engine_version: draft` executes the saved
        draft. Publish with
        `POST /api/v1/engines/{engine_slug}/versions`. `expose_warnings`,
        `notify_on_supersede` and `quotas.requests_per_minute` are the
        exceptions: Nexio reads them from the saved configuration. See
        [Settings read from the saved configuration](/engines/configuration#settings-read-from-the-saved-configuration).

        Replace the engine's configuration. The full config object must be
        provided: this is not a partial update.

        The config is validated against the engine's type-specific rules
        before being saved. If validation fails, the response includes a
        `details` array describing each issue.

        Cannot update config on an archived engine: reactivate it first
        via [Update Engine](/api-reference/engines/update-engine).

        The request body is at most 256 KiB; a larger body answers
        `400 invalid_request`.

        Accepts only the organization API key. Every scoped key receives
        `403 insufficient_capability`.
      tags: [EngineManagement]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConfigUpdateRequest'
            example:
              config:
                schema_version: 1
                egress_manifest_version: ee5f5d33e35b99102c9a8f9ae29ced43b3b7dfcbc59d34d8600383d267b7069f
                pack_key: harbor_vendor_review
                pack_version: 1
                privacy_policy:
                  mode: default_deny
                domain_key: vendor
                response_type: VENDOR_REVIEW
                analysis_instructions: Review the supplier profile against each enabled dimension. Report every gap with a severity (HIGH, MEDIUM, LOW), a category, a title, a description and a recommendation.
                analysis_dimensions:
                - key: security
                  label: Security
                  enabled: true
                - key: financial
                  label: Financial health
                  enabled: true
                profile_extraction_rules: []
                summary_counter_rules: []
                extra_result_sections: []
                deterministic_checks: []
                overlay_categories: []
                requirement_rules: []
                deterministic_gap_rules: []
                knowledge_overlay: []
      responses:
        '200':
          description: Config updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EngineConfigResponse'
        '400':
          description: Config validation failed (`validation_error` with `details`), or the body is malformed, over 256 KiB or has no `config` (`invalid_request`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                validationError:
                  summary: Config validation errors
                  value:
                    code: validation_error
                    message: Config validation failed
                    details:
                      - path: schema_version
                        message: schema_version must be 1
                      - path: analysis_dimensions.2.key
                        message: Duplicate analysis dimension key "security"
                missingConfig:
                  summary: Missing config field
                  value:
                    code: invalid_request
                    message: config is required
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Engine is archived (`engine_archived`), or a scoped key was used (`insufficient_capability`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_archived
                message: Cannot update config on an archived engine
        '404':
          $ref: '#/components/responses/EngineNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/engines/{engine_slug}/config/validate:
    post:
      operationId: validateEngineConfig
      summary: Validate Engine Config
      description: |
        Dry-run validation of a config object against the engine's
        type-specific rules. The config is **not saved**: use this to
        check for errors before calling
        [Update Engine Config](/api-reference/engines/update-engine-config).

        A well-formed request with a `config` object for an existing engine
        returns `200 OK` with a `valid` boolean and an `errors` array, even
        when the config is invalid. If `valid` is `true`, the `errors` array
        is empty. Malformed JSON, a body over 256 KiB, or an absent `config`
        answers `400 invalid_request`; an unknown engine answers
        `404 engine_not_found`.

        Accepts only the organization API key. Every scoped key receives
        `403 insufficient_capability`.
      tags: [EngineManagement]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConfigUpdateRequest'
            example:
              config:
                schema_version: 1
                egress_manifest_version: ee5f5d33e35b99102c9a8f9ae29ced43b3b7dfcbc59d34d8600383d267b7069f
                pack_key: harbor_vendor_review
                pack_version: 1
                privacy_policy:
                  mode: default_deny
                domain_key: vendor
                response_type: VENDOR_REVIEW
                analysis_instructions: Review the supplier profile against each enabled dimension. Report every gap with a severity (HIGH, MEDIUM, LOW), a category, a title, a description and a recommendation.
                analysis_dimensions:
                - key: security
                  label: Security
                  enabled: true
                - key: financial
                  label: Financial health
                  enabled: true
                profile_extraction_rules: []
                summary_counter_rules: []
                extra_result_sections: []
                deterministic_checks: []
                overlay_categories: []
                requirement_rules: []
                deterministic_gap_rules: []
                knowledge_overlay: []
      responses:
        '200':
          description: Validation result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateConfigResponse'
              examples:
                valid:
                  summary: Config is valid
                  value:
                    valid: true
                    errors: []
                invalid:
                  summary: Config has errors
                  value:
                    valid: false
                    errors:
                      - path: schema_version
                        message: schema_version must be 1
                      - path: analysis_dimensions.2.key
                        message: Duplicate analysis dimension key "security"
        '400':
          description: Malformed request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: invalid_request
                message: config is required
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OrganizationKeyRequired'
        '404':
          $ref: '#/components/responses/EngineNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  # ── Engine versions (maa-154) ──────────────────────────────────────

  /api/v1/engines/{engine_slug}/versions:
    get:
      operationId: listEngineVersions
      summary: List engine versions
      description: |
        Return the released versions of the engine, newest first. New
        releases are cut with `POST` on this same path or from the dashboard's
        Publish action.

        Scoped partner keys require `engines:read` and an explicit binding to
        this engine. Each item includes immutable request and response schema
        hashes when the released version has full schemas. The published
        supported-version registry holds schema bodies only for the generic
        fixture engine; for your own engines, read the schema on the engine's
        Contract page or in its downloaded OpenAPI file.
      tags: [EngineManagement]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
      responses:
        '200':
          description: Versions, newest first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EngineVersionsListResponse'
              example:
                versions:
                  - version: '1.0'
                    released_at: '2026-07-10T12:00:00Z'
                    changelog: Initial generic fixture-engine contract.
                    request_schema_hash: ddb18d50ae06c1a59bcfed45357bde5e11d323bff873420beca76069106bf8d5
                    response_schema_hash: 71a63e58737dbb2debe6d162dcac7bee729b4a24de2d25106887339f3d485ef2
                    is_breaking_from_previous_major: false
                    gate_verified: true
        '400':
          description: '`missing_engine_slug`: the slug in the path is blank (for example a single encoded space).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/EngineNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          description: '`insufficient_capability` (a scoped key without `engines:read`), `engine_binding_forbidden` (the key is not bound to this engine) or `engine_archived`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          description: |
            `engine_config_hash_unavailable`: the engine is still initializing and
            its configuration hash is not yet recorded. Transient; retry shortly.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                engineConfigHashUnavailable:
                  value:
                    code: engine_config_hash_unavailable
                    message: Engine is initializing. Try again shortly.
                authUnavailable:
                  value:
                    code: auth_unavailable
                    message: API key authentication is temporarily unavailable
        '504':
          $ref: '#/components/responses/RequestTimeout'
    post:
      operationId: publishEngineVersion
      summary: Publish an engine version
      description: |
        Release the engine's current saved config as a new immutable version.
        On engines with the default pin policy, unpinned runs execute the
        latest release, so this is the step that moves a validated config
        change into released traffic. `PUT /config` changes only the draft,
        which only a sandbox key's run pinned to `engine_version: draft`
        executes (`expose_warnings`, `notify_on_supersede` and
        `quotas.requests_per_minute` are read from the saved configuration).

        The platform decides the version number. The first release is `1.0`.
        On engines with the default pin policy, a breaking change to the
        declared input or output (an output field removed or retyped, a new
        required input, an input retyped, an optional input made required)
        bumps the major; everything else bumps the minor. On `exact_required`
        engines any change to the request or response schema hash bumps the
        major, with reasons `request_schema_changed` and
        `response_schema_changed`.

        `version` is enforced on `exact_required` engines: when supplied it
        must equal the computed version, or the publish is refused with `409
        engine_version_publish_mismatch` (or `409
        engine_version_schema_change_requires_major` when the schemas
        changed). On other engines it is ignored.
        `is_breaking_from_previous_major` is always ignored.

        Accepts only the organization API key; every scoped key receives
        `403 insufficient_capability`.

        Re-publishing an unchanged config returns the current latest version
        with `already_released: true` instead of minting a new one.
      tags: [EngineManagement]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                changelog:
                  type: string
                  description: Human-readable summary recorded on the version row.
                version:
                  type: string
                  description: On `exact_required` engines, the version you expect; refused with 409 when it differs from the computed version. Ignored on other engines.
                is_breaking_from_previous_major:
                  type: boolean
                  description: Accepted for compatibility; the schema comparator decides.
      responses:
        '200':
          description: |
            The released version: freshly minted, or the existing latest
            release (`already_released: true`) when the saved config is
            already released. The body omits `request_schema_hash` and
            `response_schema_hash`; read them from
            `GET /api/v1/engines/{engine_slug}/versions/{version}`.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/EngineVersion'
                  - type: object
                    properties:
                      already_released:
                        type: boolean
                        description: Present and true on the idempotent no-op.
                      bump:
                        type: string
                        enum: [major, minor]
                        description: Comparator verdict for a fresh publish. A first release reports `minor`.
                      reasons:
                        type: array
                        items: {type: string}
                        description: Breaking-change reasons when `bump` is `major`.
              example:
                version: '2.0'
                released_at: '2026-06-11T18:00:00Z'
                changelog: 'v2 config: cover letter + budget review outputs.'
                is_breaking_from_previous_major: true
                gate_verified: false
                bump: major
                reasons: ['output field removed: output.readiness_score']
        '400':
          description: '`invalid_request`: the body is not valid JSON or is over 1 MiB. `missing_engine_slug`: the slug in the path is blank.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: invalid_request
                message: Request body is not valid JSON
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: |
            `insufficient_capability`: every scoped key is refused; publishing takes an
            organization key. `engine_archived`: the engine is archived.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                insufficientCapability:
                  value:
                    code: insufficient_capability
                    message: API key does not have permission for this action
                engineArchived:
                  value:
                    code: engine_archived
                    message: Engine is archived
        '404':
          $ref: '#/components/responses/EngineNotFound'
        '409':
          description: |
            The engine's config was saved again between the eval gate
            evaluating the candidate and the release being written
            (`engine_config_changed`).

            `engine_config_changed` is retryable: publishing again re-runs the
            gate against the config that is live now. It does not mean the
            config was rejected.

            `engine_version_publish_mismatch` and `engine_version_schema_change_requires_major`
            (exact-required engines) carry `details` with `requested_version`,
            `required_version` and, for the second, `reasons`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_config_changed
                message: 'release blocked: the engine config changed after the eval gate evaluated it; publish again to re-run the gate against the current config'
        '422':
          description: |
            The live config could not be parsed for schema derivation
            (`invalid_engine_config`), or the release is blocked by the eval
            gate (`cold_start_gate_not_met`, `cold_start_gate_failed`,
            `cold_start_gate_regressed`). The gate runs on every release, not
            only a first one, and never blocks an engine with no evaluation set.
            On a first release the candidate config must pass every evaluation
            set that has a mismatch tolerance or, when no set has one, have a
            completed eval with no case errors; on a later release the gate blocks only when an
            evaluation set with a mismatch tolerance does not pass. The `cold_start_` prefix is retained
            for compatibility with clients that already match on these codes.

            `provider_not_approved`, `egress_manifest_version_required` or
            `egress_manifest_version_mismatch` when the configured model's provider or
            the egress pin is not acceptable.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: cold_start_gate_not_met
                message: "release blocked: the config being released has no passing eval on this engine's corpus; run one against this candidate config hash before publishing"
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: '`publish_failed`: the release could not be written or read back, or the saved config fails release-time validation (fix the config with `PUT /config`, then publish again). `internal_error` or another operation-specific code for other internal failures.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            `engine_config_hash_unavailable`: the engine is still initializing and
            its configuration hash is not yet recorded. Transient; retry shortly.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                engineConfigHashUnavailable:
                  value:
                    code: engine_config_hash_unavailable
                    message: Engine is initializing. Try again shortly.
                authUnavailable:
                  value:
                    code: auth_unavailable
                    message: API key authentication is temporarily unavailable
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/engines/{engine_slug}/versions/{version}:
    get:
      operationId: getEngineVersion
      summary: Get engine version
      description: |
        Return a specific released version by `major.minor` (e.g. `1.0`).
        Bare-integer pins (`1`) and three-tier semver (`1.0.3`) are
        rejected: versioning is two-tier by design.

        Scoped partner keys require `engines:read` and an explicit engine
        binding. This route returns hashes and release metadata, not schema
        bodies. The supported-version registry publishes schema bytes only for
        the generic fixture engine; for your own engines, read the schema on
        the engine's Contract page or in its downloaded OpenAPI file.
      tags: [EngineManagement]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
        - in: path
          name: version
          required: true
          description: Released version as `major.minor`, for example `1.0`.
          schema:
            type: string
            pattern: '^\d+\.\d+$'
          example: '1.0'
      responses:
        '200':
          description: Single version row.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EngineVersion'
        '400':
          description: '`engine_version_invalid_format`: the version is not `major.minor` or a segment is too large. `missing_engine_slug`: the slug in the path is blank.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_version_invalid_format
                message: Use major.minor (e.g. 1.0). Three-tier semver and bare major are not supported.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: |
            `engine_not_found`: no engine with this slug in the organization.
            `engine_version_not_found`: the engine has no release with this number.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_version_not_found
                message: Engine version not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '403':
          description: '`insufficient_capability` (a scoped key without `engines:read`), `engine_binding_forbidden` (the key is not bound to this engine) or `engine_archived`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          description: |
            `engine_config_hash_unavailable`: the engine is still initializing and
            its configuration hash is not yet recorded. Transient; retry shortly.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                engineConfigHashUnavailable:
                  value:
                    code: engine_config_hash_unavailable
                    message: Engine is initializing. Try again shortly.
                authUnavailable:
                  value:
                    code: auth_unavailable
                    message: API key authentication is temporarily unavailable
        '504':
          $ref: '#/components/responses/RequestTimeout'

  # ── Runs ───────────────────────────────────────────────────────────

  /api/v1/engines/{engine_slug}/runs:
    post:
      operationId: submitRun
      summary: Submit Run
      description: |
        Submit a payload for asynchronous processing via an engine-scoped endpoint.
        The API queues the run immediately and returns a `202 Accepted` response
        with a `run_id`.

        Scoped partner keys require `runs:write` and an explicit binding to
        this engine. Engines with the `exact_required` pin policy reject an
        omitted `engine_version` with `engine_version_required` and reject an
        `N.x` pin with `engine_version_exact_required`.

        The shape of `input` depends on the engine's type and configuration:
        see [Engine types](/engines/overview#engine-types), the engine's
        Contract page in the portal, and `request_schema` from
        `GET /api/v1/engines/{engine_slug}/config`.

        After submission, poll `GET /api/v1/runs/{run_id}` or receive a
        [webhook](/api-reference/webhooks/overview). Statuses, idempotency
        and limits: [Runs](/engines/runs).
      tags: [Engines]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
        - name: Idempotency-Key
          in: header
          required: false
          description: |
            Optional retry key, at most 255 characters (`400
            invalid_idempotency_key` otherwise). Scoped to the organization,
            the key's environment, the submitter (the acting principal, or the
            API key when none is sent) and the key value. The same key with the
            same request returns the original run with `202` and creates
            nothing. The same key with a changed request returns `409
            idempotency_key_reused` with the original `run_id` in `details`.
            Keys have no timer; the binding lasts as long as the run.
          schema:
            type: string
            maxLength: 255
        - name: X-Nexio-Acting-Principal
          in: header
          required: false
          description: |
            The person in your organization the run acts for. An engine of the
            `matching` type refuses a submission without it
            (`400 run_requires_acting_principal`).
            A nonempty body `submitted_by` must equal this header after trimming,
            and is refused when the header is absent
            (`400 acting_principal_mismatch`).
          schema:
            type: string
        - $ref: '#/components/parameters/ActingEmailHeader'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SubmitRunRequest'
            example:
              input:
                request_id: harbor-vendor-0042
                vendor:
                  name: Example Logistics
                  country: US
                  annual_spend_usd: 480000
                  certifications:
                    - ISO 9001
      responses:
        '202':
          description: Run accepted. A new run is queued. A retry with the same `Idempotency-Key` and request returns the existing run with its current status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SubmitRunResponse'
              example:
                run_id: bcb87157-0bfc-404d-a120-7f5c9cd01037
                status: queued
        '400':
          description: |
            Invalid request, or the engine's stored type is not a supported engine type.

            Codes: `invalid_request` (malformed JSON or an unknown field),
            `missing_engine_slug`, `invalid_idempotency_key`, `acting_principal_mismatch`,
            `run_requires_acting_principal`, `missing_input`, `invalid_input`,
            `invalid_offerings`, `invalid_engine_type`,
            `engine_version_invalid_format`, `engine_version_not_found`,
            `engine_version_required`, `engine_version_exact_required`,
            `engine_version_draft_requires_sandbox_key`, `test_scenario_sandbox_only`,
            `test_scenario_exact_version_required`, `test_scenario_version_not_supported`,
            `invalid_test_scenario`, and `request_bound_exceeded` for a per-value bound
            (string length, array items, object fields, object depth).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalidRequest:
                  summary: Malformed JSON
                  value:
                    code: invalid_request
                    message: Request body is not valid JSON
                invalidEngineType:
                  summary: The engine's stored type is not a supported engine type
                  value:
                    code: invalid_engine_type
                    message: 'engine_type must be one of: comparison, diligence, entity_analysis, matching, opportunity, triage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: |
            `insufficient_capability`: a scoped key without `runs:write`.
            `engine_binding_forbidden`: the scoped key is not bound to this engine.
            `engine_archived`: the engine is archived. `test_scenario_forbidden`:
            `test_scenario` was sent by a key that is not a scoped key holding
            `runs:test`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                insufficientCapability:
                  value:
                    code: insufficient_capability
                    message: API key does not have permission for this action
                testScenarioForbidden:
                  value:
                    code: test_scenario_forbidden
                    message: The authenticated principal does not have the runs:test capability.
        '404':
          $ref: '#/components/responses/EngineNotFound'
        '409':
          description: The idempotency key is already bound to a different request.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: idempotency_key_reused
                message: Idempotency-Key was already used with a different request
                details:
                  run_id: bcb87157-0bfc-404d-a120-7f5c9cd01037
        '413':
          description: The HTTP envelope or normalized JSON payload exceeds its byte limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RequestBoundError'
        '422':
          description: The resolved release of an engine of the `matching` type cannot be served (`engine_release_unservable`), or a platform-assembled submission is refused (`submission_stamp_<reason>`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Per-minute rate limit or monthly run cap exceeded.
          headers:
            Retry-After:
              description: Seconds until the RPM window resets. Present for `rate_limited`.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rateLimited:
                  value:
                    code: rate_limited
                    message: Rate limit exceeded
                monthlyCap:
                  value:
                    code: run_cap_exceeded
                    message: Monthly run cap reached for this organization. Contact support to raise the limit.
        '500':
          description: No version is released and none was pinned (`engine_version_none_released`), or an internal failure.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_version_none_released
                message: This engine has no released version to run against. A version must be published before it can take traffic.
        '503':
          description: |
            `queue_unreachable`: the run row was created but its job could not
            be queued, and the platform marks the run `failed`. A retry with the
            same `Idempotency-Key` returns that run instead of a new one, so
            retry with a new key.
            `engine_config_version_unavailable`: the released configuration
            the pin resolved to is briefly unreadable; retry. A `submission_stamp_*`
            code when the platform cannot verify a stamped submission right now;
            no run was created, so retry shortly.

            `engine_config_hash_unavailable`: the engine is still initializing and
            its configuration hash is not yet recorded. Transient; retry shortly.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                authUnavailable:
                  summary: API key authentication temporarily unavailable
                  value:
                    code: auth_unavailable
                    message: API key authentication is temporarily unavailable
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/engines/{engine_slug}/runs/{run_id}/outcomes:
    post:
      operationId: submitOutcome
      summary: Submit Run Outcome
      description: |
        Record what happened after a run. Each event carries a caller-supplied `event_id`
        for idempotent replay, an `event_type`, and a typed `payload` whose
        shape depends on the event type.

        The event types and payloads are fixed by the platform and are the
        same for every engine. The identifier field names
        (`accepted_carrier_id`, `chosen_carrier_id`, `carrier_id`,
        `broker_id`), the `placement_outcome` type with its `status` values,
        and the `reason_code` list are a fixed set. Identifiers are stored as
        sent and are not checked against the run.

        Event types:

        - `viewed`: a person looked at the result.
        - `accepted`: the recommended alternative was taken.
        - `overridden`: a different alternative was chosen, with a `reason_code`
          (one of `appetite_mismatch`, `better_commission`, `broker_preference`,
          `carrier_appetite`, `claims_service_concern`, `client_preference`,
          `coverage_gap`, `customer_preference`, `existing_carrier_relationship`,
          `jurisdiction_issue`, `other`, `price`, `pricing_not_competitive`,
          `prior_bind_history`). The optional `reason_taxonomy_version` must be
          `2026-08-31-unified`. The server stamps that version when it is absent or empty.
        - `placement_outcome`: the final business result, with `status` one of
          `quoted`, `bound`, `lost`, `declined`.

        Idempotency. An `event_id` is unique within the organization and the
        key's environment bucket (live, or test for every sandbox together).
        Re-posting the same `event_id` with the same run, engine, event type
        and payload returns `200 OK` (a no-op replay). A first write returns
        `201 Created`. Re-using an `event_id` with anything different, or from
        another sandbox in the same bucket, is rejected with `409 Conflict`
        (`event_id_reused`).

        `reason_text` (at most 1,000 characters) is required when `reason_code`
        is `other`. Timestamps are RFC 3339 with an offset of at most 23 hours.
        The run must belong to the engine in the path.
      tags: [Engines]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
        - in: path
          name: run_id
          required: true
          schema:
            type: string
            format: uuid
          example: bcb87157-0bfc-404d-a120-7f5c9cd01037
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/OutcomeViewedRequest'
                - $ref: '#/components/schemas/OutcomeAcceptedRequest'
                - $ref: '#/components/schemas/OutcomeOverriddenRequest'
                - $ref: '#/components/schemas/OutcomePlacementOutcomeRequest'
              discriminator:
                propertyName: event_type
                mapping:
                  viewed: '#/components/schemas/OutcomeViewedRequest'
                  accepted: '#/components/schemas/OutcomeAcceptedRequest'
                  overridden: '#/components/schemas/OutcomeOverriddenRequest'
                  placement_outcome: '#/components/schemas/OutcomePlacementOutcomeRequest'
            examples:
              viewed:
                summary: A person looked at the result
                value:
                  event_id: a2c4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81
                  event_type: viewed
                  payload:
                    viewed_at: '2026-06-17T11:58:00Z'
              accepted:
                summary: Recommended alternative accepted
                value:
                  event_id: 7e1d2c3b-1111-4444-8888-aaaaaaaaaaaa
                  event_type: accepted
                  payload:
                    accepted_at: '2026-06-17T12:00:00Z'
                    accepted_carrier_id: example-logistics
              overridden:
                summary: A different alternative was chosen
                value:
                  event_id: 9a2b3c4d-2222-4444-8888-bbbbbbbbbbbb
                  event_type: overridden
                  payload:
                    overridden_at: '2026-06-17T12:05:00Z'
                    chosen_carrier_id: example-freight
                    reason_code: price
                    reason_text: 'Lower cost for the same scope of work.'
                    reason_taxonomy_version: '2026-08-31-unified'
              placement_outcome:
                summary: Final business result
                value:
                  event_id: c4d5e6f7-3333-4444-8888-cccccccccccc
                  event_type: placement_outcome
                  payload:
                    outcome_at: '2026-06-20T09:00:00Z'
                    status: bound
                    carrier_id: example-logistics
      responses:
        '201':
          description: Outcome recorded.
          content:
            application/json:
              schema:
                type: object
                required: [event_id, status]
                properties:
                  event_id:
                    type: string
                  status:
                    type: string
                    enum: [recorded]
              example:
                event_id: 7e1d2c3b-1111-4444-8888-aaaaaaaaaaaa
                status: recorded
        '200':
          description: Idempotent replay. The same event_id and payload was already recorded.
          content:
            application/json:
              schema:
                type: object
                required: [event_id, status]
                properties:
                  event_id:
                    type: string
                  status:
                    type: string
                    enum: [recorded]
              example:
                event_id: 7e1d2c3b-1111-4444-8888-aaaaaaaaaaaa
                status: recorded
        '400':
          description: 'Invalid outcome. Codes: `invalid_request` (malformed JSON, an unknown top-level field, or a body over 1 MiB), `missing_event_id`, `invalid_event_id`, `invalid_event_type`, `invalid_payload`, `reason_text_too_long`, `reason_taxonomy_version_unknown`, `invalid_run_id` (not a UUID, or not in canonical 36-character form), `missing_run_id`, `missing_engine_slug`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`insufficient_capability`: a scoped key without `runs:write`. `engine_binding_forbidden`: the scoped key is not bound to the run''s engine.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_binding_forbidden
                message: API key is not bound to this run's engine
        '404':
          description: '`run_not_found`: no run with this ID in this organization and environment on the engine named by `engine_slug`. An unknown slug also answers `run_not_found`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: run_not_found
                message: Run not found
        '409':
          description: '`event_id_reused`: the `event_id` was already recorded with a different run, engine, event type or payload, or by another sandbox in the same environment bucket.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: event_id_reused
                message: event_id was previously used for a different run, engine, event_type, or payload
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/runs/{run_id}/cancel:
    post:
      operationId: cancelRun
      summary: Cancel Run
      description: |
        Request cancellation of a queued or running run. The first request
        records its timestamp and reason. Repeated requests preserve that
        original request. A queued run becomes terminal immediately. A running
        run stops cooperatively before terminal publication.

        Requires `runs:write`. A scoped key not bound to the run's engine gets
        `403 engine_binding_forbidden`. A run in another organization or
        environment returns `404 run_not_found`. See [Runs](/engines/runs#cancel-a-run).
      tags: [Runs]
      parameters:
        - name: run_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                reason:
                  type: string
                  description: Free text stored with the first cancel request, trimmed. The whole body is limited to 16 KiB.
      responses:
        '200':
          description: |
            The run was already terminal and is unchanged. The body is the
            run's status fields and stored `output`, built without solutions:
            it carries no `solutions`, `input`, `work_items`, `warnings`,
            `computed_at_head`, `served_head` or `stale`. `output` is the
            stored output as written: a matching run's operator block is not
            trimmed and no acting principal's field policy is applied. Read the
            served result with `GET /api/v1/runs/{run_id}`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunStatusResponse'
        '202':
          description: |
            Cancellation was accepted. `status` is `cancelled` when the run was
            queued and `processing` when it was running.
          content:
            application/json:
              schema:
                type: object
                required: [run_id, status, cancel_requested_at]
                properties:
                  run_id:
                    type: string
                    format: uuid
                  status:
                    type: string
                  cancel_requested_at:
                    type: string
                    format: date-time
        '400':
          description: '`invalid_run_id`: `run_id` is not a UUID. `invalid_request`: the body is not JSON, carries a field other than `reason`, or exceeds 16 KiB.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Missing `runs:write`, or the scoped key is not bound to the run's engine.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_binding_forbidden
                message: API key is not bound to this run's engine
        '404':
          description: Run not found in this organization and environment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: run_not_found
                message: Run not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: '`load_run_failed`, `cancel_run_failed`, `run_lookup_failed`, or another internal code. Retry; the first cancel request''s timestamp and reason are kept.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/runs/{run_id}:
    get:
      operationId: getRunStatus
      summary: Get Run Status
      description: |
        Retrieve a run's status and, once it is terminal, its output. Poll
        until `status` is `completed`, `degraded`, `failed` or `cancelled`.
        `output` and `solutions` are served only on `completed` and
        `degraded` runs, with `output_phase: final`.
        `completed_deterministic_at` can appear while a run of the
        `comparison` type is still `processing`; it is a timing fact, not a
        result.

        Run IDs are unique across engines, so no engine slug is needed.

        Scoped keys require `runs:read` and a binding to the run's engine
        (`403 engine_binding_forbidden` otherwise). A run in another
        organization or environment returns `404 run_not_found`.

        `include` accepts `input` (return the admitted submission) and, for
        runs of the `matching` type, `operator` (return the operator block as
        stored, without the trim; the full audit record behind
        `operator.blob_ref` is not served by this API).

        With `X-Nexio-Acting-Principal`, every `output` key in a field class
        the person's role policy denies is removed at any depth; if the policy
        cannot be resolved, every class the filter covers is removed. The
        filtered classes are in the access vocabulary. `solutions` is not
        filtered.

        Statuses, transitions, degraded semantics, idempotency and polling
        guidance: [Runs](/engines/runs).
      tags: [Runs]
      parameters:
        - name: run_id
          in: path
          required: true
          description: The run identifier returned by `POST /api/v1/engines/{engine_slug}/runs`.
          schema:
            type: string
            format: uuid
        - name: include
          in: query
          required: false
          description: Comma-separated. `input` adds the admitted submission; `operator` returns the operator block of a `matching`-type run as stored, without the trim.
          schema:
            type: string
          example: input
        - name: X-Nexio-Acting-Principal
          in: header
          required: false
          description: |
            The person reading the run. Keys in field classes that person's
            role policy denies are removed from `output`; when the policy
            cannot be resolved, every class the filter covers is removed. Does
            not change which runs the key can read.
          schema:
            type: string
      responses:
        '200':
          description: Current run status and results (if completed).
          headers:
            Server-Timing:
              description: Stage timings for this read. Diagnostic only; the stage names can change.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RunStatusResponse'
              examples:
                queued:
                  summary: Queued
                  value:
                    run_id: 3f6c2a8e-5b1d-4c7e-9a2f-8d4b6e1c0a57
                    engine_type: entity_analysis
                    engine_version: '1.0'
                    engine_config_version_hash: 5c1e9a7b3d2f4e60
                    status: queued
                    environment: test
                    attempt: 1
                    created_at: '2026-09-23T14:02:11Z'
                processing:
                  summary: Processing
                  value:
                    run_id: 3f6c2a8e-5b1d-4c7e-9a2f-8d4b6e1c0a57
                    engine_type: entity_analysis
                    engine_version: '1.0'
                    engine_config_version_hash: 5c1e9a7b3d2f4e60
                    status: processing
                    environment: test
                    attempt: 1
                    trace_id: 4bf92f3577b34da6a3ce929d0e0e4736
                    created_at: '2026-09-23T14:02:11Z'
                    started_at: '2026-09-23T14:02:12Z'
                completed:
                  summary: Completed entity analysis run
                  value:
                    run_id: 8d2f4b61-0c9e-4a3b-b7d5-1e6a9c0f2b47
                    engine_type: entity_analysis
                    engine_version: '1.0'
                    engine_config_version_hash: 5c1e9a7b3d2f4e60
                    status: completed
                    environment: test
                    attempt: 1
                    created_at: '2026-09-23T15:04:05Z'
                    started_at: '2026-09-23T15:04:06Z'
                    completed_at: '2026-09-23T15:04:19Z'
                    duration_ms: 12840
                    total_duration_ms: 14102
                    trace_id: 4bf92f3577b34da6a3ce929d0e0e4736
                    output_phase: final
                    output:
                      response_type: VENDOR_REVIEW
                      request_id: harbor-vendor-0042
                      diagnostics: []
                      profile_summary:
                        vendor_name: Example Logistics
                        country: US
                      gaps:
                        - id: gap_001
                          severity: MEDIUM
                          category: SECURITY
                          title: No security attestation on file
                          description: The vendor lists ISO 9001 but no information security attestation.
                          recommendation: Request a current SOC 2 Type II report before onboarding.
                          data_sources:
                            - input.vendor.certifications
                      summary:
                        total_gaps: 1
                        high_severity: 0
                        medium_severity: 1
                        low_severity: 0
                completed_declared_contract:
                  summary: Completed declared-contract run
                  description: |
                    A run of the `vendor-intake` engine from
                    [Declared-contract engines](/engines/guides/contract-mode).
                    The output has exactly the declared keys.
                  value:
                    run_id: 8e4b2c7a-1f5d-4a3e-9c6b-0d2f8a7e5b14
                    engine_type: entity_analysis
                    engine_version: '1.0'
                    engine_config_version_hash: 5d20b8e4c1f7a693
                    status: completed
                    environment: test
                    attempt: 1
                    created_at: '2026-09-23T14:02:11Z'
                    started_at: '2026-09-23T14:02:11Z'
                    completed_at: '2026-09-23T14:02:12Z'
                    duration_ms: 668
                    total_duration_ms: 1000
                    trace_id: 4bf92f3577b34da6a3ce929d0e0e4736
                    output_phase: final
                    output:
                      response_type: vendor_intake
                      request_id: 8e4b2c7a-1f5d-4a3e-9c6b-0d2f8a7e5b14
                      vendor:
                        annual_spend_usd: 480000
                        country: US
                        name: Example Logistics
                      next_step: Request a current security attestation from Example Logistics before onboarding.
                      standard: supplier-onboarding-2026
                failed:
                  summary: Failed on declared input
                  value:
                    run_id: b7e2d9c4-1a3f-4e6b-8c5d-2f9a0e7b6c13
                    engine_type: entity_analysis
                    engine_version: '1.0'
                    engine_config_version_hash: 5d20b8e4c1f7a693
                    status: failed
                    environment: test
                    stage: FAILED
                    attempt: 1
                    error: required input vendor.name is missing or empty
                    error_details:
                      type: validation_error
                      message: required input vendor.name is missing or empty
                      stage: INTAKE
                      step: parse_submission
                      attempt: 1
                      max_attempts: 3
                      retryable: false
                    trace_id: 0af7651916cd43dd8448eb211c80319c
                    created_at: '2026-09-23T14:05:40Z'
                    started_at: '2026-09-23T14:05:41Z'
                    completed_at: '2026-09-23T14:05:42Z'
                    total_duration_ms: 1807
        '400':
          description: '`invalid_run_id`: `run_id` is not a UUID.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: invalid_run_id
                message: Run ID must be a valid UUID
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Missing `runs:read`, or the scoped key is not bound to the run's engine.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_binding_forbidden
                message: API key is not bound to this run's engine
        '404':
          description: Run not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: run_not_found
                message: Run not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '500':
          description: '`load_run_failed`, `load_solutions_failed` or `load_work_items_failed`: part of the run could not be read. `run_lookup_failed`: a scoped key''s engine binding could not be checked. Retry.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/runs/{run_id}/defensibility-packet:
    get:
      operationId: getRunDefensibilityPacket
      summary: Get Run Defensibility Packet
      description: |
        Return the portable sourcing chain for a run: a single
        document a client, board, or auditor can read to see how the output
        was reached. A scoped key without `runs:defensibility:read` gets `403
        insufficient_capability`; one not bound to the run's engine gets `403
        engine_binding_forbidden`; a run in another organization or environment
        returns `404 run_not_found`.

        The packet bundles the run identity and lineage, the customer-visible
        `output`, the per-element `provenance` map (with a
        `provenance_coverage` marker that reads `none` when the run recorded no
        provenance map, so an absent map never looks sourced-clean), the external
        (enrichment) calls that fed the run, the event timeline (the first
        1,000 events), and an LLM cost/token summary.

        Model calls are attested by `stage` so the chain is complete, but the
        prompt and completion bodies are withheld. That prompt-engineering
        detail is the engine's implementation IP and is never part of the
        customer contract.
      tags: [Engines]
      parameters:
        - in: path
          name: run_id
          required: true
          schema:
            type: string
            format: uuid
          example: bcb87157-0bfc-404d-a120-7f5c9cd01037
      responses:
        '200':
          description: The customer-facing defensibility packet.
          content:
            application/json:
              schema:
                type: object
                required: [version, run, provenance_coverage, model_calls, external_calls, events, llm_summary, redaction]
                properties:
                  version:
                    type: string
                    example: '1'
                  run:
                    type: object
                    description: Run identity and lineage.
                    required: [id, status, started_at, environment, cost_rollup]
                    properties:
                      id:
                        type: string
                        format: uuid
                      status:
                        type: string
                        description: The stored status in upper case (`PENDING`, `RUNNING`, `COMPLETED`, `DEGRADED`, `FAILED`, `CANCELLED`), unlike the lower-case `status` of `GET /api/v1/runs/{run_id}`.
                      started_at:
                        type: string
                        format: date-time
                        description: When the run was created.
                      completed_at:
                        type: string
                        format: date-time
                      engine_id:
                        type: string
                      engine_type:
                        type: string
                      environment:
                        type: string
                      engine_config_version_hash:
                        type: string
                      trace_id:
                        type: string
                      submitted_by:
                        type: string
                      submitted_by_label:
                        type: string
                        description: A readable label for `submitted_by`, for example the API key name.
                      parent_run_id:
                        type: string
                      triggered_by_run_id:
                        type: string
                      error_details:
                        type: object
                        additionalProperties: true
                      warning_details: {}
                      cost_rollup:
                        type: object
                        required: [input_tokens, output_tokens, cost_micros]
                        properties:
                          input_tokens:
                            type: integer
                          output_tokens:
                            type: integer
                          cost_micros:
                            type: integer
                  output:
                    type: object
                    description: The run's stored output. Unlike `GET /api/v1/runs/{run_id}`, the matching `operator` block is not trimmed and no per-reader field policy is applied.
                  provenance:
                    type: object
                    description: Per-element provenance map, present when the run recorded one.
                  provenance_coverage:
                    type: string
                    enum: [present, none]
                    description: '`none` when the run recorded no provenance map; an absent map must not read as sourced-clean.'
                  model_calls:
                    type: array
                    description: Stage attestation that a model call ran, without the prompt or completion body.
                    items:
                      type: object
                      properties:
                        stage:
                          type: string
                        blob_type:
                          type: string
                        blob:
                          type: 'null'
                          description: Always null. The prompt and completion body are withheld.
                  external_calls:
                    type: array
                    description: Enrichment calls that fed the run, with their payloads.
                    items:
                      type: object
                      properties:
                        stage:
                          type: string
                        blob_type:
                          type: string
                        blob:
                          type: object
                  events:
                    type: array
                    description: The run event timeline in time order, cut off after the first 1,000 events.
                    items:
                      type: object
                  llm_summary:
                    type: object
                    description: Aggregate model cost and token usage for the run.
                  redaction:
                    type: object
                    required: [fields_redacted]
                    properties:
                      fields_redacted:
                        type: integer
                        minimum: 0
        '400':
          description: '`invalid_run_id`: `run_id` is not a UUID. `missing_run_id`: the segment is empty.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: invalid_run_id
                message: run id must be a valid UUID
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`insufficient_capability`: a scoped key without `runs:defensibility:read`. `engine_binding_forbidden`: the scoped key is not bound to the run''s engine.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_binding_forbidden
                message: API key is not bound to this run's engine
        '404':
          description: Run not found in this organization and environment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: run_not_found
                message: run not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/engines/{engine_slug}/runs/{run_id}/annotations:
    post:
      operationId: createAnnotation
      summary: Annotate a Run
      description: |
        Attach human signal to a run your org owns: a `rating`
        (`good` / `bad` / `neutral`), a required free-text `comment`, and an
        optional `target` pointer at the specific output element the note is
        about. This is the programmatic counterpart to annotating in the portal
        (see [Outcomes and annotations](/engines/outcomes-and-annotations)). Annotations
        are not idempotent: each request records a new annotation.

        Scoped by the authenticated API key. A run in another organization or
        environment returns `404 run_not_found`. `engine_slug` is not checked
        against the run: the annotation attaches to `run_id`. Unknown body fields are ignored. Submissions are stamped `source: api` so customer-API signal is
        distinguishable from portal or review signal.
      tags: [Engines]
      parameters:
        - $ref: '#/components/parameters/EngineSlug'
        - in: path
          name: run_id
          required: true
          schema:
            type: string
            format: uuid
          example: bcb87157-0bfc-404d-a120-7f5c9cd01037
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rating, comment]
              properties:
                rating:
                  type: string
                  enum: [good, bad, neutral]
                comment:
                  type: string
                  minLength: 1
                  description: Required free-text note with at least one non-whitespace character.
                  example: The recommendation matched our own review of this supplier.
                target:
                  type: [object, 'null']
                  description: Optional pointer at the output element the note is about. Any JSON object.
                submitter_id:
                  type: [string, 'null']
                  description: Optional caller-supplied identifier for who submitted the signal.
                time_on_task_seconds:
                  type: [integer, 'null']
                  minimum: 0
                  maximum: 86399
                  description: Seconds the person spent reviewing.
      responses:
        '201':
          description: Annotation recorded.
          content:
            application/json:
              schema:
                type: object
                required: [id, org_id, run_id, rating, comment, source, created_at, updated_at]
                properties:
                  id:
                    type: string
                    format: uuid
                  org_id:
                    type: string
                  run_id:
                    type: string
                    format: uuid
                  target:
                    type: [object, 'null']
                    additionalProperties: true
                  rating:
                    type: string
                    enum: [good, bad, neutral]
                  comment:
                    type: string
                  submitter_id:
                    type: string
                  source:
                    type: string
                    example: api
                  time_on_task_seconds:
                    type: integer
                  created_at:
                    type: string
                    format: date-time
                    description: Microsecond precision.
                  updated_at:
                    type: string
                    format: date-time
        '400':
          description: 'Codes: `invalid_request` (body is not JSON or is over 1 MiB), `missing_run_id`, `invalid_run_id`, `invalid_rating`, `missing_comment`, `invalid_target` (not a JSON object or null), `invalid_time_on_task`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: missing_comment
                message: comment is required and cannot be empty
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Missing `runs:write`, or the scoped key is not bound to the run's engine.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: engine_binding_forbidden
                message: API key is not bound to this run's engine
        '404':
          description: Run not found in this organization and environment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: run_not_found
                message: run not found
        '409':
          description: '`target.kind` is `opportunity`. This route does not annotate opportunity rows.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: scoped_annotation_required
                message: Use the scoped Opportunity annotation resource
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/environments:
    post:
      operationId: createEnvironment
      summary: Create environment
      description: |
        Create a sandbox environment under your org. Accepts organization keys
        only; a scoped key gets 403 `insufficient_capability`. The slug must
        match `^[a-z0-9_]{1,16}$`, cannot be one of the reserved slugs `live`,
        `test`, `internal`, `admin`, `api`, `nexio`, `draft`, `all`, and cannot
        change later. Creating an environment when the org already has 5
        sandbox environments is refused with `environment_limit_reached`. The
        body is at most 4 KiB and unknown fields are rejected.
      tags: [Environments]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [slug]
              properties:
                slug:
                  type: string
                  pattern: '^[a-z0-9_]{1,16}$'
                  not:
                    enum: [live, test, internal, admin, api, nexio, draft, all]
                  example: dev
                name:
                  type: [string, 'null']
                  description: Optional display label, at most 64 UTF-8 bytes. An empty string or null leaves it unset.
                  example: Development
      responses:
        '201':
          description: Environment created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Environment'
        '400':
          description: A malformed or mistyped body, an unknown field, or a body over 4 KiB (`invalid_request`), invalid slug (`environment_slug_invalid`), reserved slug (`environment_slug_reserved`), slug already used (`environment_slug_taken`), name over 64 UTF-8 bytes (`environment_name_invalid`), or 5 sandboxes already exist (`environment_limit_reached`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                slugTaken:
                  value:
                    code: environment_slug_taken
                    message: An environment with this slug already exists in your org
                limitReached:
                  value:
                    code: environment_limit_reached
                    message: Maximum non-live environments reached; contact support to raise the cap
                slugReserved:
                  value:
                    code: environment_slug_reserved
                    message: 'slug is reserved; choose another (avoid: live, test, internal, admin, api, nexio, draft, all)'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OrganizationKeyRequired'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The environment could not be created (`environment_operation_failed`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    get:
      operationId: listEnvironments
      summary: List environments
      description: |
        Return every environment in your org, including the `live`
        environment, whatever environment the calling key belongs to.
        Accepts organization keys only; a scoped key gets 403
        `insufficient_capability`.
      tags: [Environments]
      responses:
        '200':
          description: Environments in the org.
          content:
            application/json:
              schema:
                type: object
                required: [environments]
                properties:
                  environments:
                    type: array
                    items:
                      $ref: '#/components/schemas/Environment'
              example:
                environments:
                  - id: 0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d
                    slug: live
                    kind: live
                    name: Production
                    created_at: '2026-01-10T00:00:00Z'
                  - id: 1b2c3d4e-5f6a-7b8c-9d0e-1f2a3b4c5d6e
                    slug: dev
                    kind: sandbox
                    name: Development
                    created_at: '2026-02-01T00:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OrganizationKeyRequired'
        '429':
          $ref: '#/components/responses/RateLimited'

        '500':
          description: The list could not be read (`list_environments_failed`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /api/v1/environments/{slug}:
    patch:
      operationId: updateEnvironment
      summary: Update environment
      description: |
        Change an environment's `name`, the only mutable field. Send a string
        of at most 64 UTF-8 bytes, or `null` to clear it. An empty body or any
        other field is rejected with 400 `invalid_request`. Accepts
        organization keys only; a scoped key gets 403 `insufficient_capability`.
      tags: [Environments]
      parameters:
        - in: path
          name: slug
          required: true
          schema:
            type: string
          example: dev
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [name]
              properties:
                name:
                  type: [string, 'null']
                  description: At most 64 UTF-8 bytes. `null` or an empty string clears it.
                  example: Dev (shared)
      responses:
        '200':
          description: Updated environment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Environment'
        '400':
          description: '`invalid_request`: malformed JSON, a body over 4 KiB, no `name` field, another field, or a `name` that is neither a string nor null. `environment_name_invalid`: `name` over 64 UTF-8 bytes.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: invalid_request
                message: PATCH body must include the name field. Use null to clear.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OrganizationKeyRequired'
        '404':
          description: No environment with this slug in your org.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: environment_not_found
                message: Environment not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: '`environment_operation_failed`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    delete:
      operationId: deleteEnvironment
      summary: Delete environment
      description: |
        Delete a sandbox environment. The `live` environment cannot be deleted
        (400 `environment_live_immutable`). While any API key (revoked keys
        included), webhook endpoint (deleted endpoints included) or run
        references the environment, the request is refused with 409 and a
        top-level `blockers` object. Accepts organization keys only; a scoped
        key gets 403 `insufficient_capability`.
      tags: [Environments]
      parameters:
        - in: path
          name: slug
          required: true
          schema:
            type: string
          example: dev
      responses:
        '204':
          description: Environment deleted.
        '400':
          description: The live environment cannot be deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: environment_live_immutable
                message: The live environment is managed by the platform and cannot be created or deleted via the API
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OrganizationKeyRequired'
        '404':
          description: No environment with this slug in your org.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: environment_not_found
                message: Environment not found
        '409':
          description: The environment still has bound resources.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EnvironmentInUseError'
              example:
                code: environment_in_use
                message: Environment still has bound API keys, webhook endpoints, or runs. Revoke / delete those first.
                blockers:
                  api_keys: 1
                  webhook_endpoints: 0
                  runs: 37
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: '`environment_operation_failed`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/webhooks:
    post:
      operationId: createWebhook
      summary: Create Webhook Endpoint
      description: |
        Register a new webhook endpoint. Returns the endpoint and a signing
        secret (`whsec_` followed by 64 hex characters). The secret is returned
        only in this response: store it securely.

        Nexio signs every delivery with HMAC-SHA256 using this secret. See
        [Webhooks](/api-reference/webhooks/overview) for verification code.

        `url` must be HTTPS, at most 2048 UTF-8 bytes after trimming, with no user info, not
        `localhost`, and no address it resolves to may be loopback, private, link-local or unspecified.
        Unknown request fields are refused with `invalid_request`.

        You can provide an `auth_token` that Nexio sends as
        `Authorization: Bearer <token>` on every delivery, for gateway filtering.

        Maximum active endpoints per environment: 10.

        Scoped keys require `webhooks:manage`. The endpoint is bound to the
        key's environment; the request cannot choose another.
      tags: [Webhooks]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWebhookRequest'
            example:
              url: https://hooks.example.com/nexio
              events: [run.completed, run.failed, run.cancelled]
              description: Production run callbacks
              payload_mode: full
              auth_token: gw_example_token
      responses:
        '201':
          description: Endpoint created. Secret is included only in this response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CreateWebhookResponse'
              example:
                id: 2f7c1b9e-4a3d-4e8b-9c61-0d5a7e3f8b12
                url: https://hooks.example.com/nexio
                environment: live
                events: [run.completed, run.failed, run.cancelled]
                description: Production run callbacks
                active: true
                auth_token_configured: true
                webhook_version: '2026-03-22'
                payload_mode: full
                created_at: '2026-09-23T14:02:11Z'
                secret: whsec_example000000000000000000000000000000000000000000000000000000000
        '400':
          description: |
            The request failed validation:
            `invalid_request` (body not JSON, an unknown field, or a body over 64 KiB),
            `invalid_url`, `invalid_events`, `invalid_description`,
            `invalid_auth_token`, or `invalid_payload_mode`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: Maximum active endpoints reached.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: webhook_limit_exceeded
                message: maximum active webhook endpoints reached for this environment
        '429':
          $ref: '#/components/responses/RateLimited'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '500':
          description: '`create_webhook_failed`: the endpoint was not created.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          $ref: '#/components/responses/RequestTimeout'

    get:
      operationId: listWebhooks
      summary: List Webhook Endpoints
      description: |
        Returns the webhook endpoints in the calling key's environment. Secrets
        and auth token values are never returned. Scoped keys require
        `webhooks:manage`.
      tags: [Webhooks]
      responses:
        '200':
          description: List of webhook endpoints.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListWebhooksResponse'
              example:
                endpoints:
                  - id: 550e8400-e29b-41d4-a716-446655440000
                    url: https://api.example.com/nexio/webhooks
                    environment: live
                    events: [run.completed, run.failed]
                    description: Production callback
                    active: true
                    auth_token_configured: true
                    webhook_version: '2026-03-22'
                    payload_mode: full
                    created_at: '2026-03-22T12:00:00Z'
                    updated_at: '2026-03-22T12:00:00Z'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/webhooks/{endpointID}:
    patch:
      operationId: updateWebhook
      summary: Update Webhook Endpoint
      description: |
        Update one or more fields on an existing webhook endpoint.

        Allowed fields: `url`, `events`, `description`, `active`, `auth_token`,
        `payload_mode`. Any other field, or a body with none of these, is
        refused with `invalid_request`. Bodies over 64 KiB are refused with
        `invalid_request`.

        Setting `active` to `false` cancels pending deliveries. Setting it to
        `true` clears a system deactivation. Updating `url` affects only new
        deliveries (each delivery keeps its own `target_url`). Set
        `description` or `auth_token` to `null` or to an empty string to clear
        it. An empty `payload_mode` is refused.

        Scoped keys require `webhooks:manage`.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/EndpointID'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWebhookRequest'
            example:
              url: https://api.example.com/nexio/webhooks/v2
              active: true
      responses:
        '200':
          description: Updated endpoint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookEndpoint'
        '400':
          description: |
            The request failed validation:
            `invalid_request` (body not JSON, an unsupported field, no field, or a
            body over 64 KiB), `invalid_url`, `invalid_events`, `invalid_description`,
            `invalid_active`, `invalid_auth_token`, `invalid_payload_mode`,
            `missing_endpoint_id`, or `invalid_endpoint_id`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WebhookNotFound'
        '409':
          description: The update would reactivate an endpoint past the maximum active endpoints for this environment (`webhook_limit_exceeded`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

    delete:
      operationId: deleteWebhook
      summary: Delete Webhook Endpoint
      description: |
        Deactivate the endpoint and cancel its pending deliveries. Historical
        delivery rows are retained for audit and troubleshooting.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/EndpointID'
      responses:
        '204':
          description: Endpoint deleted.
        '400':
          description: |
            `missing_endpoint_id` or `invalid_endpoint_id`: the endpoint ID in the path is blank or not a UUID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WebhookNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/webhooks/{endpointID}/rotate-secret:
    post:
      operationId: rotateWebhookSecret
      summary: Rotate Webhook Secret
      description: |
        Generate a new signing secret for the endpoint. The previous secret
        remains valid according to a server-owned environment policy: 24 hours
        for `live-v1` and five minutes for `sandbox-v1`. The response identifies
        the policy and exact expiration. Callers cannot set the overlap.

        During the overlap window, deliveries include multiple `v1=` values in
        the `X-Nexio-Signature` header: one for each active secret.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/EndpointID'
      responses:
        '200':
          description: New secret generated.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RotateSecretResponse'
              example:
                id: 550e8400-e29b-41d4-a716-446655440000
                secret: whsec_example111111111111111111111111111111111111111111111111111111111
                rotation_policy_id: live-v1
                previous_secret_expires_at: '2026-03-23T12:00:00Z'
        '400':
          description: |
            `missing_endpoint_id` or `invalid_endpoint_id`: the endpoint ID in the path is blank or not a UUID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/WebhookNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/webhooks/{endpointID}/deliveries:
    get:
      operationId: listWebhookDeliveries
      summary: List Webhook Deliveries
      description: |
        Returns an endpoint's deliveries, newest first, 50 per page, with
        status, attempt count, attempt limit, and the last attempt's status
        code. Filter by `status` or `event_type`; page with `cursor`. The
        filters match exactly and are not validated: a value outside the listed
        ones returns an empty list. An unknown endpoint ID also returns an
        empty list. Per-attempt durations are on the delivery
        detail. Scoped keys require `webhooks:manage`.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/EndpointID'
        - name: status
          in: query
          description: Filter deliveries by status.
          schema:
            type: string
            enum: [pending, in_progress, success, dead_letter, cancelled]
        - name: event_type
          in: query
          description: Filter deliveries by event type.
          schema:
            type: string
            enum: [run.completed, run.failed, run.cancelled, run.superseded]
        - name: cursor
          in: query
          description: The `next_cursor` value from the previous page.
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: List of deliveries.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ListDeliveriesResponse'
              example:
                deliveries:
                  - id: d1234567-abcd-4000-8000-000000000001
                    event_id: c4a1e9d2-7b3f-5e60-8a14-2d9f6b0c3e71
                    endpoint_id: 550e8400-e29b-41d4-a716-446655440000
                    run_id: f912ee92-af38-4a4a-a49c-43ac089cc301
                    event_type: run.completed
                    webhook_version: '2026-03-22'
                    target_url: https://api.example.com/nexio/webhooks
                    status: success
                    attempt_count: 1
                    max_attempts: 8
                    next_attempt_at: '2026-03-22T12:00:01Z'
                    next_attempt_trigger: automatic
                    created_at: '2026-03-22T12:00:01Z'
                    completed_at: '2026-03-22T12:00:02Z'
                    last_status_code: 200
        '400':
          description: |
            `missing_endpoint_id` or `invalid_endpoint_id`: the endpoint ID in the path is blank or not a UUID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'

        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /api/v1/webhooks/{endpointID}/deliveries/{deliveryID}:
    get:
      operationId: getWebhookDelivery
      summary: Get Webhook Delivery
      description: |
        Return environment-scoped delivery metadata and the durable attempt
        ledger summary. Signed payload bytes, receiver response bodies, and
        operator-only fields are not returned. Scoped partner keys require
        `webhooks:manage`.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/EndpointID'
        - $ref: '#/components/parameters/DeliveryID'
      responses:
        '200':
          description: Delivery metadata and attempt summaries.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookDeliveryDetail'
        '400':
          description: |
            `missing_endpoint_id` or `invalid_endpoint_id`: the endpoint ID in the path is blank or not a UUID.
            `missing_delivery_id` or `invalid_delivery_id`: the delivery ID in the path is blank or not a UUID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/DeliveryNotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/webhooks/{endpointID}/deliveries/{deliveryID}/resend:
    post:
      operationId: resendWebhookDelivery
      summary: Resend Dead-Letter Webhook Delivery
      description: |
        Start a new audited generation for a `dead_letter` delivery. The
        delivery ID and the stored body bytes do not change; each attempt is
        signed again with a fresh timestamp. `attempt_count` resets to 0,
        `resend_generation` increases by one, and `next_attempt_trigger`
        becomes `manual_resend`. The new generation keeps the delivery's own
        attempt limit (`max_attempts`, 8 on `standard-v3`). The endpoint must be
        active and not deleted. The resend posts to the delivery's stored
        `target_url`, not the endpoint's current `url`. Queue insertion follows the durable database
        transition, so the sweeper recovers an enqueue failure. Scoped keys
        require `webhooks:manage`.
      tags: [Webhooks]
      parameters:
        - $ref: '#/components/parameters/EndpointID'
        - $ref: '#/components/parameters/DeliveryID'
      responses:
        '200':
          description: A new resend generation is pending.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/WebhookResendResponse'
        '400':
          description: |
            `missing_endpoint_id` or `invalid_endpoint_id`: the endpoint ID in the path is blank or not a UUID.
            `missing_delivery_id` or `invalid_delivery_id`: the delivery ID in the path is blank or not a UUID.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          description: The delivery is not in `dead_letter`, an attempt on it is in progress, its endpoint is inactive or deleted, or no such delivery exists for this endpoint in this environment.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: delivery_not_resendable
                message: Delivery is not in dead_letter status or endpoint is inactive/deleted
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/converse:
    post:
      operationId: converse
      summary: Converse (streaming)
      description: |
        Runs one conversational model turn and streams the result as
        Server-Sent Events. The caller supplies tool definitions (JSON
        schemas) and executes those tools in its own application; the platform
        never executes a tool and stores no conversation state. The
        conversation lives caller-side and is replayed in full on each request.

        The turn is single-shot: the platform emits assistant text and any
        tool-use requests, then ends the turn. When the turn ends with
        `stop_reason: "tool_use"`, the caller runs the requested tools locally,
        appends the results as `tool_result` content in a new user message, and
        POSTs the whole conversation again to continue the loop.

        Scoped credentials require `conversations:use`. Requests are per-org rate
        limited. Guardrails cap the number of messages, the number of tools,
        and `max_tokens`; a violation returns a 400 before the stream starts.

        The response is `text/event-stream`. Each event is one of
        `message_start`, `text_delta`, `tool_use`, `message_end`, or `error`.
        A failure before the first byte returns the standard JSON error
        envelope; a failure after the stream has started arrives as a terminal
        `error` event.
      tags: [Converse]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ConverseRequest'
      responses:
        '200':
          description: |
            A Server-Sent Events stream of the assistant turn. Frames are
            `event: <type>` with a JSON `data:` payload.
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  SSE frames, each `event: <type>` then `data: <json>` then a
                  blank line. Every payload carries `type`. Order:
                  `message_start` (`{type}`), then `text_delta`
                  (`{type, text}`) and `tool_use` (`{type, id, name, input}`,
                  one complete call per frame), then exactly one terminal
                  frame: `message_end` (`{type, stop_reason, usage}`) or
                  `error` (`{type, code, message}`). `usage` is
                  `{input_tokens, output_tokens, total_tokens,
                  cached_input_tokens, cache_creation_input_tokens,
                  cache_read_input_tokens}`; `cached_input_tokens` is filled
                  by OpenAI models and is a subset of `input_tokens`, the two
                  `cache_*` fields are filled by Anthropic models.
                  `stop_reason` uses one vocabulary for both providers, for
                  example `end_turn`, `tool_use`, `max_tokens`; a provider
                  value with no mapping passes through.
        '400':
          description: |
            The request failed validation before the stream started:
            `invalid_request` (body not JSON or over 1 MiB, or `messages` empty),
            `too_many_messages` (over 100), `too_many_tools` (over 64),
            `invalid_max_tokens` (below 0 or above 8192),
            `invalid_message_role`, `invalid_message` (empty content),
            `invalid_content_block` (a type other than text, tool_use,
            tool_result), `invalid_tool`, or `unknown_model`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The platform could not resolve the model (`internal_error`) or cannot stream (`streaming_unsupported`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          description: |
            The model provider failed before the stream started
            (`provider_error`). This route returns `code` and `message` only;
            it does not carry the provider failure `reason` that instance turns
            carry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            Converse is not enabled in this deployment (`converse_unavailable`),
            the provider circuit is open after repeated failures
            (`provider_unavailable`), or API key authentication is temporarily
            unavailable (`auth_unavailable`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/conversation-instances:
    get:
      operationId: listConversationInstances
      summary: List Conversation instances
      description: |
        Lists the organization's Conversation instances (config blobs
        omitted; the detail route carries them). Scoped credentials require
        `conversations:use`.
      tags: [Conversations]
      responses:
        '200':
          description: The organization's instances, in slug order.
          content:
            application/json:
              schema:
                type: object
                required: [instances]
                properties:
                  instances:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConversationInstance'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`insufficient_capability`: a scoped key without `conversations:use`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    post:
      operationId: createConversationInstance
      summary: Create a Conversation instance
      description: |
        Creates a Conversation instance: the configured object of the
        Conversations family, an orchestrator over engines. Its versioned
        config declares the model policy, the `engines` access allowlist
        (`"*"` or an explicit list of engine slugs/ids the instance may read
        through pack tools), tools, components, `data_sources`, limits,
        guardrail policy, eval policy, and retention. Omitting `config`
        creates the instance with the platform default config; a supplied
        config is validated and rejected with per-field issues
        (`invalid_instance_config`). Unknown body fields are rejected.
        Organization API keys only: every scoped key is refused with
        `403 insufficient_capability`.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [slug, label]
              additionalProperties: false
              properties:
                slug:
                  type: string
                  minLength: 3
                  maxLength: 50
                  pattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$'
                label:
                  type: string
                  minLength: 1
                  description: 1 to 100 UTF-8 bytes.
                description:
                  type: string
                  description: At most 500 UTF-8 bytes.
                config:
                  allOf:
                    - $ref: '#/components/schemas/ConversationInstanceConfigWrite'
                  description: Full instance config; defaults applied when omitted.
      responses:
        '201':
          description: The created instance.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationInstance'
        '400':
          description: |
            `invalid_request`: the body is not JSON, carries an unknown field,
            is over 1 MiB, or a field fails its rule (slug, label, description).
            `invalid_instance_config`: `config` failed validation; `details`
            lists `[{path, message}]`. A `config` of `null` fails this way; omit
            the field to get the default config.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OrganizationKeyRequired'
        '409':
          description: An instance with this slug already exists (`instance_slug_conflict`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
    get:
      operationId: getConversationInstance
      summary: Get a Conversation instance
      description: |
        Returns the instance with its live draft config. Archived instances are returned too. Followers of a
        platform-managed source instance (`follows_canonical: true`) carry
        no authoritative local config and return none. Requires
        `conversations:use`.
      tags: [Conversations]
      responses:
        '200':
          description: The instance.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationInstance'
        '400':
          description: '`invalid_request`: the slug in the path is blank (for example a single encoded space).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`insufficient_capability`: a scoped key without `conversations:use`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`instance_not_found`: no live instance with this slug in the organization.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    patch:
      operationId: updateConversationInstance
      summary: Update a Conversation instance
      description: |
        Updates label, description, status, group key, and/or the live draft
        config. Config changes are validated (`invalid_instance_config`) and
        do not change live behavior until published. Unknown body fields are
        rejected; `managed_by` and the follow pointer are
        platform-managed and cannot be set through this surface. A follower
        instance rejects config changes with `instance_follows_canonical`.
        Setting `status: active` reactivates an archived instance; this
        route stays available while the instance is archived. The body is a
        full replacement for `config` when present: a partial config is
        invalid. Organization API keys only: every scoped key is refused
        with `403 insufficient_capability`.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              additionalProperties: false
              properties:
                label: {type: string, minLength: 1, description: '1 to 100 UTF-8 bytes.'}
                description: {type: string, description: 'At most 500 UTF-8 bytes.'}
                status: {type: string, enum: [active, archived]}
                group_key: {type: string, description: 'At most 100 UTF-8 bytes.'}
                config:
                  $ref: '#/components/schemas/ConversationInstanceConfigWrite'
      responses:
        '200':
          description: The updated instance.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationInstance'
        '400':
          description: |
            `invalid_request`: the slug in the path is blank, the body is not
            JSON, carries an unknown field or is over 1 MiB, no field was
            supplied (a field sent as `null` counts as not supplied), or a field
            fails its rule (label, description, status, group_key).
            `invalid_instance_config`: `config` failed validation; `details`
            lists `[{path, message}]`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OrganizationKeyRequired'
        '404':
          description: '`instance_not_found`: no live instance with this slug in the organization. Checked before the body, so a bad body on an unknown slug answers 404.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The instance follows a platform-managed source instance (`instance_follows_canonical`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/versions:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
    get:
      operationId: listConversationInstanceVersions
      summary: List instance versions
      description: |
        Returns the instance's released versions, newest first (config blobs
        omitted; each version freezes the config snapshot server-side).
        Requires `conversations:use`.
      tags: [Conversations]
      responses:
        '200':
          description: Versions, newest first.
          content:
            application/json:
              schema:
                type: object
                required: [versions]
                properties:
                  versions:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConversationInstanceVersion'
        '400':
          description: '`missing_instance_slug`: the slug in the path is blank (for example a single encoded space).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`instance_not_found`: no live instance with this slug in the organization.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    post:
      operationId: publishConversationInstanceVersion
      summary: Publish an instance version
      description: |
        Releases the instance's live draft config as the next integer
        version. The live draft is revalidated first, so drafts saved by an
        older client with retired model selectors fail with
        `invalid_instance_config`. When the instance carries active `gate`-suite eval scenarios
        and the draft differs from the latest release, the publish is GATED: every such
        scenario replays against the candidate
        config with real model calls before anything is released, the run is
        recorded against the candidate, and the per-instance
        `evals.on_regression` policy decides `block` (publish refuses with
        the per-scenario diff, `conversation_eval_regressed`) or `warn`
        (publish proceeds, result recorded). An explicit waiver (`waived_by`
        + `waive_reason`, always together) bypasses a block and is recorded
        on the run row; a waiver sent when nothing is blocked is ignored. A concurrent config save between the gate and the
        release refuses with `config_changed_during_publish`, and a `gate`-suite
        scenario created, edited, or deleted while the gate ran refuses with
        `scenario_set_changed_during_publish` (nothing is released in either
        case). Re-publishing an unchanged config returns the current
        latest version with `already_released: true`. Followers reject with
        `instance_follows_canonical`. Organization API keys only: every scoped key is refused with
        `403 insufficient_capability`. The route is exempt from the 30 second
        request timeout; the gate plus release is bounded at 50 minutes.
        The gate runs only `gate`-suite scenarios. An archived instance can
        still be published.
      tags: [Conversations]
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                changelog: {type: string}
                waived_by: {type: string}
                waive_reason: {type: string}
      responses:
        '200':
          description: The released version (or the idempotent no-op). `released_at` has whole-second precision. On a new release it is the time the response was built and can differ slightly from the version list. When `already_released` is true, `changelog` and `published_by` are the latest version's, not this request's.
          content:
            application/json:
              schema:
                type: object
                required: [version, config_hash, released_at, changelog]
                properties:
                  version: {type: integer}
                  config_hash: {type: string}
                  released_at: {type: string, format: date-time}
                  changelog: {type: string}
                  already_released: {type: boolean}
                  published_by: {type: string}
        '400':
          description: |
            The slug in the path is blank or the body is not JSON or is over
            1 MiB (`invalid_request`), only one of `waived_by` and
            `waive_reason` was sent (`invalid_eval_waiver`; a value of only
            spaces counts as not sent), or the draft fails revalidation
            (`invalid_instance_config`, with `details` as
            `[{path, message}]`). A bad body on an unknown slug answers 404,
            and on a follower answers 409 `instance_follows_canonical`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/OrganizationKeyRequired'
        '404':
          description: '`instance_not_found`: no live instance with this slug in the organization.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            No live config to publish (`instance_config_hash_missing`), the
            instance follows a platform-managed source instance
            (`instance_follows_canonical`), the config changed while the
            eval gate ran (`config_changed_during_publish`), or the eval
            scenario set changed while the gate ran
            (`scenario_set_changed_during_publish`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: |
            The candidate regressed the instance's Conversation eval set
            under `on_regression: block` (`conversation_eval_regressed`).
            Regression is per scenario and is judged against the gate run
            recorded for the latest released version: a scenario that passed
            there and now fails, or a scenario with no result there that
            fails, blocks. When the latest version has no recorded gate run
            (for example the first publish with scenarios), the run is the
            baseline and never blocks. The failing run is still recorded. The
            `details` object carries `pass_count`, `fail_count`,
            `prior_pass_count`, `newly_failing` (regressions), `new_failing`
            (newly added failing scenarios), `newly_passing`, and
            `eval_run_id`; publish with an explicit waiver or fix the config.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The publish failed (`internal_error` when the instance lookup failed, `publish_failed` for any later step, `auth_context_missing` before the route ran); nothing was released.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '502':
          description: The eval gate could not execute a scenario (`conversation_eval_execution_failed`); nothing was recorded or released.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            The instance has active `gate`-suite eval scenarios and a draft that differs from the latest release, but no eval executor is available (`conversation_eval_gate_unavailable`); the gate cannot run and the publish fails closed.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /api/v1/conversation-instances/{instance_slug}/conversations:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
    post:
      operationId: createConversation
      summary: Create a conversation
      description: |
        Creates a conversation on a Conversation instance. Conversations are
        scoped to the organization and to the calling key's environment, and
        belong to an `end_user` (a consumer-asserted identifier, at most 256
        UTF-8 bytes after trimming; the trimmed value is stored). Unknown body
        fields are ignored. Scoped credentials require `conversations:use`. An
        archived instance answers `403 instance_archived`.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [end_user]
              properties:
                end_user:
                  type: string
                  minLength: 1
                  description: At most 256 UTF-8 bytes after trimming. Consumer-asserted end-user identifier. Every later request for this conversation must send the same value.
                title:
                  type: [string, 'null']
                  description: Optional conversation title, at most 300 UTF-8 bytes. Null is the same as omitting it.
                scope:
                  type: [object, 'null']
                  additionalProperties: true
                  description: >-
                    Opaque consumer context attached to the conversation, at
                    most 16384 bytes. A JSON object, or `null`, which is the
                    same as omitting it: the conversation starts with no scope.
                    Any other JSON shape is rejected with `400`. Never grants
                    access.
      responses:
        '201':
          description: The created conversation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
        '400':
          description: |
            `invalid_request`: the body is not JSON or is over 1 MiB, `end_user`
            is blank or over 256 bytes after trimming, `title` is over 300
            bytes, or `scope` is over 16384 bytes or not an object.
            `missing_instance_slug`: the slug in the path is blank.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`instance_not_found`: no live instance with this slug in the organization.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    get:
      operationId: listConversations
      summary: List conversations
      description: |
        Lists one end user's conversations on this instance, in the key's
        environment, most recent first, with cursor pagination. Requires
        `conversations:use` for scoped keys. An archived instance answers
        `403 instance_archived`.
      tags: [Conversations]
      parameters:
        - name: end_user
          in: query
          required: true
          schema: {type: string}
          description: The end user whose conversations to list, compared after trimming. Blank answers 400 `invalid_request`.
        - name: cursor
          in: query
          required: false
          schema: {type: string}
          description: The `next_cursor` from the previous page. A malformed value answers 400 `invalid_cursor`.
        - name: limit
          in: query
          required: false
          schema: {type: integer, minimum: 1, maximum: 100, default: 50}
          description: Page size. A value outside 1 to 100, or not an integer, answers 400 `invalid_request`.
        - name: q
          in: query
          required: false
          schema: {type: string}
          description: Case-insensitive substring match on the conversation title (`%` and `_` match themselves). Untitled conversations never match. At most 200 UTF-8 bytes after trimming; a longer value answers 400 `invalid_request`. Blank means no filter.
      responses:
        '200':
          description: Conversations page.
          content:
            application/json:
              schema:
                type: object
                required: [conversations, pagination]
                properties:
                  conversations:
                    type: array
                    items:
                      allOf:
                        - $ref: '#/components/schemas/Conversation'
                        - type: object
                          required: [has_messages]
                          properties:
                            has_messages:
                              type: boolean
                              description: Whether the conversation has any stored message.
                  pagination:
                    type: object
                    required: [has_more]
                    properties:
                      has_more:
                        type: boolean
                      next_cursor:
                        type: string
                        description: Opaque cursor for the next page; omitted when exhausted.
        '400':
          description: '`end_user` is missing or blank, `limit` is not an integer from 1 to 100, or `q` is too long (`invalid_request`); the cursor is not valid (`invalid_cursor`); or the slug in the path is blank (`missing_instance_slug`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`instance_not_found`: no live instance with this slug in the organization.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getConversation
      summary: Get a conversation with its messages
      description: |
        Returns the conversation and the most recent 500 messages of the branch
        it serves, oldest first. Messages carry typed content blocks (text,
        tool_use, tool_result, component, document_reference, citation) and
        the instance config version hash the turn ran under; internal platform
        bookkeeping blocks are never included. When that branch holds more than
        500 messages (or the conversation holds more than 5000 in all) the
        response is the tail window and `truncated` is true;
        the full transcript is available through the conversation export
        endpoint (`conversations:export`). The `end_user`
        assertion is REQUIRED and the lookup is scoped to it: a missing
        assertion is a 400, and cross-org or cross-end-user access returns
        404.

        When a message has been revised, the transcript is the currently
        selected branch, not every version: each message carries
        `parent_message_id`, and a message that has sibling versions also
        carries `branch`. A conversation whose messages were never revised
        returns the same flat, insertion-ordered window it always did.

        A missing, malformed, or foreign conversation id, or a different end
        user or key environment, all answer `404 conversation_not_found`.
      tags: [Conversations]
      parameters:
        - name: end_user
          in: query
          required: true
          schema: {type: string}
          description: The asserted end-user identity the conversation must belong to, compared after trimming. Missing or blank answers 400 `invalid_request`; another end user's value answers 404 `conversation_not_found`.
      responses:
        '200':
          description: Conversation detail with the transcript tail.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationDetail'
        '400':
          description: '`invalid_request`: `end_user` is missing or blank. `missing_instance_slug`: the slug in the path is blank.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`conversation_not_found`: the id is malformed or unknown, or the conversation belongs to another end user, key environment, or instance. `instance_not_found`: no live instance with this slug.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    patch:
      operationId: updateConversation
      summary: Update a conversation
      description: |
        Updates the conversation title, status and/or caller-owned subject scope. Scope never grants access. Requires
        `conversations:use`. The `end_user` assertion is REQUIRED and the
        lookup is scoped to it: a missing assertion is a 400, a mismatch a
        404.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [end_user]
              properties:
                end_user:
                  type: string
                  description: |
                    The asserted end-user identity the conversation must
                    belong to. Provide at least one of title, status, scope
                    alongside it.
                scope:
                  type: object
                  additionalProperties: true
                  description: Replaces subject metadata (maximum 16384 bytes). Omit to preserve; use an empty object to clear. Never supplies authorization. A null scope is rejected.
                title: {type: string, description: 'At most 300 UTF-8 bytes.'}
                status: {type: string, enum: [active, archived]}
      responses:
        '200':
          description: The updated conversation.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Conversation'
        '400':
          description: |
            `invalid_request`: the body is not JSON or is over 1 MiB, `end_user`
            is missing, none of `title`, `status`, `scope` is supplied,
            `scope` is null, over 16384 bytes or not an object, `title` is over
            300 bytes, or `status` is not `active` or `archived`. The
            conversation is looked up first, so an unknown conversation answers
            404 even when the rest of the body is invalid.
            `missing_instance_slug`: the slug in the path is blank.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`conversation_not_found`: the id is malformed or unknown, or the conversation belongs to another end user, key environment, or instance. `instance_not_found`: no live instance with this slug.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/attachments:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      operationId: createConversationAttachment
      summary: Attach a file to a conversation
      description: |
        Uploads one file against a conversation, so a later turn can carry it
        with `attachment_ids`.

        To send a file from a browser straight to storage, use
        the reserve, PUT, finalize path instead
        (`POST .../attachments/reserve`).

        The platform stores the bytes and hands them to the model on the turn
        that names them. Apart from opening a zip or email into its files (an
        email's From, To, Cc, Date, and Subject headers and its body text are
        kept as a `message.txt` file), it does not parse the document and
        stores no extracted text; what the model reads is the file itself.

        The instance's PUBLISHED `attachments` config governs what is
        accepted: whether uploads are enabled at all, how many files ride one
        message, the per-file size ceiling, an optional narrowing of the
        accepted media types, whether containers are opened, and how long
        bytes are kept. An instance with no `attachments` block, or with
        `enabled: false`, takes none and answers `attachments_not_enabled`.

        Accepted formats are a closed set: PDF, Word (including `.odt` and
        `.rtf`), PowerPoint, Excel, CSV, TSV, plain text, Markdown, JSON, XML,
        HTML, PNG, JPEG, WebP, GIF, and (when the instance opens containers)
        `.eml` and `.zip`. A file is classified by its extension (by its
        declared type only when the name has no extension), closed against
        that set, and then checked to confirm
        its contents agree; a mismatch is refused. Outlook `.msg` is not
        accepted, and its refusal says to attach the file inside the message
        or save it as `.eml`.

        Some formats are read with a limit worth knowing. A spreadsheet is
        read to the first 1,000 rows per sheet, and text is read out of Office
        files while images and charts inside them are not. When a limit
        applies, the response carries a `notice` naming it in plain words. Show
        that notice: an answer drawn from part of a schedule is not an answer
        about the schedule.

        A `.zip` or `.eml` container (accepted only when the instance sets
        `unwrap_archives: true`) is opened at upload, one level deep. Each
        file whose name the platform does not accept, and each container inside
        the container, is skipped without being extracted. The rest are
        extracted (at most 200; accepted names past that are omitted) and
        checked like a file sent on its own; files that are oversized, refused
        by the instance's policy, or whose bytes disagree with their name are
        skipped, and they still count toward the 200. At most 25 accepted files are kept and each is
        stored as its own attachment; accepted files past the 25th are omitted
        and the upload still succeeds. When anything was omitted or skipped,
        the container's `notice` counts the files read, names up to 20
        omitted files and counts the rest, and counts the skipped ones. A
        container that cannot be opened, or with a file inside that cannot be
        read or decompressed, or from which no file could be extracted, answers
        `400 attachment_rejected`. A container whose extracted files all fail
        the per-file checks answers `415 attachment_rejected`. A turn that names the container carries the
        files inside it, not the container. See
        [How a zip or email is opened](/conversations/attachments#how-a-zip-or-email-is-opened).

        One conversation holds at most 100 live attachments, files found
        inside containers included. This is a platform ceiling, not a config
        knob. An upload past it, or a container whose files would take the
        conversation past it, is refused whole with `400 attachment_rejected`
        and a message that says so; deleting attachments, or letting retention
        close them, makes room. Re-attaching the same bytes under the same file
        name, while the instance's policy still accepts them, returns the
        existing record and does not count against the ceiling.

        Scope is org, conversation, AND end user. A conversation belonging to
        another org, or to another end user, is `404`, never `403`.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [end_user, file]
              properties:
                end_user:
                  type: string
                  description: |
                    The asserted end-user identity the conversation must
                    belong to. A mismatch is a 404.
                file:
                  type: string
                  format: binary
                  description: The file to attach.
      responses:
        '201':
          description: The stored attachment.
          content:
            application/json:
              schema:
                type: object
                required: [attachment]
                properties:
                  attachment:
                    $ref: '#/components/schemas/ConversationAttachment'
        '400':
          description: |
            The request or the file itself is malformed. Covers an empty
            instance slug (`missing_instance_slug`), a body that is not
            multipart form data or is missing the `file` part or `end_user`
            (`invalid_request`), an instance whose published config has no
            attachments block or sets `enabled: false`
            (`attachments_not_enabled`), and a file refused for its own shape
            (`attachment_rejected`): an empty file, a container that could not
            be opened, a zip that lists more than 10,000 entries or expands past
            100 MiB, a file inside a container that could not be read, a
            container from which no file could be extracted, a conversation
            already at its 100-attachment ceiling, or a container whose files
            would take the conversation past it. Distinct from 415 on purpose:
            converting a malformed file to another type would not fix it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: attachment_rejected
                message: The file is empty.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`) or the conversation (`conversation_not_found`) is not in scope. A conversation in another organization, under another end user, or created with a key from another environment answers the same way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The instance has no published version, so no upload policy is in force (`instance_not_published`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: |
            The file is larger than the instance accepts
            (`attachment_too_large`). Distinct from 415 on purpose: a size
            problem and a type problem call for different things from the
            person who sent the file.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          description: |
            The file's type was refused (`attachment_rejected`): it is outside
            the accepted set, the instance narrowed the accepted types past
            it, its contents disagree with its name, or it is a container
            whose extracted files all fail those checks. A container from
            which no file could be extracted is `400` instead. The message names the
            reason in words a person can act on.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The published config cannot be parsed (`instance_config_invalid`) or the upload failed (`internal_error`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AttachmentsUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    get:
      operationId: listConversationAttachments
      summary: List a conversation's attachments
      description: |
        One page of this conversation's attachments for the asserted end user,
        newest first, including the files found inside any container. Records
        that are not ready yet (a reservation still `storing`, a folder still
        `unwrapping`) are listed too; check `status`. Expired attachments are
        left out.

        Pages hold up to 100. Read `has_more` rather than the page length: a
        full page and the end of the list look identical otherwise, and a
        caller that stops early believes it has seen every document the end
        user attached. Pass `next_offset` back as `offset` to continue.
      tags: [Conversations]
      parameters:
        - name: end_user
          in: query
          required: true
          schema: {type: string}
        - name: offset
          in: query
          required: false
          schema: {type: integer, minimum: 0}
          description: Where to resume, from a previous page's `next_offset`.
      responses:
        '200':
          description: One page of the conversation's attachments.
          content:
            application/json:
              schema:
                type: object
                required: [attachments, has_more, next_offset]
                properties:
                  attachments:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConversationAttachment'
                  has_more:
                    type: boolean
                    description: Whether more attachments follow this page.
                  next_offset:
                    type: integer
                    description: Pass back as `offset` to read the next page.
        '400':
          description: |
            The instance slug is empty (`missing_instance_slug`), `end_user`
            is missing (`invalid_request`), or `offset` is not a
            non-negative integer (`invalid_offset`). A bad offset is refused
            rather than silently read as zero, because restarting from page
            one on a typo would hand the caller duplicates.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`) or the conversation (`conversation_not_found`) is not in scope. A conversation in another organization, under another end user, or created with a key from another environment answers the same way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AttachmentsUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/attachments/{attachment_id}:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
      - name: attachment_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getConversationAttachment
      summary: Read one attachment's record
      description: |
        The stored record for one attachment: its name, media type, size,
        status, how it reaches the model, and any notice about how its format
        is read. A multipart upload's `201` is already `ready`. A reserved
        upload stays `storing` until it is finalized.
      tags: [Conversations]
      parameters:
        - name: end_user
          in: query
          required: true
          schema: {type: string}
      responses:
        '200':
          description: The attachment.
          content:
            application/json:
              schema:
                type: object
                required: [attachment]
                properties:
                  attachment:
                    $ref: '#/components/schemas/ConversationAttachment'
        '400':
          description: 'The instance slug is empty (`missing_instance_slug`) or `end_user` is missing (`invalid_request`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`), the conversation (`conversation_not_found`) or the attachment (`attachment_not_found`) is not in scope. A malformed, deleted or expired attachment id answers `attachment_not_found`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AttachmentsUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    delete:
      operationId: deleteConversationAttachment
      summary: Delete an attachment
      description: |
        Removes the attachment, and any files found inside it when it is a
        container. The record is hidden the moment the delete returns: nothing
        can list, serve, or carry it on a turn from then on. The stored bytes
        are removed by a later cleanup pass, no sooner than one hour after the
        delete, rather than inline,
        so a turn that resolved the attachment moments before the delete can
        finish reading, and a removal that fails is retried instead of
        abandoned.
      tags: [Conversations]
      parameters:
        - name: end_user
          in: query
          required: true
          schema: {type: string}
      responses:
        '204':
          description: Deleted. The record is gone now; the bytes follow on the sweep.
        '400':
          description: 'The instance slug is empty (`missing_instance_slug`) or `end_user` is missing (`invalid_request`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`), the conversation (`conversation_not_found`) or the attachment (`attachment_not_found`) is not in scope, or the attachment id is malformed or already deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            The attachment is one file found inside a zip or email container
            (`attachment_member_delete`). A file inside a folder can be deleted on its own. The packet is the unit of deletion:
            delete the parent attachment, which removes it and every file
            found inside it.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AttachmentsUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/attachments/{attachment_id}/content:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
      - name: attachment_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getConversationAttachmentContent
      summary: Fetch an attachment's bytes
      description: |
        Redirects to a short-lived URL for the stored file. The URL expires in
        five minutes and is minted only after the caller's scope is checked.
      tags: [Conversations]
      parameters:
        - name: end_user
          in: query
          required: true
          schema: {type: string}
      responses:
        '302':
          description: Redirect to the short-lived file URL. The URL downloads the file rather than displaying it.
          headers:
            Location:
              description: The presigned download URL. It expires five minutes after it is issued.
              schema:
                type: string
                format: uri
        '400':
          description: 'The instance slug is empty (`missing_instance_slug`) or `end_user` is missing (`invalid_request`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`), the conversation (`conversation_not_found`) or the attachment (`attachment_not_found`) is not in scope. An attachment id that is malformed, deleted, expired, or not `ready` yet also answers `attachment_not_found`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AttachmentsUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/turns:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      operationId: converseConversationTurn
      summary: Take a Converse turn (streaming)
      description: |
        Runs one instance turn on the conversation and streams the result as
        Server-Sent Events. Exactly one of `message`, `tool_results`, or
        `confirmations` starts a turn: a new user message, the results of a
        client-tool handoff, or the decisions for a paused confirm gate.

        The instance's published config governs the turn: model policy,
        platform tool packs (executed server-side, filtered at turn start to
        what the calling principal may invoke), client tools (returned to the
        caller with a `tool_use` frame; the turn stays open for the follow-up
        `tool_results` POST), guardrail policy (refusals and escalations emit
        `guardrail` frames and are recorded), and per-turn limits. Every
        persisted message is stamped with the instance config version hash the
        turn resolved. A new `message` needs a published version
        (`instance_not_published`). The retired `depth` field is refused with
        `400 invalid_request`; every configured assistant runs `gpt-6-sol`.

        The response is `text/event-stream`. Frames: `conversation`, `attachment`,
        `text_delta`, `tool_activity`, `tool_use`, `component`, `guardrail`,
        `pending_confirmation`, `turn_end`, `error`. A failure before the
        first byte returns the standard JSON error envelope; a failure after
        the stream has started arrives as a terminal `error` event. The turn
        survives client disconnect: it runs to completion and persists, and
        the finished exchange is present when the conversation is fetched again.
        One segment is limited to 10 minutes on the server. The route is not
        subject to the 30 second request timeout.

        Turns are serialized per conversation: a second POST while a segment is
        running returns `409` (`turn_in_progress`), a new `message` while a
        paused turn awaits `tool_results` or `confirmations` returns `409`
        (`pending_turn`), a resume that does not match the paused state
        returns `409` (`turn_state_conflict`), and an archived conversation returns
        `409` (`conversation_archived`). A resume runs under the paused turn's
        original config version; only a new `message` resolves the latest
        release.

        A turn can also REVISE an earlier user message instead of appending to
        the transcript: send `edit_of_message_id` alongside `message`. The
        revision is stored as a sibling version of the referenced message, the
        superseded exchange is left out of the history the model sees, and the
        reply streams on the new branch, which the conversation serves from
        then on. An unparseable id, a missing `message`, or a target that is
        not a `user` message returns `400`; a target outside this org,
        conversation, or end user returns `404`. Switch between versions with
        the branch route.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [end_user]
              properties:
                end_user:
                  type: string
                  description: |
                    The asserted end-user identity the conversation must
                    belong to; required on every turn. A missing assertion is
                    a 400, a mismatch a 404.
                message:
                  type: string
                  maxLength: 32000
                  description: |
                    New end-user input (at most 32000 characters; longer
                    messages reject with `message_too_long`). Leading and
                    trailing spaces are trimmed; a blank message answers
                    `invalid_turn_request`. Exactly one of the three starters.
                edit_of_message_id:
                  type: string
                  format: uuid
                  description: |
                    Revises the referenced user message: the value of `message`
                    is stored as a sibling version of that message, and the new
                    branch is served from then on. Requires `message`; omit it
                    on an ordinary turn.
                attachment_ids:
                  type: array
                  uniqueItems: true
                  items: {type: string, format: uuid}
                  description: |
                    Attachments this message carries. Each must already be
                    uploaded to this conversation and belong to the same end
                    user; the platform loads the bytes and hands the files to
                    the model. Requires `message`, and is rejected alongside
                    `tool_results` or `confirmations`.

                    A named attachment that is missing, out of scope, or not
                    yet readable fails the whole turn rather than running it
                    with fewer documents than were attached. An attachment
                    that is a container (an email or a zip) resolves to the
                    files inside it. The combined bytes are capped at the
                    provider request limit; over it returns `413`.
                tool_results:
                  type: array
                  description: Client-tool results completing a `tool_use` handoff.
                  items:
                    type: object
                    required: [tool_call_id]
                    properties:
                      tool_call_id:
                        type: string
                        description: The handed-off call this result answers.
                      content:
                        description: Opaque tool output passed to the model verbatim. Absent content is sent as an empty string.
                      is_error:
                        type: boolean
                        description: True when the client tool failed.
                confirmations:
                  type: array
                  description: Decisions resolving a `pending_confirmation` pause.
                  items:
                    type: object
                    required: [tool_call_id, approved]
                    properties:
                      tool_call_id:
                        type: string
                        description: The confirm-gated call this decision resolves.
                      approved:
                        type: boolean
                      reason:
                        type: string
                        description: Optional denial reason surfaced to the model.
                page_context:
                  description: |
                    Untrusted consumer page context, any JSON value, at most
                    16384 bytes. Sent to the model as a delimited data block on
                    every round of this segment; never stored.
      responses:
        '200':
          description: |
            A Server-Sent Events stream of the instance turn. Frames are
            `event: <type>` with a JSON `data:` payload.
          content:
            text/event-stream:
              schema:
                type: string
                description: |
                  SSE frames, each `event: <type>` then `data: <json>` then a
                  blank line. Every payload carries `type`.
                  `attachment` (`{type, message_id, attachments: [{id, name,
                  media_type, notice?}]}`) comes first, only when the message
                  carries files. `conversation` (`{type, conversation_id,
                  turn_id, user_message_id?}`) comes first otherwise;
                  `user_message_id` is absent on a resume. Then any of
                  `guardrail` (`{type, rule_id, decision}`, decision
                  `refused`, `escalated`, `output_check_triggered`),
                  `text_delta` (`{type, text}`), `tool_activity`
                  (`{type, tool_call_id, name, execution, phase}`, execution
                  `platform` or `client`, phase `started`, `completed`,
                  `failed`), `tool_use` (`{type, tool_call_id, name, input}`,
                  client tools only), `component` (`{type, component, version,
                  props}`), `pending_confirmation` (`{type, tool_call_id, name,
                  input, reason, execution}`). Exactly one terminal frame ends
                  the segment: `turn_end` (`{type, stop_reason, usage}`) or
                  `error` (`{type, code, message, reason?}`; codes include
                  `provider_error`, `provider_unavailable`,
                  `invalid_conversation_history`, `attachment_changed`,
                  `attachment_read_failed`, `attachments_unavailable` and
                  `internal_error`). `usage` is
                  cumulative across the turn's segments and has the six token
                  fields of the stateless converse route. `stop_reason` is a
                  provider value (`end_turn`, `max_tokens`) or one of
                  `tool_use`, `pending_confirmation`, `refusal`, `escalated`,
                  `output_check_triggered`, `max_output_tokens`.
        '400':
          description: |
            `invalid_request` (body not JSON or over 1 MiB, `end_user` missing
            or blank, or `depth` sent, even as null), `invalid_turn_request`
            (not exactly one starter, blank `message`, `edit_of_message_id`
            without `message` or not a UUID, `page_context` over 16384 bytes,
            `attachment_ids` without `message`, not UUIDs, or repeated),
            `message_too_long`, `invalid_tool_result` or
            `invalid_confirmation` (including results or decisions that do
            not cover exactly the pending call ids), `invalid_edit_target`,
            `attachments_not_enabled`, `too_many_attachments` (over the
            instance's per-message limit, or over 25 files once containers are
            opened), `attachment_not_accepted`, `missing_instance_slug` (the
            slug in the path is blank). The turn-request rules run after the
            conversation lookup, the archive check and, for a new `message`,
            the published-version check, so those 404 and 409 answers come
            first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            `instance_not_found` (unknown slug), `conversation_not_found`
            (unknown or malformed id, or a different org, environment,
            instance, or end user), `message_not_found`
            (`edit_of_message_id` is not a message in this conversation),
            `attachment_not_found` (a named attachment is missing or out of
            scope).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            `turn_in_progress` (another segment is running),
            `pending_turn` (a paused turn must be resumed first; `details`
            carries `pause_kind` `confirm` or `handoff` and
            `pending_call_ids`), `turn_state_conflict` (the resume does not
            match the paused state), `conversation_archived`,
            `instance_not_published` (a new message on an instance with no
            published version), `confirmation_environment_unpinned` (the
            paused action has no recorded environment). A named attachment
            removed while the turn ran (`attachment_changed`) arrives as a
            terminal `error` frame, not as this status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: The files on the message exceed 52428800 bytes once base64-encoded (`attachments_too_large`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: |
            The required `gpt-6-sol` model is not available
            (`instance_model_unavailable`; no fallback model is used), the
            guardrail classifier failed (`guardrail_evaluation_failed`), an
            output check does not compile (`guardrail_config_invalid`), or a
            platform fault (`instance_config_missing`,
            `instance_config_invalid`, `attachment_read_failed`,
            `streaming_unsupported`, `auth_context_missing`,
            `internal_error`). `invalid_conversation_history` is raised only
            after the stream starts, so it arrives as a terminal `error`
            frame.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            Converse is not enabled in this deployment
            (`converse_unavailable`), or attachment storage is not configured
            (`attachments_unavailable`). A model provider failure
            (`provider_error`, `provider_unavailable`) happens after the first
            frame, so it arrives as a terminal `error` frame on a `200` stream,
            never as an HTTP status.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/branch:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      operationId: setConversationBranch
      summary: Select a message version
      description: |
        Selects which version of a revised message the conversation serves.
        `message_id` names any message on the wanted branch; the server
        resolves that message's ancestry, follows the newest reply from there,
        and records the selection on the conversation. The selection is
        durable: a later fetch and the next turn both follow the selected
        branch.

        The response is the conversation detail payload already assembled for
        the selected branch, the same shape the conversation GET returns, so
        one round trip both switches and renders and the caller holds no
        message tree of its own. Requires `conversations:use`. The `end_user`
        assertion is REQUIRED and the lookup is scoped to it: a missing
        assertion is a 400, and cross-org, cross-end-user, or
        cross-conversation access returns 404.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [end_user, message_id]
              properties:
                end_user:
                  type: string
                  description: |
                    The asserted end-user identity the conversation must
                    belong to. A missing assertion is a 400, a mismatch a 404.
                message_id:
                  type: string
                  format: uuid
                  description: |
                    A message on the branch to serve, normally one id from a
                    `branch.siblings` array. It must belong to this
                    conversation.
      responses:
        '200':
          description: Conversation detail assembled for the selected branch.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationDetail'
        '400':
          description: '`invalid_request`: the body is not JSON or is over 1 MiB, or `end_user` is missing. `missing_instance_slug`: the slug in the path is blank. A malformed `message_id` is checked first and answers 404 `message_not_found`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            The instance (`instance_not_found`), the conversation
            (`conversation_not_found`: malformed or unknown id, another end
            user, key environment, or instance), or `message_id`
            (`message_not_found`: missing, malformed, or not a message of this
            conversation) was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The conversation is archived (`conversation_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/annotations:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: listConversationAnnotations
      summary: List conversation-turn annotations
      description: |
        Lists the live API-submitted annotations on one conversation so a
        consumer can restore recorded feedback after a reload. The
        `end_user` assertion and stable `submitter_id` are REQUIRED. The end
        user must match the conversation, and only that submitter's rows are
        returned;
        missing assertions return 400 and cross-user or cross-org reads
        return 404. Portal-submitted annotations are never returned through
        this public route. Requires `conversations:use`.
      tags: [Conversations]
      parameters:
        - name: end_user
          in: query
          required: true
          schema: {type: string}
          description: The asserted end-user identity the conversation must belong to.
        - name: submitter_id
          in: query
          required: true
          schema: {type: string}
          description: Stable caller identity used when the annotations were created.
      responses:
        '200':
          description: Up to 500 live API annotations for this submitter, most recently updated first.
          content:
            application/json:
              schema:
                type: object
                required: [annotations, truncated]
                properties:
                  annotations:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConversationAnnotation'
                  truncated:
                    type: boolean
                    description: True when more than 500 matching live annotations exist.
        '400':
          description: '`invalid_request`: `end_user` or `submitter_id` is missing or blank. `missing_instance_slug`: the slug in the path is blank.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`conversation_not_found`: the id is malformed or unknown, or the conversation belongs to another end user, key environment, or instance. `instance_not_found`: no live instance with this slug.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    post:
      operationId: createConversationAnnotation
      summary: Annotate a conversation turn
      description: |
        Records explicit human signal (a rating, an optional structured
        reason, an optional comment) against one turn of a conversation on a
        Conversation instance. The `end_user` assertion is REQUIRED and the
        lookup is scoped to it: a missing assertion is a
        400, and an assertion that contradicts the conversation returns 404.
        A `turn_id` that does not belong to this conversation returns 404
        (`turn_not_found`). Requires `conversations:use`;
        cross-org access returns 404.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [end_user, turn_id, rating]
              properties:
                end_user:
                  type: string
                  description: |
                    The asserted end-user identity the conversation must
                    belong to. Required; a mismatch is a 404.
                turn_id:
                  type: string
                  format: uuid
                  description: The turn this annotation anchors to.
                rating:
                  type: string
                  enum: [good, bad, neutral]
                comment:
                  type: string
                  description: Optional free-text comment. At most 4,000 UTF-8 bytes.
                reason:
                  type: string
                  description: Optional structured reason token (reason-chip taxonomy). At most 64 UTF-8 bytes.
                target:
                  type: object
                  additionalProperties: true
                  description: |
                    Optional JSON object naming a sub-target inside the turn,
                    at most 4096 bytes as sent. Any other JSON value, `null`
                    included, answers `invalid_target`.
                submitter_id:
                  type: string
                  description: |
                    Stable consumer-side submitter identifier. Optional for
                    backward compatibility with legacy annotation callers.
                feedback_key:
                  type: string
                  maxLength: 64
                  description: |
                    Optional consumer-defined logical signal key. When paired
                    with `submitter_id`, replaying the same conversation turn
                    and feedback key updates one live annotation. Omitting it
                    preserves the legacy append-only annotation contract.
      responses:
        '201':
          description: The created annotation, or the updated one when `submitter_id` and `feedback_key` match a live annotation on the same turn. An update that changes `rating`, `comment` or `reason` resets `triage.status` to `new` and adds 1 to `triage.revision`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationAnnotation'
        '400':
          description: |
            The request failed validation: `invalid_request` (body not JSON or
            over 1 MiB, or `end_user` missing), `invalid_turn_id` (missing or
            not a UUID), `invalid_rating`, `invalid_comment` (over 4000
            bytes), `invalid_reason` (over 64 bytes), `invalid_target` (over
            4096 bytes or not a JSON object, `null` included),
            `invalid_feedback_key` (`feedback_key` over 64 characters or sent
            without `submitter_id`), or `missing_instance_slug` (the slug in
            the path is blank). The conversation is looked up before the
            fields are checked, so an unknown conversation answers 404 first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: 'The instance (`instance_not_found`), the conversation (`conversation_not_found`: malformed or unknown id, another end user, key environment, or instance) or the turn (`turn_not_found`: no message of this conversation carries that `turn_id`) was not found.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/annotations/{annotation_id}:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
      - name: annotation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    delete:
      operationId: deleteConversationAnnotation
      summary: Retract a conversation-turn annotation
      description: |
        Soft-deletes one live API-submitted annotation. Both `end_user` and
        `submitter_id` must match the recorded annotation; a foreign,
        portal-submitted, already retracted, or otherwise mismatched
        annotation returns 404. Requires `conversations:use`.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [end_user, submitter_id]
              properties:
                end_user:
                  type: string
                  description: The asserted end-user identity the conversation must belong to.
                submitter_id:
                  type: string
                  description: Consumer-side submitter identifier recorded on creation.
      responses:
        '204':
          description: Annotation retracted.
        '400':
          description: '`invalid_request`: the body is not JSON, or `end_user` or `submitter_id` is missing or blank. `missing_instance_slug`: the slug in the path is blank.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`annotation_not_found`: the id is malformed or unknown, or the annotation is portal-submitted, already retracted, or recorded for another end user or submitter. `conversation_not_found`: the conversation id is malformed or unknown, or belongs to another end user, key environment, or instance. `instance_not_found`: no live instance with this slug.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/export:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: exportConversation
      summary: Export a conversation (compliance)
      description: |
        Reconstructs one conversation end to end as a single JSON document
        for compliance review: the conversation header, EVERY message (the
        full transcript; the 500-message display window does not apply),
        every instance config version that served it (hash plus the released
        version number when one exists), all tool events, all guardrail
        events, and all non-retracted annotations. Internal platform
        bookkeeping blocks are stripped from message content, exactly as on
        display responses. Requires the dedicated `conversations:export`
        capability (an export is a bulk disclosure, not a conversational
        read). The `end_user` assertion is REQUIRED
        and the lookup is scoped to it: a missing assertion is a 400, and
        cross-org or cross-end-user access returns 404.

        An archived instance answers 403 instance_archived.
      tags: [Conversations]
      parameters:
        - name: end_user
          in: query
          required: true
          schema: {type: string}
          description: The asserted end-user identity the conversation must belong to.
      responses:
        '200':
          description: The complete export document.
          content:
            application/json:
              schema:
                type: object
                required: [export_version, generated_at, conversation, messages, config_versions, tool_events, guardrail_events, annotations]
                properties:
                  export_version:
                    type: string
                    description: Export document contract version. Currently `"1"`.
                  generated_at:
                    type: string
                    format: date-time
                  conversation:
                    type: object
                    required: [id, org_id, environment, instance_id, end_user, title, status, created_at, updated_at]
                    properties:
                      id: {type: string, format: uuid}
                      org_id: {type: string}
                      environment: {type: string}
                      instance_id: {type: string, format: uuid}
                      end_user: {type: string}
                      title:
                        type: [string, 'null']
                      status: {type: string, enum: [active, archived]}
                      created_at: {type: string, format: date-time}
                      updated_at: {type: string, format: date-time}
                  messages:
                    type: array
                    description: |
                      The complete transcript in insertion order. An export is
                      an audit artifact, so it keeps EVERY version of a revised
                      message, not only the branch the conversation serves;
                      `parent_message_id` records how the versions relate.
                    items:
                      type: object
                      required: [id, role, content, turn_id, config_version_hash, created_at, parent_message_id]
                      properties:
                        id: {type: string, format: uuid}
                        role: {type: string, enum: [user, assistant]}
                        turn_id: {type: string, format: uuid}
                        config_version_hash: {type: string}
                        parent_message_id:
                          type: [string, 'null']
                          format: uuid
                          description: |
                            The message this one follows on its branch; null
                            when it starts the conversation.
                        content:
                          type: array
                          items:
                            type: object
                            additionalProperties: true
                        created_at: {type: string, format: date-time}
                  config_versions:
                    type: array
                    description: Every distinct config version that served a message.
                    items:
                      type: object
                      required: [hash]
                      properties:
                        hash: {type: string}
                        released_version:
                          type: string
                          description: |
                            The newest released instance version carrying this hash,
                            as an integer string (for example `"4"`); omitted
                            when the hash was never released.
                  tool_events:
                    type: array
                    description: In the order they were recorded, oldest first.
                    items:
                      type: object
                      required: [id, turn_id, tool_name, execution, outcome, duration_ms, created_at]
                      properties:
                        id: {type: string, format: uuid}
                        turn_id: {type: string, format: uuid}
                        tool_name: {type: string}
                        execution: {type: string}
                        outcome: {type: string}
                        duration_ms: {type: integer}
                        created_at: {type: string, format: date-time}
                  guardrail_events:
                    type: array
                    description: In the order they were recorded, oldest first.
                    items:
                      type: object
                      required: [id, turn_id, config_version_hash, rule_id, decision, created_at]
                      properties:
                        id: {type: string, format: uuid}
                        turn_id: {type: string, format: uuid}
                        config_version_hash: {type: string}
                        rule_id: {type: string}
                        decision: {type: string}
                        created_at: {type: string, format: date-time}
                  annotations:
                    type: array
                    description: Every non-retracted annotation on the conversation, API and portal submitted, newest first.
                    items:
                      $ref: '#/components/schemas/ConversationAnnotation'
        '400':
          description: '`invalid_request`: `end_user` is missing or blank. `missing_instance_slug`: the slug in the path is blank.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks `conversations:export` (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`conversation_not_found`: the id is malformed or unknown, or the conversation belongs to another end user, key environment, or instance. `instance_not_found`: no live instance with this slug.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: '`internal_error`: the instance, transcript, tool events, guardrail events, annotations or released versions could not be read. `auth_context_missing`: the authenticated context was incomplete before the route ran.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/eval-scenarios:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
    get:
      operationId: listConversationEvalScenarios
      summary: List Conversation eval scenarios
      description: |
        Lists the instance's active (not retired) eval scenarios in authoring
        order, optionally for one suite. Requires `conversations:use` for
        scoped keys.
      parameters:
        - name: suite
          in: query
          required: false
          schema:
            type: string
            enum: [gate, workflows, adversarial, smoke]
      tags: [Conversations]
      responses:
        '200':
          description: The instance's active eval scenarios.
          content:
            application/json:
              schema:
                type: object
                required: [scenarios]
                properties:
                  scenarios:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConversationEvalScenario'
        '400':
          description: The instance slug is empty (`missing_instance_slug`), or `suite` is not one of `gate`, `workflows`, `adversarial`, `smoke` (`invalid_request`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance was not found in this organization (`instance_not_found`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    post:
      operationId: createConversationEvalScenario
      summary: Author a Conversation eval scenario
      description: |
        Authors one eval scenario: a scripted multi-turn conversation plus an
        expected-behavior rubric. The publish gate runs every active `gate`-suite scenario
        against a candidate config with real model calls, so the set is
        bounded: at most 40 active scenarios in the `gate` suite and 200 in
        each other suite (a further authoring attempt rejects with
        `conversation_eval_scenario_cap`), at most 8 scripted turns per
        scenario. Authored rubrics must carry at least one deterministic
        check (`must_refuse`, `must_escalate`, `must_call_tools`,
        `must_emit_components`, `must_cite`, `must_not_refuse`,
        `final_must_not_contain`): deterministic checks alone decide
        pass/fail; `judge` dimensions record scores and never decide.
        Organization API keys only: every scoped key is refused with
        `403 insufficient_capability`. A follower instance refuses with
        `409 instance_follows_canonical`.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, script, rubric]
              properties:
                name:
                  type: string
                  maxLength: 200
                  description: Must not be blank. At most 200 UTF-8 bytes.
                script:
                  $ref: '#/components/schemas/ConversationEvalScript'
                rubric:
                  $ref: '#/components/schemas/ConversationEvalRubric'
                suite:
                  type: string
                  enum: [gate, workflows, adversarial, smoke]
                  default: gate
                  description: |
                    Defaults to `gate` (the publish-gate set, capped at 40
                    active scenarios). The on-demand suites run only when a
                    run of that suite is requested and are capped at 200
                    active scenarios each.
      responses:
        '201':
          description: The created scenario.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationEvalScenario'
        '400':
          description: |
            The instance slug is empty (`missing_instance_slug`), the body is not
            JSON (`invalid_request`), or the scenario is invalid
            (`invalid_eval_scenario`): an unknown `suite`, a blank or too long
            `name`, a script that is not 1 to 8 turns with a non-empty
            `message` each (or is over 64 KiB), a rubric over 16 KiB or with an
            unknown field, a rubric with no deterministic check, or an empty
            entry in a rubric list. Validation issues are listed in `details`,
            each with `path` and `message`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Every scoped key is refused (`insufficient_capability`), whatever capabilities it holds, or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance was not found in this organization (`instance_not_found`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            The suite already carries its maximum of active scenarios
            (`conversation_eval_scenario_cap`), or the instance follows a platform-managed source instance (`instance_follows_canonical`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /api/v1/conversation-instances/{instance_slug}/eval-scenarios/{scenario_id}:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: scenario_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    patch:
      operationId: updateConversationEvalScenario
      summary: Correct a Conversation eval scenario
      description: |
        Corrects a stored scenario in place, keeping its id and with it every
        recorded run result that references it. Every field is optional; the
        fields you omit keep their stored values, and the merged scenario is
        validated as a whole, so an edit can never leave a scenario without a
        deterministic rubric check. Moving a scenario into another suite is
        subject to that suite's active-scenario cap
        (`conversation_eval_scenario_cap`). Editing a `gate` scenario changes
        the scenario-set fingerprint the publish gate pins, so an edit arriving
        mid-publish aborts that release rather than shipping an unevaluated
        set. Organization API keys only: every scoped key is refused with 403 insufficient_capability.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              properties:
                name:
                  type: string
                  maxLength: 200
                  description: Must not be blank. At most 200 UTF-8 bytes.
                script:
                  $ref: '#/components/schemas/ConversationEvalScript'
                rubric:
                  $ref: '#/components/schemas/ConversationEvalRubric'
                suite:
                  type: string
                  enum: [gate, workflows, adversarial, smoke]
      responses:
        '200':
          description: The updated scenario.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationEvalScenario'
        '400':
          description: |
            The instance slug is empty (`missing_instance_slug`), the body is not
            JSON or names none of `name`, `script`, `rubric`, `suite`
            (`invalid_request`; unknown fields are ignored), or the merged
            scenario is invalid (`invalid_eval_scenario`), including an unknown
            `suite`. Validation issues are listed in `details`, each with `path`
            and `message`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Every scoped key is refused (`insufficient_capability`), whatever capabilities it holds, or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`) or an active scenario with this id (`conversation_eval_scenario_not_found`) was not found. A malformed or retired scenario id answers the same way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            The suite already carries its maximum of active scenarios
            (`conversation_eval_scenario_cap`), or the instance follows a platform-managed source instance (`instance_follows_canonical`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    delete:
      operationId: retireConversationEvalScenario
      summary: Retire a Conversation eval scenario
      description: |
        Retires a scenario from the active set. The row is soft-deleted, so
        the recorded results of past runs that measured it stay readable.
        Retirement removes the scenario from every later selection: the next
        publish gate, and any on-demand run that has not yet read its pending
        scenarios. An on-demand run that already read its list may still run
        it. Retiring a `gate` scenario while a publish's gate is running aborts
        that publish with `409 scenario_set_changed_during_publish`.
        Retiring a scenario that is already retired (or does not exist in this
        instance) is a 404. Organization API keys only: every scoped key is refused with 403 insufficient_capability.
      tags: [Conversations]
      responses:
        '204':
          description: The scenario was retired.
        '400':
          description: '`missing_instance_slug`: the slug in the path is blank (for example a single encoded space).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Every scoped key is refused (`insufficient_capability`), whatever capabilities it holds, or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`) or an active scenario with this id (`conversation_eval_scenario_not_found`) was not found. A malformed or retired scenario id answers the same way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The instance follows a platform-managed source instance (`instance_follows_canonical`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /api/v1/conversation-instances/{instance_slug}/annotations/{annotation_id}/promote:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: annotation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      operationId: promoteConversationAnnotation
      summary: Promote an annotation to an eval scenario
      description: |
        One action from an annotated turn (usually a downrated one) to an
        eval scenario: the scenario's script is drafted from the annotated
        conversation's real transcript (the annotated turn's own branch, up to
        and including that turn, at most the last 8 turns, read from the
        conversation's newest 500 messages; recorded client tool
        results replay verbatim including error state, and every client call
        with a recorded result scripts as confirmed so the model-visible
        history reproduces). The rubric is REQUIRED and must carry at least
        one deterministic check: a scenario that could never fail would sit
        active against the scenario cap until someone corrected it.
        Organization API keys only: every scoped key is refused with 403 insufficient_capability.

        The drafted scenario always goes into the gate suite.
      tags: [Conversations]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rubric]
              properties:
                name:
                  type: string
                  maxLength: 200
                  description: Optional scenario name, at most 200 UTF-8 bytes; defaults to a name derived from the annotation id.
                rubric:
                  $ref: '#/components/schemas/ConversationEvalRubric'
      responses:
        '201':
          description: The drafted scenario.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationEvalScenario'
        '400':
          description: The instance slug is empty (`missing_instance_slug`), the body is not JSON (`invalid_request`), or `rubric` is absent (`invalid_eval_scenario`). A rubric that is present but invalid, including `{}` or a judge-only rubric, is 422 `annotation_not_promotable`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Every scoped key is refused (`insufficient_capability`), whatever capabilities it holds, or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`) or the annotation (`annotation_not_found`) was not found. A malformed or deleted annotation id answers the same way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            The `gate` suite already carries its maximum of 40 active scenarios
            (`conversation_eval_scenario_cap`), or the instance follows a platform-managed source instance (`instance_follows_canonical`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: 'The annotation cannot be promoted (`annotation_not_promotable`): the annotated turn is not among the conversation''s newest 500 messages, the conversation has no scriptable user turns, or the drafted scenario is invalid (a rubric with no deterministic check, such as `{}` or judge-only, a rubric with an unknown field, or a name over 200 bytes). For an invalid drafted scenario, `details` lists the issues, each with `path` and `message`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/versions/{version}/eval-run:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: version
        in: path
        required: true
        schema: {type: integer, minimum: 1}
        description: Released instance version (a positive integer, e.g. `3`).
    get:
      operationId: getConversationEvalRun
      summary: Get a version's Conversation eval run
      description: |
        Returns the latest recorded eval run for a released version of a
        Conversation instance, whatever started it: the publish gate run
        first, then any later on-demand run for the same version, which
        replaces it in this answer. The body carries the run's status
        (`running`, `passed`, `failed`, `waived` with who and why, or
        `error`), per-scenario verdicts and scores, and the
        diff against the immediately preceding version's recorded run
        (scenario names that newly fail, that fail with no prior result, and
        that newly pass). The diff is omitted when the preceding version has
        no recorded run. A version published while the instance had no
        `gate` scenarios, and not run on demand since, has no run and returns
        `conversation_eval_run_not_found`. Requires `conversations:use`.
      tags: [Conversations]
      responses:
        '200':
          description: The version's recorded eval run.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationEvalRunDetail'
        '400':
          description: 'The instance slug is empty (`missing_instance_slug`), or `version` is not a positive integer (`instance_version_invalid_format`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            The instance (`instance_not_found`), the version
            (`instance_version_not_found`), or any recorded eval run for it
            (`conversation_eval_run_not_found`) was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /health:
    get:
      operationId: getHealth
      summary: Health check
      description: |
        Liveness check. Needs no key. Answers 200 whenever the handler
        answers; database or queue trouble shows as `status: degraded` in the
        body, never as a non-200 status. The shared request middleware can
        still answer `500 internal_error` (an unexpected handler failure) or
        `504 request_timeout` (past the 30-second request limit), as on every
        route; treat either as the API not answering. `commit` is the running
        build and is absent when the build is unstamped. There is no
        `/api/v1/health` route.
      tags: [Platform]
      security: []
      responses:
        '200':
          description: The API process is up. Read `status` for dependency health.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              examples:
                ok:
                  value:
                    status: ok
                    commit: 0f3c1a9e7b2d4c6a8e1f3b5d7a9c2e4f6b8d0a1c
                degraded:
                  value:
                    status: degraded
                    commit: 0f3c1a9e7b2d4c6a8e1f3b5d7a9c2e4f6b8d0a1c
        '500':
          $ref: '#/components/responses/InternalError'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/attachments/reserve:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      operationId: reserveConversationAttachment
      summary: Reserve an attachment for a direct upload
      description: |
        Step one of the direct upload path. Opens an attachment record in
        status `storing` and returns `upload_url`, a presigned PUT URL for
        this one file that expires in 15 minutes. PUT the raw bytes to
        `upload_url` without an `Authorization` header, then call finalize.
        Use this path to send bytes from a browser straight to storage: the API sends no CORS headers, and the URL carries no read
        capability and reaches no other file.

        The declared `filename` and `size_bytes` are checked here only so a
        file that cannot be accepted is refused before it is uploaded.
        Finalize measures the bytes that actually arrived and is the check
        that binds. The instance's latest published `attachments` config is
        the policy. A record nobody finalizes becomes eligible for removal
        one hour after it was reserved; the daily cleanup pass removes it,
        and a failed pass can delay removal further.

        Requires `conversations:use` for scoped keys. Scope is org,
        conversation, and end user; a miss on any of them is `404`.
      tags: [Conversations]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [end_user, filename, size_bytes]
              properties:
                end_user:
                  type: string
                  description: The asserted end-user identity the conversation must belong to.
                filename:
                  type: string
                  description: |
                    File name. May carry directories; each segment is cleaned
                    and traversal segments are dropped. The extension decides
                    the media type.
                content_type:
                  type: string
                  description: Declared media type. Used only when the name has no extension.
                size_bytes:
                  type: integer
                  format: int64
                  minimum: 1
                  description: Declared size in bytes. Zero or less is refused as an empty file.
            example:
              end_user: u_dana_ortiz
              filename: statement-of-values.xlsx
              content_type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
              size_bytes: 18874368
      responses:
        '201':
          description: The reserved record and its upload URL.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationAttachmentReservation'
        '400':
          description: |
            The instance slug is empty (`missing_instance_slug`); the body is
            unreadable, has an unknown field, or has no
            `filename` (`invalid_request`); `end_user` is missing
            (`invalid_request`); the published config does not enable
            attachments (`attachments_not_enabled`); or the file is refused
            for its shape (`attachment_rejected`): declared empty, or the
            conversation already holds 100 attachments.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`) or the conversation (`conversation_not_found`) is not in scope. A conversation in another organization, under another end user, or created with a key from another environment answers the same way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The instance has no published version, so no upload policy is in force (`instance_not_published`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: The declared size is over the instance's per-file limit (`attachment_too_large`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          description: The type is outside the accepted set or narrowed away by the instance (`attachment_rejected`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The published config cannot be parsed (`instance_config_invalid`) or the upload could not be started (`internal_error`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AttachmentsUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/attachments/folders:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      operationId: openConversationAttachmentFolder
      summary: Open a folder of attachments
      description: |
        Opens a folder container, a container your application opens, from
        its list of files, and returns one reservation (with its own
        `upload_url`) per file the instance will read. PUT each member's
        bytes to its `upload_url`, then finalize each member by its
        `attachment.id`. The folder is `ready` when every member is
        finalized, and a turn names the folder by `container.id`.

        At most 25 files per folder. Files the instance will not read are
        named in `skipped` with a reason, and the rest of the folder still
        opens. A folder with nothing readable is refused. A folder's identity
        is its roster: each member's path and declared size. File contents
        are not compared, so a file changed without changing its size is not
        detected. When a folder with the same roster is already open and
        `ready` in the conversation, the response is `200` with `reused: true` and nothing
        needs uploading; otherwise a new folder is opened with `201`. A
        folder counts as one attachment toward `max_files_per_message`, and
        the folder plus its files count toward the conversation's 100
        attachment limit. The folder record has `container_kind: folder`,
        `delivery: unwrap`, and status `unwrapping` until every member is
        finalized.

        Requires `conversations:use` for scoped keys. Scope is org,
        conversation, and end user; a miss on any of them is `404`.
      tags: [Conversations]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [end_user, name, files]
              properties:
                end_user:
                  type: string
                  description: The asserted end-user identity the conversation must belong to.
                name:
                  type: string
                  description: Folder name.
                files:
                  type: array
                  maxItems: 25
                  items:
                    type: object
                    additionalProperties: false
                    required: [path, size_bytes]
                    properties:
                      path:
                        type: string
                        description: The file's path inside the folder, as your application holds it. Echoed back as `source_path`.
                      size_bytes:
                        type: integer
                        format: int64
                        description: Declared size in bytes. Zero or less is skipped as empty; over the per-file limit is skipped as too large.
            example:
              end_user: u_dana_ortiz
              name: supplier-contracts
              files:
                - path: supplier-contracts/contract-2026.pdf
                  size_bytes: 482113
                - path: supplier-contracts/contract-2025.pdf
                  size_bytes: 391208
      responses:
        '200':
          description: The same folder was already open and ready; it is returned with `reused` true and no members to upload.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationAttachmentFolder'
        '201':
          description: The opened folder and one reservation per readable file.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationAttachmentFolder'
        '400':
          description: |
            The instance slug is empty (`missing_instance_slug`), the body is
            unreadable, has an unknown field, or has no `name`
            (`invalid_request`), `end_user` is missing (`invalid_request`), the
            published config does not enable attachments
            (`attachments_not_enabled`), or the folder is refused for its shape
            (`attachment_rejected`): it has no files, more than 25 files, or
            the folder and its readable files would take the conversation past
            its limit of 100 attachments.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`) or the conversation (`conversation_not_found`) is not in scope. A conversation in another organization, under another end user, or created with a key from another environment answers the same way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The instance has no published version (`instance_not_published`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: The readable files together exceed what one message can carry once base64-encoded, 52428800 bytes (`attachment_too_large`). A single file over the per-file limit is not refused here; it is listed in `skipped`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          description: No file in the folder is left to read once unreadable, oversized, empty, archive and unaccepted files are skipped (`attachment_rejected`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The published config cannot be parsed (`instance_config_invalid`) or the folder could not be opened (`internal_error`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AttachmentsUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/attachments/policy:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getConversationAttachmentPolicy
      summary: Get the attachment policy
      description: |
        Returns the upload policy the instance's latest published config
        enforces: the numbers a client checks before uploading.
        An instance that does not accept attachments answers
        `400 attachments_not_enabled`, so `enabled` is `true` on every
        `200`. This route reads no storage and answers even where
        attachment storage is not configured. Requires `conversations:use`
        for scoped keys.
      tags: [Conversations]
      security:
        - BearerAuth: []
      parameters:
        - name: end_user
          in: query
          required: true
          schema: {type: string}
          description: The asserted end-user identity the conversation must belong to.
      responses:
        '200':
          description: The resolved policy.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationAttachmentPolicy'
        '400':
          description: 'The instance slug is empty (`missing_instance_slug`), `end_user` is missing (`invalid_request`), or the published config does not enable attachments (`attachments_not_enabled`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`) or the conversation (`conversation_not_found`) is not in scope. A conversation in another organization, under another end user, or created with a key from another environment answers the same way.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The instance has no published version (`instance_not_published`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The published config cannot be parsed (`instance_config_invalid`), or a lookup failed (`internal_error`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/conversations/{conversation_id}/attachments/{attachment_id}/finalize:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: conversation_id
        in: path
        required: true
        schema: {type: string, format: uuid}
      - name: attachment_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    post:
      operationId: finalizeConversationAttachment
      summary: Finalize a reserved attachment
      description: |
        Step three of the direct upload path. Reads the bytes that arrived
        on the reservation, measures their size, resolves the media type from
        the name and the full content, hashes them, and marks the record
        `ready`, or refuses it. When the bytes never arrived, the refusal is
        `400 attachment_rejected` and the record stays `storing`, so you can
        PUT the bytes and finalize again; the cleanup pass removes a record
        still `storing` after an hour. Any other refusal removes the record
        from lists at once. Nothing declared at reserve time is trusted. A
        zip or email is opened here by the same rules as a multipart upload
        (at most 25 accepted files kept; see
        [How a zip or email is opened](/conversations/attachments#how-a-zip-or-email-is-opened)).

        Safe to repeat: finalizing a record that is already `ready` returns
        it again. When the bytes are identical to a ready file the
        conversation already holds under the same name, the reservation is
        retired and the existing record is returned with its own `id`; use
        the `id` from this response. Files inside a folder are exempt from
        that reuse. Requires `conversations:use` for scoped keys.
      tags: [Conversations]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [end_user]
              properties:
                end_user:
                  type: string
                  description: The asserted end-user identity the conversation must belong to.
            example:
              end_user: u_dana_ortiz
      responses:
        '200':
          description: The ready attachment.
          content:
            application/json:
              schema:
                type: object
                required: [attachment]
                properties:
                  attachment:
                    $ref: '#/components/schemas/ConversationAttachment'
        '400':
          description: |
            The instance slug is empty (`missing_instance_slug`), the body is
            unreadable or has an unknown field (`invalid_request`), `end_user`
            is missing (`invalid_request`), the attachment id segment is empty
            (`invalid_request`), the published config does not
            enable attachments (`attachments_not_enabled`), or the file is
            refused for its shape (`attachment_rejected`): the bytes never
            arrived, the file is empty, a container could not be opened or a
            file inside it could not be read, no file could be extracted
            from a container, or a container holds more files than the
            conversation has room for under its limit of 100 attachments.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`), the conversation (`conversation_not_found`) or the attachment (`attachment_not_found`) is not in scope, or the attachment was deleted or refused.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: |
            The instance has no published version (`instance_not_published`),
            or the record exists but is not waiting for bytes, for example it
            is a folder container or a container still unwrapping
            (`attachment_not_reservable`). A deleted or refused attachment
            answers 404 `attachment_not_found`. A record that is already `ready` returns
            200 with the record, so a retried finalize is safe.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: The measured size is over the per-file limit (`attachment_too_large`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '415':
          description: The type is not accepted, the content disagrees with the name, or every file extracted from a container fails those checks (`attachment_rejected`). A container from which no file could be extracted, or with a file inside that cannot be read, is `400`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The published config cannot be parsed (`instance_config_invalid`) or the file could not be saved (`internal_error`). An `attachment_id` that is not a UUID also answers 500 `internal_error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AttachmentsUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/eval-runs:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
    post:
      operationId: createConversationEvalRun
      summary: Start an on-demand eval run
      description: |
        Queues a run of one eval suite against the instance's latest
        published version (never the draft). The run executes in the
        background with real model calls metered to the organization, one
        scenario after another; poll the run until its status leaves
        `running`. Platform write tools run as a dry run during evals.

        Scoped keys are refused (`403 insufficient_capability`); use an
        organization API key or the portal's Evals tab. A follower instance
        refuses with `instance_follows_canonical`.
      tags: [Conversations]
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [suite]
              properties:
                suite:
                  type: string
                  enum: [gate, workflows, adversarial, smoke]
            example:
              suite: workflows
      responses:
        '201':
          description: The queued run.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationEvalOnDemandRun'
        '400':
          description: The instance slug is empty (`missing_instance_slug`), or the body is not JSON or `suite` is not one of the four suites (`invalid_request`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Every scoped key is refused (`insufficient_capability`), whatever capabilities it holds, or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance was not found in this organization (`instance_not_found`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: The instance has no published version (`instance_not_published`) or follows a platform-managed instance (`instance_follows_canonical`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The run could not be started (`internal_error`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            No eval executor is configured in this deployment (`conversation_eval_unavailable`).

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    get:
      operationId: listConversationEvalRuns
      summary: List eval runs
      description: |
        Lists the instance's eval runs, publish gate runs and on-demand runs
        alike, newest first. Requires `conversations:use` for scoped keys.
      tags: [Conversations]
      security:
        - BearerAuth: []
      parameters:
        - name: suite
          in: query
          required: false
          schema:
            type: string
            enum: [gate, workflows, adversarial, smoke]
          description: Only runs of this suite.
        - name: limit
          in: query
          required: false
          schema: {type: integer, minimum: 1, maximum: 200, default: 50}
      responses:
        '200':
          description: Runs, newest first.
          content:
            application/json:
              schema:
                type: object
                required: [runs]
                properties:
                  runs:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConversationEvalOnDemandRun'
        '400':
          description: 'The instance slug is empty (`missing_instance_slug`), or `suite` is not a known suite or `limit` is not an integer from 1 to 200 (`invalid_request`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance was not found in this organization (`instance_not_found`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The runs could not be listed (`internal_error`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/eval-runs/{run_id}:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: run_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: getConversationEvalRunDetail
      summary: Get an eval run
      description: |
        Returns one eval run with a result per scenario recorded so far.
        While the run is `running`, `results` grows as scenarios finish.
        Requires `conversations:use` for scoped keys.
      tags: [Conversations]
      security:
        - BearerAuth: []
      responses:
        '200':
          description: The run and its results.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationEvalOnDemandRunDetail'
        '400':
          description: '`missing_instance_slug`: the slug in the path is blank (for example a single encoded space).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: The instance (`instance_not_found`) or the run (`conversation_eval_run_not_found`) was not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The run or its results could not be loaded (`internal_error`). A `run_id` that is not a UUID also answers 500 `internal_error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/conversation-instances/{instance_slug}/eval-runs/{run_id}/diff:
    parameters:
      - $ref: '#/components/parameters/InstanceSlug'
      - name: run_id
        in: path
        required: true
        schema: {type: string, format: uuid}
    get:
      operationId: diffConversationEvalRuns
      summary: Diff an eval run against a baseline
      description: |
        Compares the run's per-scenario verdicts with a baseline run's, by
        scenario, and reports scenario names. The baseline is `?baseline=`
        when given, otherwise the previous completed run of the same suite.
        Requires `conversations:use` for scoped keys.
      tags: [Conversations]
      security:
        - BearerAuth: []
      parameters:
        - name: baseline
          in: query
          required: false
          schema: {type: string, format: uuid}
          description: The run to compare against. Omit to use the previous completed run of the same suite.
      responses:
        '200':
          description: The diff.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationEvalRunDiff'
        '400':
          description: '`missing_instance_slug`: the slug in the path is blank (for example a single encoded space).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: A scoped key lacks the capability (`insufficient_capability`), or the instance is archived (`instance_archived`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: |
            The instance (`instance_not_found`), the run or the named baseline
            (`conversation_eval_run_not_found`) was not found, or no earlier
            finished (`passed`, `failed` or `waived`) run
            of the suite exists (`conversation_eval_baseline_not_found`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: The runs or their results could not be loaded (`internal_error`). A `run_id` or `baseline` that is not a UUID also answers 500 `internal_error`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/events/ingest/{source_key}:
    post:
      operationId: ingestEvent
      summary: Send an inbound event
      description: |
        Append one event to your organization's event log through an ingest
        source that Nexio registered for you. The route takes no API key. A
        `nexio` or `github` source's request is authenticated by an
        HMAC-SHA256 signature that covers the raw body, keyed with the source's
        signing secret (`evsec_...`). An `ams360_ons` source (notifications from AMS360, a supported
        system type) cannot sign a body: it authenticates by the
        authentication code the sending system includes with each notification, and answers
        `401 invalid_credentials` when the code is missing or wrong. The source
        key in the path alone decides the organization.

        Signature: `v1 = hex(HMAC-SHA256(secret, timestamp + "." + raw_body))`,
        sent as `X-Nexio-Signature: t=<timestamp>,v1=<hex>`. The timestamp must
        be within 300 seconds of Nexio's clock.

        Idempotency: the same `X-Nexio-Delivery` with the same bytes appends
        nothing and returns the first delivery's outcome: a 200 with
        `inserted: false`, or the same 422. The same
        delivery ID with different bytes answers `409 delivery_id_reused`.

        Checks run in this order: rate limit, body size, source key, signature
        or authentication code, source enabled, content. Rate limits: 120
        requests per minute per client address and 600 per minute per source key.

        This schema describes `nexio` sources. A `github` source instead accepts
        GitHub's own `deployment_status` webhook body with `X-Hub-Signature-256`,
        `X-GitHub-Delivery`, and `X-GitHub-Event`, and records a `code.deployed`
        event. An `ams360_ons` source sends neither `X-Nexio-Signature`
        nor `X-Nexio-Delivery`: its delivery identity is the SHA-256 of the
        body, so a repeated identical notification appends nothing. See
        /events/inbound.
      tags: [Events]
      security: []
      parameters:
        - name: source_key
          in: path
          required: true
          description: The ingest source key Nexio issued (`evsrc_` followed by 64 hex characters).
          schema:
            type: string
            example: evsrc_example000000000000000000000000000000000000000000000000000000000
        - name: X-Nexio-Delivery
          in: header
          required: false
          description: Required for `nexio` sources. Your unique ID for this delivery. Reuse it only to retry the same bytes.
          schema:
            type: string
            example: 0d9b7c4e-2a61-4f38-9e15-6c3a8b2f7d90
        - name: X-Nexio-Timestamp
          in: header
          required: false
          description: Required for `nexio` sources. Current Unix time in seconds, decimal digits only. Must be within 300 seconds of Nexio's clock.
          schema:
            type: string
            pattern: '^[0-9]+$'
            example: '1790175845'
        - name: X-Nexio-Signature
          in: header
          required: false
          description: |
            Required for `nexio` sources.
            `t=<timestamp>,v1=<hex HMAC-SHA256 of timestamp + "." + raw body>`.
            `t` must equal `X-Nexio-Timestamp`. More than one `v1` value is
            accepted; the request passes when any one matches.
          schema:
            type: string
            example: t=1790175845,v1=5c2f8e1a9b3d7c4e6f0a2b8d1e9c7a3f5b6d4e2c8a0f1b3d9e7c5a2f4b6d8e0c
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IngestEventEnvelope'
            example:
              type: crm.account_updated
              subject: account/ACC-10442
              body:
                account_id: ACC-10442
                changed_fields: [mailing_address, primary_contact]
                updated_by: dana.ortiz
              occurred_at: '2026-09-23T15:04:05Z'
      responses:
        '200':
          description: Delivery recorded. `inserted` is `false` for a byte-identical retry of a delivery already recorded.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestDeliveryResponse'
              examples:
                appended:
                  summary: Event appended
                  value:
                    delivery_id: 0d9b7c4e-2a61-4f38-9e15-6c3a8b2f7d90
                    outcome: appended
                    event_id: 4a8e2d71-5c93-4b06-8f1e-9d2c7a3b6e58
                    inserted: true
                replay:
                  summary: Byte-identical retry
                  value:
                    delivery_id: 0d9b7c4e-2a61-4f38-9e15-6c3a8b2f7d90
                    outcome: appended
                    event_id: 4a8e2d71-5c93-4b06-8f1e-9d2c7a3b6e58
                    inserted: false
                ignored:
                  summary: Valid but not translated (GitHub source)
                  value:
                    delivery_id: 72d1e6b0-9a4f-11ef-8c3d-5e7a1b2c9d40
                    outcome: ignored
                    reason: event_not_translated
                    inserted: true
        '400':
          description: The body could not be read.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: invalid_body
                message: could not read the delivery body
        '401':
          description: |
            `invalid_signature`: a required header is missing or malformed, `t`
            does not equal `X-Nexio-Timestamp`, or no signature matches an
            accepted secret. `stale_timestamp`: the timestamp is more than 300
            seconds from Nexio's clock. `ingest_source_not_found`: no ingest
            source has this key; an unknown key is an authentication failure,
            so it answers 401, not 404. `invalid_credentials`: an `ams360_ons`
            source's delivery carried no accepted authentication code.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                invalidSignature:
                  summary: No matching signature
                  value:
                    code: invalid_signature
                    message: invalid signature
                staleTimestamp:
                  summary: Timestamp outside the window
                  value:
                    code: stale_timestamp
                    message: signature timestamp outside the replay window
                unknownSource:
                  summary: No ingest source has this key
                  value:
                    code: ingest_source_not_found
                    message: unknown ingest source
        '403':
          description: The delivery authenticated (signature or authentication code) but the source is disabled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: ingest_source_disabled
                message: ingest source is disabled
        '409':
          description: The delivery ID was already recorded with a different body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: delivery_id_reused
                message: delivery_id was previously recorded for a different body
        '413':
          description: The body is larger than 1 MiB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: body_too_large
                message: delivery body exceeds the size limit
        '422':
          description: |
            The delivery was recorded as rejected. `code` is the reason:
            `event_type_not_allowed`, `event_type_reserved`, `subject_required`,
            `body_not_object`, `invalid_payload`, and for GitHub sources
            `repository_not_allowed`, `environment_not_allowed`,
            `commit_sha_required`. A retry of the same bytes returns the same 422.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: event_type_not_allowed
                message: delivery was refused by the ingest source configuration
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: |
            `internal_error`: Nexio could not look up the source, read its
            secret, or record the delivery; retry with the same delivery ID and
            bytes. `ingest_source_misconfigured`: the source's stored kind or
            configuration is invalid; contact Nexio.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                code: internal_error
                message: could not record the delivery
        '504':
          $ref: '#/components/responses/RequestTimeout'

  /api/v1/records/actions:
    post:
      operationId: appendRecordsActions
      summary: Append commands to the action ledger
      description: |-
        Records 1 to 20 commands for one person in one transaction: all are recorded or none. Each command addresses one system-of-record record the person may open (by `client_key` or `policy_key`), or (notes only) one Catalog entity. A Catalog target is not looked up or checked against the person's access, and a batch of only Catalog notes resolves no identity against the system of record. The command set is fixed in code: a batch that contains `source_activity.create` or `renewal_decision.set` is refused whole with 403 `overlay_read_only`, and the internal `book_edit_intent.*` commands answer 400 `action_schema_unknown`. Nothing is written to the connected system of record.

        Every command carries an `idempotency_key` (UUID). Resending a command with the same key, command, `schema_rev`, target, `payload`, `basis` and `actor_principal` returns its original `command_id` and `seq` with `idempotent_replay: true` and status 200 (`id` and `issued_at` are not compared). Reusing a key with any of those different answers 400 `action_payload_invalid`. A session with `X-Nexio-Records-Lens` cannot write.

        Credential: the organization's live API key, or a scoped key with `actions:write`.
      tags:
        - Records
      security:
        - BearerAuth: []
      parameters:
        - name: X-Nexio-Acting-Principal
          in: header
          required: false
          description: 'The person the request is for, as your identity provider''s stable user id. Optional on this route; when absent, the body''s `actor_principal` is used. When both are sent they must match, or the request answers 400 `invalid_request`. Narrows what the key reaches.'
          schema:
            type: string
        - name: X-Nexio-Acting-Email
          in: header
          required: false
          description: The acting person's verified sign-in email. The seat is derived from it.
          schema:
            type: string
        - name: X-Nexio-Records-Assertion
          in: header
          required: false
          description: 'Signed assertion `v1.<unix seconds>.<hex HMAC-SHA256>` over `acting_principal|acting_email|reserved|timestamp` (each part trimmed, the email lowercased). The third field is reserved: send an empty string. The timestamp must be within 5 minutes of the server clock, either way. Checked once Nexio enables assertion verification for your organization, when it provisions the signing secret; a missing or wrong assertion then answers 403 `assertion_invalid` and one outside the window 403 `assertion_stale`.'
          schema:
            type: string
        - name: X-Nexio-Records-Lens
          in: header
          required: false
          description: 'Do not send on this route. Any non-empty value refuses the write with 403 `book_lens_read_only`.'
          schema:
            type: string
        - name: connection_id
          in: query
          required: false
          description: 'The system-of-record connection to use when a command addresses a system-of-record record. Optional when the organization has exactly one qualifying connection; required when it has several (otherwise 400 `book_connection_ambiguous`). Ignored for a batch of Catalog notes only. A connection that does not exist in the organization answers 404 `not_found`.'
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RecordsActionsAppendRequest'
      responses:
        '200':
          description: The recorded (or replayed) commands.
          headers:
            X-Nexio-Engine-Build:
              description: The engine commit that computed this response, when the build is stamped. Key caches on it.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecordsActionsAppendResponse'
        '400':
          description: '`invalid_request` (body is not one JSON value or is over 1 MiB, `actor_principal` missing, `actor_type` not `producer` or `agent`, no commands or more than 20, `X-Nexio-Acting-Principal` differs from `actor_principal`, a command that does not name exactly one target, a key that does not decode, or `connection_id` not a UUID), `action_schema_unknown` (unknown command or `schema_rev`), `action_payload_invalid` (payload, `id` or `idempotency_key` invalid, a target type the command does not accept, or an idempotency key reused for a different command), or `book_connection_ambiguous`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`action_out_of_scope` (a target the person may not open), `overlay_read_only`, `book_lens_read_only`, `insufficient_capability`, `scoped_key_required`, `dataset_denied`, or an identity refusal (`identity_unmapped`, `identity_needs_review`, `identity_suspended`, `identity_stale`, `scope_unavailable`, `assertion_invalid`, `assertion_stale`, `service_identity_unknown`). Identity and policy checks run only when a command addresses an account or policy.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`not_found`: the named connection does not exist in this organization.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`book_unavailable`: the connected data cannot be read now to check the addressed records. `details.reason` names the warehouse cause when there is one; no reason means no qualifying connection exists yet. Nothing was recorded.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '499':
          $ref: '#/components/responses/ClientClosedRequest'
        '500':
          description: '`audit_write_failed` (nothing was recorded), `book_key_ambiguous` (a key matched more than one record), or `internal_error`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            `book_unavailable` with `details.reason: busy` when the warehouse statement queue is full, or `resolve_retry_exhausted`. Retry the same request; it is idempotent.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Sent with `book_unavailable` reason `busy`.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          $ref: '#/components/responses/RequestTimeout'
    get:
      operationId: listRecordsActions
      summary: List ledger commands
      description: |-
        The action ledger itself, command by command, oldest first after `since_seq`. A `Self` scope sees only commands the acting person authored; `All` and `Platform` see the whole ledger. With `X-Nexio-Records-Lens` on a `Self` person, the list still shows only the caller's own commands.

        Credential: the organization's live API key, or a scoped key with `actions:read`.
      tags:
        - Records
      security:
        - BearerAuth: []
      parameters:
        - name: X-Nexio-Acting-Principal
          in: header
          required: true
          description: The person the request is for, as your identity provider's stable user id. Required on this route; without it the request answers 403 `identity_unmapped`. Narrows what the key reaches.
          schema:
            type: string
        - name: X-Nexio-Acting-Email
          in: header
          required: false
          description: The acting person's verified sign-in email. The seat is derived from it.
          schema:
            type: string
        - name: X-Nexio-Records-Assertion
          in: header
          required: false
          description: 'Signed assertion `v1.<unix seconds>.<hex HMAC-SHA256>` over `acting_principal|acting_email|reserved|timestamp` (each part trimmed, the email lowercased). The third field is reserved: send an empty string. The timestamp must be within 5 minutes of the server clock, either way. Checked once Nexio enables assertion verification for your organization, when it provisions the signing secret; a missing or wrong assertion then answers 403 `assertion_invalid` and one outside the window 403 `assertion_stale`.'
          schema:
            type: string
        - name: X-Nexio-Records-Lens
          in: header
          required: false
          description: '`principal:<id>`: a read-only view of another person''s data, honored only for a caller whose own scope is All or Platform and ignored for anyone else. For such a caller, a value that is not `principal:<id>`, or names nobody, answers 400 `lens_target_unknown`.'
          schema:
            type: string
        - name: since_seq
          in: query
          required: false
          description: Return commands with `seq` greater than this. Defaults to 0.
          schema: {type: integer, format: int64, minimum: 0, default: 0}
        - name: limit
          in: query
          required: false
          description: Rows per page. Defaults to 100; values above 500 are treated as 500.
          schema: {type: integer, minimum: 1, default: 100}
        - name: family
          in: query
          required: false
          description: 'Command family: the part of the command name before the first dot, for example `note`, `task` or `status`. Exact match; an unknown family returns no rows.'
          schema:
            type: string
        - name: connection_id
          in: query
          required: false
          description: 'The system-of-record connection to use. Optional when the organization has exactly one qualifying connection; required when it has several (otherwise 400 `book_connection_ambiguous`). A connection that does not exist in the organization answers 404 `not_found`.'
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: One page of ledger rows.
          headers:
            X-Nexio-Engine-Build:
              description: The engine commit that computed this response, when the build is stamped. Key caches on it.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecordsLedgerResponse'
        '400':
          description: '`invalid_request` (`since_seq` negative or not an integer, `limit` not a positive integer, `connection_id` not a UUID), `lens_target_unknown`, or `book_connection_ambiguous`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`insufficient_capability`, `scoped_key_required`, `dataset_denied`, `lens_caller_unattributed`, or an identity refusal: `identity_unmapped` (including no `X-Nexio-Acting-Principal`), `identity_needs_review`, `identity_suspended`, `identity_stale`, `scope_unavailable` (including an Office scope), `assertion_invalid`, `assertion_stale`, `service_identity_unknown`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`not_found`: the named connection does not exist in this organization.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`book_unavailable`: the connected data cannot be read now to resolve the person''s access. `details.reason` names the warehouse cause when there is one; no reason means no qualifying connection exists yet.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '499':
          $ref: '#/components/responses/ClientClosedRequest'
        '500':
          description: '`internal_error`, or `audit_write_failed` when a lensed read could not be audited.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            `book_unavailable` with `details.reason: busy` when the warehouse statement queue is full, or `resolve_retry_exhausted`. Retry the same request; it is idempotent.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Sent with `book_unavailable` reason `busy`.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /api/v1/records/notes:
    get:
      operationId: listRecordsNotes
      summary: List notes
      description: |-
        The current notes projected from the action ledger for the connection, oldest first. Send `client_key` or `policy_key` to read one system-of-record record. No paging: when more than 10,000 rows on the connection match the filter (counted before rows outside your scope are removed), the read answers 400 `action_list_too_large`. Deleted notes, notes written by internal Nexio seats, and rows outside your scope are not returned. Rows about a `policy` record identify it by `ams360_datasource` and `policy_id`, the source's tenant key and record id.

        Credential: the organization's live API key, or a scoped key with `actions:read`.
      tags:
        - Records
      security:
        - BearerAuth: []
      parameters:
        - name: X-Nexio-Acting-Principal
          in: header
          required: true
          description: The person the request is for, as your identity provider's stable user id. Required on this route; without it the request answers 403 `identity_unmapped`. Narrows what the key reaches.
          schema:
            type: string
        - name: X-Nexio-Acting-Email
          in: header
          required: false
          description: The acting person's verified sign-in email. The seat is derived from it.
          schema:
            type: string
        - name: X-Nexio-Records-Assertion
          in: header
          required: false
          description: 'Signed assertion `v1.<unix seconds>.<hex HMAC-SHA256>` over `acting_principal|acting_email|reserved|timestamp` (each part trimmed, the email lowercased). The third field is reserved: send an empty string. The timestamp must be within 5 minutes of the server clock, either way. Checked once Nexio enables assertion verification for your organization, when it provisions the signing secret; a missing or wrong assertion then answers 403 `assertion_invalid` and one outside the window 403 `assertion_stale`.'
          schema:
            type: string
        - name: X-Nexio-Records-Lens
          in: header
          required: false
          description: '`principal:<id>`: a read-only view of another person''s data, honored only for a caller whose own scope is All or Platform and ignored for anyone else. For such a caller, a value that is not `principal:<id>`, or names nobody, answers 400 `lens_target_unknown`.'
          schema:
            type: string
        - name: connection_id
          in: query
          required: false
          description: The system-of-record connection to read. Optional when the organization has exactly one qualifying connection; required when it has several (otherwise 400 `book_connection_ambiguous`).
          schema:
            type: string
        - name: client_key
          in: query
          required: false
          description: One record by client key. Send this or `policy_key`, not both.
          schema:
            type: string
        - name: policy_key
          in: query
          required: false
          description: One record by policy key.
          schema:
            type: string
      responses:
        '200':
          description: The current rows.
          headers:
            X-Nexio-Engine-Build:
              description: The engine commit that computed this response, when the build is stamped. Key caches on it.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecordsNoteListResponse'
        '400':
          description: '`invalid_request` (both `client_key` and `policy_key` sent, a malformed key, or a `connection_id` that is not a UUID), `book_connection_ambiguous`, `lens_target_unknown`, or `action_list_too_large`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`scoped_key_required` (a sandbox or test organization key), `insufficient_capability` (a scoped key without `actions:read`), an identity refusal (`identity_unmapped`, including a missing `X-Nexio-Acting-Principal`, `identity_needs_review`, `identity_suspended`, `identity_stale`, `scope_unavailable`, `assertion_invalid`, `assertion_stale`, `service_identity_unknown`, `lens_caller_unattributed`), or `dataset_denied` from the acting person''s policy.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`not_found`: the named connection does not exist in this organization.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`book_unavailable`: the read cannot be served now. `details.reason`, when present, is one of `executor_timeout`, `budget_exhausted`, `pin_unavailable`, `warehouse_unavailable`, `scope_datasources_missing`, `result_too_large`; no reason means no qualifying connection exists yet.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '499':
          $ref: '#/components/responses/ClientClosedRequest'
        '500':
          description: '`internal_error`, or `audit_write_failed` when a lensed read could not be audited.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            `book_unavailable` with `details.reason: busy` when the warehouse statement queue is full, or `resolve_retry_exhausted`. Retry the same request; it is idempotent.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Sent with `book_unavailable` reason `busy`.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /api/v1/records/tasks:
    get:
      operationId: listRecordsTasks
      summary: List ledger tasks
      description: |-
        The current tasks projected from the action ledger for the connection, oldest first. Send `client_key` or `policy_key` to read one system-of-record record. No paging: when more than 10,000 rows on the connection match the filter (counted before rows outside your scope are removed), the read answers 400 `action_list_too_large`. Deleted tasks and rows outside your scope are not returned. Rows about a `policy` record identify it by `ams360_datasource` and `policy_id`, the source's tenant key and record id.

        Credential: the organization's live API key, or a scoped key with `actions:read`.
      tags:
        - Records
      security:
        - BearerAuth: []
      parameters:
        - name: X-Nexio-Acting-Principal
          in: header
          required: true
          description: The person the request is for, as your identity provider's stable user id. Required on this route; without it the request answers 403 `identity_unmapped`. Narrows what the key reaches.
          schema:
            type: string
        - name: X-Nexio-Acting-Email
          in: header
          required: false
          description: The acting person's verified sign-in email. The seat is derived from it.
          schema:
            type: string
        - name: X-Nexio-Records-Assertion
          in: header
          required: false
          description: 'Signed assertion `v1.<unix seconds>.<hex HMAC-SHA256>` over `acting_principal|acting_email|reserved|timestamp` (each part trimmed, the email lowercased). The third field is reserved: send an empty string. The timestamp must be within 5 minutes of the server clock, either way. Checked once Nexio enables assertion verification for your organization, when it provisions the signing secret; a missing or wrong assertion then answers 403 `assertion_invalid` and one outside the window 403 `assertion_stale`.'
          schema:
            type: string
        - name: X-Nexio-Records-Lens
          in: header
          required: false
          description: '`principal:<id>`: a read-only view of another person''s data, honored only for a caller whose own scope is All or Platform and ignored for anyone else. For such a caller, a value that is not `principal:<id>`, or names nobody, answers 400 `lens_target_unknown`.'
          schema:
            type: string
        - name: connection_id
          in: query
          required: false
          description: The system-of-record connection to read. Optional when the organization has exactly one qualifying connection; required when it has several (otherwise 400 `book_connection_ambiguous`).
          schema:
            type: string
        - name: client_key
          in: query
          required: false
          description: One record by client key. Send this or `policy_key`, not both.
          schema:
            type: string
        - name: policy_key
          in: query
          required: false
          description: One record by policy key.
          schema:
            type: string
      responses:
        '200':
          description: The current rows.
          headers:
            X-Nexio-Engine-Build:
              description: The engine commit that computed this response, when the build is stamped. Key caches on it.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecordsTaskListResponse'
        '400':
          description: '`invalid_request` (both `client_key` and `policy_key` sent, a malformed key, or a `connection_id` that is not a UUID), `book_connection_ambiguous`, `lens_target_unknown`, or `action_list_too_large`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`scoped_key_required` (a sandbox or test organization key), `insufficient_capability` (a scoped key without `actions:read`), an identity refusal (`identity_unmapped`, including a missing `X-Nexio-Acting-Principal`, `identity_needs_review`, `identity_suspended`, `identity_stale`, `scope_unavailable`, `assertion_invalid`, `assertion_stale`, `service_identity_unknown`, `lens_caller_unattributed`), or `surface_denied` or `dataset_denied` from the acting person''s policy.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`not_found`: the named connection does not exist in this organization.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`book_unavailable`: the read cannot be served now. `details.reason`, when present, is one of `executor_timeout`, `budget_exhausted`, `pin_unavailable`, `warehouse_unavailable`, `scope_datasources_missing`, `result_too_large`; no reason means no qualifying connection exists yet.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '499':
          $ref: '#/components/responses/ClientClosedRequest'
        '500':
          description: '`internal_error`, or `audit_write_failed` when a lensed read could not be audited.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            `book_unavailable` with `details.reason: busy` when the warehouse statement queue is full, or `resolve_retry_exhausted`. Retry the same request; it is idempotent.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          headers:
            Retry-After:
              description: Seconds to wait before retrying. Sent with `book_unavailable` reason `busy`.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /api/v1/records/status:
    get:
      operationId: getRecordsStatus
      summary: Get status
      description: |-
        Preflight for Records: whether a qualifying connection exists, how the acting person resolves, and which reads are available. Unlike every other read, when a qualifying connection exists an identity or scope refusal (`identity_unmapped`, `identity_needs_review`, `identity_suspended`, `identity_stale`, `scope_unavailable`) is answered as 200 with `scope.refused` set. A seat that is held for review (no single employee record matches) currently answers 500 `internal_error`. Assertion and lens refusals keep their HTTP status. With no qualifying connection (`has_published: false`), the caller is admitted as on other governed routes: a registered service identity (`X-Nexio-Service-Identity`, or the API key itself when registered as one) is served, and otherwise a request without an acting principal gets 403 `identity_unmapped` under the shadow and lit postures, and an identity refusal answers with its HTTP status. `X-Nexio-Service-Identity` is not read when a qualifying connection exists.

        Optional diagnostics headers `X-Nexio-Consumer`, `X-Nexio-Consumer-Build` and `X-Nexio-Consumer-Vocabulary` are recorded and never used for authorization.

        Credential: the organization's live API key, or a scoped key with `records:read`. A sandbox or test organization key gets 403 `scoped_key_required`.
      tags:
        - Records
      security:
        - BearerAuth: []
      parameters:
        - name: X-Nexio-Acting-Principal
          in: header
          required: false
          description: The person the request is for, as your identity provider's stable user id. Narrows what the key reaches; never widens it. Send it on every request; under the shadow and lit postures a read without it answers 403 `identity_unmapped`, or 200 with `scope.refused` when a qualifying connection exists.
          schema:
            type: string
        - name: X-Nexio-Acting-Email
          in: header
          required: false
          description: The acting person's verified sign-in email. The seat is derived from it.
          schema:
            type: string
        - name: X-Nexio-Records-Assertion
          in: header
          required: false
          description: 'Signed assertion `v1.<unix seconds>.<hex HMAC-SHA256>` over `acting_principal|acting_email|reserved|timestamp` (each part trimmed, the email lowercased). The third field is reserved: send an empty string. The timestamp must be within 5 minutes of the server clock, either way. Checked once Nexio enables assertion verification for your organization, when it provisions the signing secret; a missing or wrong assertion then answers 403 `assertion_invalid` and one outside the window 403 `assertion_stale`.'
          schema:
            type: string
        - name: X-Nexio-Records-Lens
          in: header
          required: false
          description: '`principal:<id>`: a read-only view of another person''s data, honored only for a caller whose own scope is All or Platform and ignored for anyone else. For such a caller, a value that is not `principal:<id>`, or names nobody, answers 400 `lens_target_unknown`.'
          schema:
            type: string
        - name: connection_id
          in: query
          required: false
          description: The system-of-record connection to read. Optional when the organization has exactly one qualifying connection; required when it has several (otherwise 400 `book_connection_ambiguous`).
          schema:
            type: string
        - name: include_team
          in: query
          required: false
          description: 'Add the person''s team to `entitlement`. May cost one extra warehouse read. Accepts `true`, `false`, `1` or `0`; any other value is 400 `invalid_request`. Defaults to false.'
          schema:
            type: boolean
        - name: include_related_books
          in: query
          required: false
          description: 'Include `entitlement.related_books`: counts of the person''s own records and of each other owner''s records the person services or shares. Accepts `true`, `false`, `1` or `0`; any other value, including an empty one, is 400 `invalid_request`. Defaults to true.'
          schema:
            type: boolean
        - name: X-Nexio-Consumer
          in: header
          required: false
          description: Optional client application name, for diagnostics.
          schema:
            type: string
        - name: X-Nexio-Consumer-Build
          in: header
          required: false
          description: Optional client build, for diagnostics.
          schema:
            type: string
        - name: X-Nexio-Consumer-Vocabulary
          in: header
          required: false
          description: Optional access vocabulary hash the client was built against, for diagnostics.
          schema:
            type: string
        - $ref: '#/components/parameters/ServiceIdentityHeader'
      responses:
        '200':
          description: Connection and authority status.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecordsStatusResponse'
          headers:
            Server-Timing:
              description: 'Per-read timing: serving resolution (`pin`, no warehouse call on a current read), each warehouse statement by name, compose, and total.'
              schema:
                type: string
            X-Nexio-Engine-Build:
              description: The engine commit that computed this response, when the build is stamped. Key caches on it.
              schema:
                type: string
        '400':
          description: '`invalid_request` (`include_team` or `include_related_books` is not a boolean, or `connection_id` is not a UUID), `book_connection_ambiguous` when several connections qualify and no `connection_id` was sent, or `lens_target_unknown` (a lens that is not `principal:<id>` or names nobody).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: 'Refused. `scoped_key_required` (a sandbox or test organization key), `insufficient_capability` (a scoped key without `records:read`), `assertion_invalid` or `assertion_stale`, or `lens_caller_unattributed`. With no qualifying connection only: an identity refusal (`identity_unmapped`, `identity_needs_review`, `identity_suspended`, `identity_stale`, `scope_unavailable`), `service_identity_unknown`, or `dataset_denied` from the caller''s policy.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`not_found`: `connection_id` names no qualifying connection in this organization.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`book_unavailable`: the acting person''s access could not be resolved now. `details.reason` names the cause, for example `warehouse_unavailable` or `executor_timeout`. Retry. No qualifying connection is not a 409 here; it answers 200 with `has_published: false`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '499':
          $ref: '#/components/responses/ClientClosedRequest'
        '500':
          description: '`internal_error` (including a seat held for review, which has no single matching employee record), or `audit_write_failed` when a lensed read could not be audited.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            `book_unavailable` with `details.reason: busy` when the warehouse statement queue is full (retry after `Retry-After` seconds), or `resolve_retry_exhausted` when authority resolution kept conflicting.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          description: '`request_timeout`: the 30 second route timeout expired.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/records/families/{plane}/{family}:
    get:
      operationId: readRecordsFamily
      summary: Read a family
      description: |-
        Reads a registered source dataset (family) on the connection by plane and name, as columns and rows. Planes and families are a registry fixed in code; every family not reserved for a dedicated route is readable here with no per-family code. `policy_key` applies only to policy-grain families and `client_key` only to client-grain families; a key on any other family answers 400 `invalid_request`. An org-grain family read by a person-scoped caller answers 200 with no rows and `family_serving.notice.code` `org_scoped_only`. Direct identifiers are never served on table or org families; on policy and client families they are null unless the read is narrowed by `policy_key` or `client_key`. A key outside the caller's scope returns no rows. An unknown plane or family, or a family served only by a dedicated route, answers 404 `not_found`.

        Credential: the organization's live API key, or a scoped key with `records:read`. A sandbox or test organization key gets 403 `scoped_key_required`.
      tags:
        - Records
      security:
        - BearerAuth: []
      parameters:
        - name: plane
          in: path
          required: true
          description: Plane name, for example `book` or `rawams_claims`.
          schema:
            type: string
        - name: family
          in: path
          required: true
          description: Family name, for example `policies` or `claim`.
          schema:
            type: string
        - name: X-Nexio-Acting-Principal
          in: header
          required: false
          description: The person the request is for, as your identity provider's stable user id. Narrows what the key reaches; never widens it. Send it on every request; under the shadow and lit postures a read without it answers 403 `identity_unmapped`.
          schema:
            type: string
        - name: X-Nexio-Acting-Email
          in: header
          required: false
          description: The acting person's verified sign-in email. The seat is derived from it.
          schema:
            type: string
        - name: X-Nexio-Records-Assertion
          in: header
          required: false
          description: 'Signed assertion `v1.<unix seconds>.<hex HMAC-SHA256>` over `acting_principal|acting_email|reserved|timestamp` (each part trimmed, the email lowercased). The third field is reserved: send an empty string. The timestamp must be within 5 minutes of the server clock, either way. Checked once Nexio enables assertion verification for your organization, when it provisions the signing secret; a missing or wrong assertion then answers 403 `assertion_invalid` and one outside the window 403 `assertion_stale`.'
          schema:
            type: string
        - name: X-Nexio-Records-Lens
          in: header
          required: false
          description: '`principal:<id>`: a read-only view of another person''s data, honored only for a caller whose own scope is All or Platform and ignored for anyone else. For such a caller, a value that is not `principal:<id>`, or names nobody, answers 400 `lens_target_unknown`.'
          schema:
            type: string
        - name: connection_id
          in: query
          required: false
          description: The system-of-record connection to read. Optional when the organization has exactly one qualifying connection; required when it has several (otherwise 400 `book_connection_ambiguous`).
          schema:
            type: string
        - name: policy_key
          in: query
          required: false
          description: 'One policy key. Policy-grain families only; on any other family, or when the key does not decode, the answer is 400 `invalid_request`.'
          schema:
            type: string
        - name: client_key
          in: query
          required: false
          description: 'One client key (the system of record''s top-level record key; starts with `bk1_`). Client-grain families only; on any other family, or when the key does not decode, the answer is 400 `invalid_request`.'
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: 'Rows per page, a positive integer. Defaults to 50; values above 10,000 are clamped to 10,000.'
          schema: {type: integer, minimum: 1}
        - name: cursor
          in: query
          required: false
          description: 'The `page.next_cursor` from the previous page. It carries the filters: a cursor sent with `policy_key` or `client_key` answers 400 `cursor_filter_mismatch`, and a cursor that does not decode answers 400 `invalid_cursor`.'
          schema:
            type: string
      responses:
        '200':
          description: One page of rows.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RecordsGenericFamilyResponse'
          headers:
            Server-Timing:
              description: 'Per-read timing: serving resolution (`pin`, no warehouse call on a current read), each warehouse statement by name, compose, and total.'
              schema:
                type: string
            X-Nexio-Engine-Build:
              description: The engine commit that computed this response, when the build is stamped. Key caches on it.
              schema:
                type: string
        '400':
          description: '`invalid_request` (a `limit` that is not a positive integer, a `connection_id` that is not a UUID, a key that does not decode, or a key filter the family''s grain does not take), `invalid_cursor`, `cursor_filter_mismatch` (a cursor sent with a key filter), `book_connection_ambiguous` when several connections qualify and no `connection_id` was sent, or `lens_target_unknown` when `X-Nexio-Records-Lens` names no known person.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Refused. `scoped_key_required` (a sandbox or test organization key), `insufficient_capability` (a scoped key without the capability), an identity refusal (`identity_unmapped`, `identity_needs_review`, `identity_suspended`, `identity_stale`, `scope_unavailable`, `assertion_invalid`, `assertion_stale`, `service_identity_unknown`, `lens_caller_unattributed`), or a policy denial (`surface_denied`, `dataset_denied`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`not_found`: the named connection does not exist in this organization, or no family with this plane and name is served here.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '409':
          description: '`book_unavailable`: the read cannot be served now. `details.reason` is one of `executor_timeout`, `budget_exhausted`, `scope_datasources_missing`, `warehouse_unavailable`, `result_too_large`; no reason means no qualifying connection exists. Also `cursor_expired` (restart pagination).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '499':
          description: '`client_closed_request`: the caller closed the connection before the read finished.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '500':
          description: '`internal_error`, or `audit_write_failed` when a lensed read could not be audited.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: |
            `book_unavailable` with `details.reason: busy` when the warehouse statement queue is full (retry after `Retry-After` seconds), or `resolve_retry_exhausted` when authority resolution kept conflicting.

            Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
          headers:
            Retry-After:
              description: Seconds to wait before retrying.
              schema:
                type: integer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '504':
          description: '`request_timeout`: the 30 second route timeout expired.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /api/v1/graph:
    get:
      operationId: getDataGraph
      summary: Get the data graph
      description: |-
        The organization's data graph as this API serves it: one node per live data connection, one node per derivation with a publication schedule on a live connection, and a `feeds` edge from each connection to each derivation it has a schedule for. `findings` is always empty.

        Credential: the organization's live API key, or a scoped key with `graph:read`. A sandbox or test organization key gets 403 `scoped_key_required`. Every call assembles the whole graph, then filters it.
      tags:
        - Graph
      security:
        - BearerAuth: []
      responses:
        '200':
          description: The whole graph.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GraphResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`scoped_key_required` or `insufficient_capability`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: '`internal_error`: the graph could not be read.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /api/v1/graph/{nodeType}:
    get:
      operationId: listDataGraphNodes
      summary: List graph nodes
      description: |-
        Every node of one kind. Only `connection` and `derivation` nodes are served; other accepted kinds return an empty list.

        Credential: the organization's live API key, or a scoped key with `graph:read`. A sandbox or test organization key gets 403 `scoped_key_required`. Every call assembles the whole graph, then filters it.
      tags:
        - Graph
      security:
        - BearerAuth: []
      parameters:
        - name: nodeType
          in: path
          required: true
          description: A node kind. Only `connection` and `derivation` have nodes; the other accepted kinds return an empty list. `document` and `warehouse_table` answer 400 `unsupported_node_type`.
          schema:
            type: string
      responses:
        '200':
          description: The nodes of that kind.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GraphNodesResponse'
        '400':
          description: '`invalid_request` (unknown kind) or `unsupported_node_type` (`document` or `warehouse_table`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`scoped_key_required` or `insufficient_capability`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: '`internal_error`: the graph could not be read.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'
  /api/v1/graph/{nodeType}/{nodeKey}:
    get:
      operationId: getDataGraphNode
      summary: Get a graph node
      description: |-
        One node and every edge that touches it. `nodeKey` is the node key after `<kind>:`.

        Credential: the organization's live API key, or a scoped key with `graph:read`. A sandbox or test organization key gets 403 `scoped_key_required`. Every call assembles the whole graph, then filters it.
      tags:
        - Graph
      security:
        - BearerAuth: []
      parameters:
        - name: nodeType
          in: path
          required: true
          description: A node kind. Only `connection` and `derivation` nodes exist; a well-formed key of another accepted kind answers 404. `document` and `warehouse_table` answer 400 `unsupported_node_type`.
          schema:
            type: string
        - name: nodeKey
          in: path
          required: true
          description: The key after the kind prefix, a connection id (UUID) for `connection`, the derivation's asset key for `derivation`. A key that does not fit the kind's shape answers 400 `invalid_request`.
          schema:
            type: string
      responses:
        '200':
          description: The node and its edges.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GraphNodeResponse'
        '400':
          description: '`invalid_request` (unknown kind or malformed key) or `unsupported_node_type` (`document` or `warehouse_table`).'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: '`scoped_key_required` or `insufficient_capability`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: '`internal_error`: the graph could not be read.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: '`not_found`: no node has that key.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          $ref: '#/components/responses/AuthUnavailable'
        '504':
          $ref: '#/components/responses/RequestTimeout'

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Send the key as `Authorization: Bearer <key>`. Two kinds of key exist.

        Organization keys are issued in the portal (Settings, then API keys),
        each bound to one environment, shaped `nx_<environment slug>_<64 hex>`. They carry
        no capabilities and pass every capability check, with one exception:
        routes under /api/v1/records, /api/v1/engines/{id}/opportunities,
        /api/v1/graph and /api/v1/catalog/documents accept an
        organization key only when its environment is `live`, and refuse any
        other with 403 `scoped_key_required`. Revocation takes effect within 60
        seconds.

        Scoped keys are issued by Nexio on request, shaped
        `nxsk_v1_<24 hex key id>_<43 character secret>`. Each is bound to one
        org, one environment, a set of engines and a set of capabilities. A
        malformed, unknown or revoked `nxsk_` key fails with 401 and is never
        retried as an organization key. Revocation takes effect on the next
        request. A scoped key without a route's capability gets 403
        `insufficient_capability`; a scoped key not bound to the engine gets 403
        `engine_binding_forbidden`.

        Key-grantable capabilities: `engines:read`, `runs:write`, `runs:read`,
        `runs:defensibility:read`, `runs:test`, `catalog:read`,
        `catalog:documents:read`, `webhooks:manage`, `conversations:use`,
        `conversations:export`, `records:read`, `records:opportunities:run`,
        `actions:write`, `actions:read`, `graph:read`, `records:analyze`.

        Routes that accept organization keys only (every scoped key gets 403
        `insufficient_capability`): environment management, engine create,
        update, configuration and publish, and conversation instance
        authoring. Each operation description names the capability a scoped
        key needs.

  parameters:
    EngineSlug:
      name: engine_slug
      in: path
      required: true
      description: Engine identifier slug (e.g. `default`).
      schema:
        type: string

    InstanceSlug:
      name: instance_slug
      in: path
      required: true
      description: Conversation instance identifier slug (e.g. `platform-assistant`).
      schema:
        type: string

    EndpointID:
      name: endpointID
      in: path
      required: true
      description: Webhook endpoint UUID.
      schema:
        type: string
        format: uuid

    DeliveryID:
      name: deliveryID
      in: path
      required: true
      description: Webhook delivery UUID.
      schema:
        type: string
        format: uuid

    ActingEmailHeader:
      name: X-Nexio-Acting-Email
      in: header
      required: false
      description: The acting person's verified sign-in email, sent with `X-Nexio-Acting-Principal`. The seat is derived from it.
      schema:
        type: string

    ServiceIdentityHeader:
      name: X-Nexio-Service-Identity
      in: header
      required: false
      description: A registered service identity, for a machine caller. When sent, it is used instead of `X-Nexio-Acting-Principal`. A value that names no active service identity answers 403 `service_identity_unknown`.
      schema:
        type: string


  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: unauthorized
            message: Missing or invalid API key

    Forbidden:
      description: The key may not call this operation. A scoped key lacks the capability (`insufficient_capability`) or is not bound to the engine (`engine_binding_forbidden`), or an organization key from a non-live environment called a live-only family (`scoped_key_required`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            insufficientCapability:
              value:
                code: insufficient_capability
                message: API key does not have permission for this action
            engineBindingForbidden:
              value:
                code: engine_binding_forbidden
                message: API key is not bound to this engine
            scopedKeyRequired:
              value:
                code: scoped_key_required
                message: This route requires a scoped API key with an explicit capability grant; legacy org keys are not accepted here
    OrganizationKeyRequired:
      description: This route accepts organization keys only. Every scoped key gets this answer, whatever capabilities it holds.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: insufficient_capability
            message: API key does not have permission for this action
    AuthUnavailable:
      description: |
        `auth_unavailable`: API key authentication was briefly unavailable
        before the route ran. Transient; retry with backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: auth_unavailable
            message: API key authentication is temporarily unavailable

    AttachmentsUnavailable:
      description: |
        `attachments_unavailable`: file attachments are not available in this
        environment because the platform has no attachment storage wired. That
        cause is not retryable by the caller; the instance's operator has to
        enable attachment storage.

        Also `auth_unavailable`: API key authentication was briefly unavailable before the route ran. That cause is transient; retry it with backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: attachments_unavailable
            message: File attachments are not available in this environment.

    ClientClosedRequest:
      description: |
        `client_closed_request`: the caller closed the connection before the
        read finished. Nothing reads this body; it exists so the abandoned
        read is recorded as 499 rather than as a server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: client_closed_request
            message: The client closed the request before the book read finished

    InternalError:
      description: An internal failure (`internal_error` or an operation-specific code). Include the `X-Request-Id` response header value when you contact support.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: internal_error
            message: Internal server error

    RequestTimeout:
      description: '`request_timeout`: the 30 second route timeout expired before the handler answered.'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: request_timeout
            message: Request timed out

    RateLimited:
      description: Rate limit exceeded. Retry after the window resets.
      headers:
        Retry-After:
          $ref: '#/components/headers/RetryAfter'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: rate_limited
            message: Rate limit exceeded

    EngineNotFound:
      description: Engine not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: engine_not_found
            message: Engine not found

    WebhookNotFound:
      description: Webhook endpoint not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: webhook_not_found
            message: Webhook endpoint not found

    DeliveryNotFound:
      description: Delivery not found in the authenticated endpoint and environment scope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            code: delivery_not_found
            message: Delivery not found

  schemas:
    RecordsServing:
      type: object
      additionalProperties: false
      required: [overlay_rev, as_of]
      properties:
        batch_set_id:
          type: string
          description: Not sent on current reads; present only on reads served from a stored copy.
        binding_id:
          type: string
          description: The connection the read was served from.
        overlay_rev:
          type: integer
          minimum: 0
          description: Always 0 today.
        as_of:
          type: string
          format: date-time
          description: The request's read time. Every Records read is a current read, queried live at request time and not held to a fixed warehouse instant. An empty page may carry either the request time or the zero time `0001-01-01T00:00:00Z`. The action, note, task, workflow-state and edit-intent routes carry the zero time, even when their account or policy scope check queries the warehouse.
        read_pin:
          type: string
          format: date-time
          description: Not sent on Records reads, because no read is held to a fixed warehouse instant.
        source:
          type: object
          additionalProperties: false
          required: [mode, fetched_at]
          properties:
            mode: {type: string, description: 'query_first: read live from the warehouse at request time.'}
            current: {type: boolean, description: '`true`: the read queried the current data at request time. Source changes can show between separate statements in one response and between pages.'}
            freshness:
              type: string
              enum: [current, pinned]
              description: Not sent on Records reads.
            fetched_at: {type: string, format: date-time, description: 'The request''s read time, the same instant as `as_of`.'}
            stale_since: {type: string, format: date-time, description: Not sent on Records reads.}
        lens:
          type: object
          additionalProperties: false
          required: [target, display_name, scope_kind]
          properties:
            target: {type: string}
            display_name: {type: string}
            scope_kind: {type: string}

    ConversationAttachment:
      type: object
      description: |
        One file attached to a conversation. There is no text field: the
        platform does not parse the document, it hands the bytes to the model.
      required: [id, filename, media_type, size_bytes, status, delivery, created_at]
      properties:
        id:
          type: string
          format: uuid
        filename:
          type: string
        media_type:
          type: string
        size_bytes:
          type: integer
          format: int64
        status:
          type: string
          enum: [storing, unwrapping, ready, rejected, failed]
          description: |
            `ready` can ride a turn. A multipart upload answers `ready`. A
            reserved upload is `storing` until finalize; a folder container
            is `unwrapping` until every member is finalized. A finalize that
            finds no bytes leaves the record `storing`. A finalize that refuses
            the file for any other reason marks it `rejected` and removes it at
            once, so it no longer lists or reads. `failed` is reserved and not
            currently set. Only `ready` records can ride a turn or be served.
        delivery:
          type: string
          enum: [file, image, unwrap]
          description: |
            How the file reaches the model. `file` and `image` are provider
            inputs. `unwrap` is a container, which never reaches the model
            itself; a turn naming it carries the files inside it.

            A folder container also has delivery `unwrap`.
        notice:
          type: string
          description: |
            How this format is read, when there is a limit worth knowing. On a
            zip or email container it says how many files were read and what
            was left out; on a folder container it says how many files the
            folder holds. A spreadsheet is read to the first 1,000 rows per sheet; text is
            read out of Office files while images and charts inside them are
            not. Absent when the file is read whole. Show it: an answer drawn
            from part of a schedule is not an answer about the schedule.
        error:
          type: string
          description: Why the record was rejected or failed. Absent otherwise.
        container_kind:
          type: string
          enum: [zip, eml, folder]
          description: |
            What a container is. `zip` and `eml` are opened by the platform;
            `folder` is opened by the client and its files uploaded one by
            one. Set on a folder, and on a zip or email stored through
            finalize. Absent on a zip or email stored by a multipart upload,
            on a plain file, and on a file inside a container.
        parent_attachment_id:
          type: string
          format: uuid
          description: The container this file was found inside, when it was.
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
          description: |
            When access to the attachment ends. From this moment it is left
            out of lists, answers `404` when read or downloaded, and cannot be
            named on a turn. A later retention sweep deletes the record and
            removes the bytes. Absent when the attachment follows its
            conversation's own retention.
    ConversationInstanceConfigWrite:
      type: object
      additionalProperties: false
      description: |
        Full configured-assistant policy. Every configured assistant uses
        `gpt-6-sol`; the config has no model selector. The retired
        `model_policy.default_model` and `model_policy.deep_model` fields are
        rejected with `invalid_instance_config` on create, update, and
        publish. Other sections define prompts, tools, data sources, access,
        limits, guardrails, evals, and retention.

        A config document is the complete instance config. The twelve
        sections below are required and must not be null; `attachments` is
        optional (absent means the instance takes no attachments). Unknown
        fields at any level are rejected with `invalid_instance_config`, with
        two exceptions kept for old configs: a top-level `data_scope` object
        is read as a one-entry `data_sources` list, and
        `retention.thread_days` is read as `retention.conversation_days`.
        When both spellings are sent, the current one wins, so either
        `data_sources` or `data_scope` must be present.
        See /conversations/configuration for what each field does.
      required: [model_policy, system_prompt, engines, tools, components, access, limits, guardrails, evals, retention, annotations]
      anyOf:
        - required: [data_sources]
        - required: [data_scope]
      properties:
        data_scope:
          type: object
          deprecated: true
          description: Legacy spelling of one `data_sources` entry. Read as a one-entry `data_sources` list when `data_sources` is absent, and ignored when it is present.
        model_policy:
          type: object
          additionalProperties: false
          required: [max_tokens]
          properties:
            max_tokens:
              type: integer
              minimum: 0
              maximum: 8192
              description: Maximum output tokens for each model call in a turn (a turn with tool rounds makes several calls; `limits.max_turn_output_tokens` bounds the whole turn). 0 selects the platform default of 1024.
        system_prompt:
          type: string
          description: The instance's instructions.
        engines:
          description: The engine allowlist. The string `*` allows every engine the calling principal may see; an array lists engine slugs or IDs. An empty array allows none. Array entries must be non-blank and unique.
          oneOf:
            - type: string
              enum: ['*']
            - type: array
              uniqueItems: true
              items: {type: string, pattern: '\S'}
        tools:
          type: object
          additionalProperties: false
          required: [platform_packs, client_tools]
          properties:
            platform_packs:
              type: array
              uniqueItems: true
              description: Declaring `platform.write` also requires `platform.read`.
              items: {type: string, enum: [platform.read, platform.write]}
            client_tools:
              type: array
              items:
                type: object
                additionalProperties: false
                required: [name, effect]
                properties:
                  name:
                    type: string
                    maxLength: 64
                    pattern: '^[a-zA-Z0-9_-]+$'
                    description: At most 64 characters, unique within the instance, and must not contain `__`.
                  description: {type: string}
                  input_schema:
                    type: [object, 'null']
                    description: JSON Schema for the tool's input. It must be an object that compiles as a JSON Schema. Absent or null means no input schema.
                  effect:
                    type: string
                    enum: [read, write, outbound, destructive]
                  requires_confirmation:
                    type: boolean
                    description: Always `true` in effect for `write`, `outbound` and `destructive` tools; sending `false` for one of them is rejected.
        components:
          type: array
          description: No two entries may share the same `component` and `version`.
          items:
            type: object
            additionalProperties: false
            required: [component, version]
            properties:
              component: {type: string, pattern: '\S', description: Must not be blank.}
              version: {type: integer, minimum: 1}
              props_schema:
                type: [object, 'null']
                description: JSON Schema for the component's props. Absent or null means no props schema.
        data_sources:
          type: array
          minItems: 1
          description: |
            At least one grounding surface. Each entry declares exactly one
            form: an inline `sources` list (at least one key, with optional
            `notes`), or a `ref` to a stored data scope with no `sources` and
            no `notes` (blank `notes` is accepted).
          items:
            type: object
            additionalProperties: false
            properties:
              sources:
                type: array
                description: 'Data-access keys, compared after trimming. Each must be one of `engines`, `runs`, `run_payloads`, `metrics`, `catalog`, `connections`, `webhooks`, `feedback`, `team`.'
                items: {type: string, pattern: '\S'}
              notes: {type: string}
              ref: {type: string, pattern: '\S'}
            oneOf:
              - required: [ref]
                properties:
                  sources: {maxItems: 0}
                  notes: {type: string, pattern: '^\s*$', description: Absent or blank. A reference entry with notes answers 400.}
              - required: [sources]
                properties:
                  sources: {minItems: 1}
                not:
                  required: [ref]
        access:
          type: object
          additionalProperties: false
          required: [personas]
          properties:
            personas:
              type: array
              minItems: 1
              description: 'At least one persona. `"*"` allows any persona.'
              items: {type: string, pattern: '\S'}
        limits:
          type: object
          additionalProperties: false
          required: [max_tool_rounds, max_turn_output_tokens, max_history_messages]
          properties:
            max_tool_rounds: {type: integer, minimum: 1, maximum: 50}
            max_turn_output_tokens: {type: integer, minimum: 1, maximum: 128000}
            max_history_messages: {type: integer, minimum: 1, maximum: 100}
        guardrails:
          type: object
          additionalProperties: false
          required: [refusal_domains, escalation_rules, output_checks]
          properties:
            refusal_domains:
              type: array
              items: {$ref: '#/components/schemas/ConversationGuardrailRule'}
            escalation_rules:
              type: array
              items: {$ref: '#/components/schemas/ConversationGuardrailRule'}
            output_checks:
              type: array
              items: {$ref: '#/components/schemas/ConversationGuardrailRule'}
        evals:
          type: object
          additionalProperties: false
          required: [on_regression]
          properties:
            on_regression:
              type: string
              enum: [block, warn]
        retention:
          type: object
          additionalProperties: false
          description: Needs `conversation_days` or its legacy spelling `thread_days`.
          anyOf:
            - required: [conversation_days]
            - required: [thread_days]
          properties:
            conversation_days:
              type: [integer, 'null']
              minimum: 1
              description: A positive number of days. Null selects the platform default.
            thread_days:
              type: [integer, 'null']
              minimum: 1
              deprecated: true
              description: Legacy spelling of `conversation_days`, read as it when `conversation_days` is absent and ignored when it is present.
        annotations:
          type: object
          additionalProperties: false
          required: [enabled]
          properties:
            enabled: {type: boolean, description: 'Recorded with the config. The annotation routes do not check it today.'}
        attachments:
          type: [object, 'null']
          additionalProperties: false
          description: Optional upload policy. Absent or null means the instance takes no attachments.
          properties:
            enabled: {type: [boolean, 'null'], description: Null is read as `false`.}
            max_files_per_message:
              type: [integer, 'null']
              minimum: 0
              maximum: 10
              description: 0 or null selects the default of 5.
            max_bytes_per_file:
              type: [integer, 'null']
              format: int64
              minimum: 0
              maximum: 39321600
              description: 0 or null selects the platform ceiling of 39,321,600 bytes.
            accepted_media_types:
              type: [array, 'null']
              description: 'Narrows the platform''s accepted types. Empty or null means every type the platform accepts. Entries must be non-empty, unique and on the platform allowlist. A container type (`message/rfc822`, `application/zip`) needs `unwrap_archives: true`.'
              uniqueItems: true
              items: {type: string, minLength: 1}
            unwrap_archives: {type: [boolean, 'null'], description: Null is read as `false`.}
            retention_days:
              type: [integer, 'null']
              minimum: 1
              description: A positive number of days. Null follows the conversation's own retention.

    ConversationGuardrailRule:
      type: object
      additionalProperties: false
      description: 'One guardrail rule. A `refusal_domains` rule needs `description`; an `escalation_rules` rule needs `condition` and `route`; an `output_checks` rule needs `check`. A field that belongs to another family is rejected. `scenarios` lists at least one eval scenario ID that tests the rule. Rule ids are non-blank and unique across all three families.'
      required: [id, scenarios]
      properties:
        id: {type: string, pattern: '\S'}
        description: {type: string}
        condition: {type: string}
        check: {type: string, description: 'A regular expression, matched case-insensitively with `.` matching newlines. It is not compiled when the config is saved; a pattern that does not compile fails every turn with 500 `guardrail_config_invalid`.'}
        route: {type: string}
        scenarios:
          type: array
          minItems: 1
          items: {type: string, pattern: '\S'}

    ConversationInstanceConfig:
      type: object
      additionalProperties: true
      description: |
        A live configured-assistant draft returned by the API. Current drafts
        use `gpt-6-sol` and expose no model selector. A draft saved by an
        older deployment can still contain retired model keys until its next
        update; publishing it is rejected with `invalid_instance_config`.
      properties:
        model_policy:
          type: object
          additionalProperties: true
          properties:
            max_tokens:
              type: integer
              minimum: 0
              maximum: 8192
              description: Maximum output tokens for each model call in a turn (a turn with tool rounds makes several calls; `limits.max_turn_output_tokens` bounds the whole turn). 0 selects the platform default of 1024.

    ConversationInstance:
      type: object
      description: |
        A Conversation instance: the configured object of the Conversations
        family, an orchestrator over engines. Its versioned config declares
        the engine access allowlist and data sources alongside model policy,
        tools, guardrails, evals, and retention. `managed_by` records who
        governs the instance (`org` or `platform`); `follows_canonical` marks
        a platform-managed follower whose effective config resolves from the platform-managed instance it follows (followers expose no local config).
      required: [id, slug, label, description, status, group_key, managed_by, follows_canonical, created_at, updated_at]
      properties:
        id: {type: string, format: uuid}
        slug: {type: string}
        label: {type: string}
        description: {type: string}
        status: {type: string, enum: [active, archived]}
        group_key:
          type: string
          description: |
            Key of the instance's semantic group within the organization, or
            an empty string when ungrouped. Same semantics as the engine
            `group_key`.
        managed_by: {type: string, enum: [org, platform]}
        follows_canonical: {type: boolean}
        config:
          $ref: '#/components/schemas/ConversationInstanceConfig'
          description: The live draft config; omitted on listings and for followers.
        config_hash:
          type: string
          description: Content hash of the live draft config. Omitted on listings and for followers, like `config`.
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    ConversationInstanceVersion:
      type: object
      description: One immutable release of an instance's config.
      required: [version, config_hash, changelog, released_at]
      properties:
        version: {type: integer, minimum: 1}
        config_hash: {type: string}
        changelog: {type: string}
        created_by: {type: string}
        released_at: {type: string, format: date-time}

    Conversation:
      type: object
      description: A conversation on a Conversation instance.
      required: [id, instance_id, end_user, title, status, created_at, updated_at]
      properties:
        id: {type: string, format: uuid}
        instance_id: {type: string, format: uuid}
        end_user: {type: string}
        title:
          type: [string, 'null']
          description: Conversation title; null until set.
        scope:
          type: [object, 'null']
          additionalProperties: true
          description: |
            Opaque consumer context attached at creation or replaced by an
            update. The key is omitted
            when the create request left `scope` out. A create that sent
            `"scope": null` stores that null and returns `"scope": null`.
            Treat both the same way: no context.
        status: {type: string, enum: [active, archived]}
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    ConversationBranch:
      type: object
      description: |
        Sibling-version metadata on a message that has more than one version.
        Present only at a fork: a message with a single version omits the
        field entirely, so a client that never reads it sees the transcript it
        always saw.
      required: [index, count, siblings]
      properties:
        index:
          type: integer
          description: 1-based position of this version among its siblings, in creation order.
        count:
          type: integer
          description: How many versions exist at this point. Always greater than 1.
        siblings:
          type: array
          description: |
            Every version's id at this point, in creation order. The first
            element is `index` 1. Pass one of these to the branch route to
            serve that version.
          items: {type: string, format: uuid}

    ConversationDetail:
      type: object
      description: |
        A conversation plus the transcript window for the branch it currently
        serves. Returned by the conversation GET and by the branch route.
      required: [conversation, messages, truncated]
      properties:
        conversation:
          $ref: '#/components/schemas/Conversation'
        messages:
          type: array
          items:
            type: object
            required: [id, role, content, turn_id, config_version_hash, created_at, parent_message_id]
            properties:
              id: {type: string, format: uuid}
              role: {type: string, enum: [user, assistant]}
              turn_id: {type: string, format: uuid}
              config_version_hash: {type: string}
              parent_message_id:
                type: [string, 'null']
                format: uuid
                description: |
                  The message this one follows on its branch; null when it
                  starts the conversation.
              branch:
                $ref: '#/components/schemas/ConversationBranch'
              content:
                type: array
                items:
                  type: object
                  additionalProperties: true
              created_at: {type: string, format: date-time}
        truncated:
          type: boolean
          description: |
            True when the branch the conversation serves has more messages
            than this response's 500-message window, or the conversation
            holds more than 5000 messages in all; the returned messages are
            the most recent ones on that branch.

    ConversationAnnotation:
      type: object
      description: Explicit human signal recorded against one conversation turn.
      required: [triage, id, conversation_id, instance_id, turn_id, rating, source, created_at, updated_at]
      properties:
        triage:
          type: object
          additionalProperties: true
          description: |
            How the platform team is handling this feedback: `status`,
            `owner`, `reply` and `revision`. Read-only and always present. A
            new annotation reads `{"status": "new", "owner": "", "reply": "",
            "revision": 0}`.
          properties:
            status: {type: string}
            owner: {type: string}
            reply: {type: string}
            revision: {type: integer}
        id: {type: string, format: uuid}
        conversation_id: {type: string, format: uuid}
        instance_id: {type: string, format: uuid}
        turn_id: {type: string, format: uuid}
        end_user:
          type: string
          description: The conversation's end user; omitted when unset.
        target:
          type: object
          additionalProperties: true
          description: Optional JSON sub-target inside the turn; omitted when unset.
        rating: {type: string, enum: [good, bad, neutral]}
        comment:
          type: string
          description: Optional free-text comment; omitted when unset.
        reason:
          type: string
          description: Optional structured reason token; omitted when unset.
        submitter_user_id:
          type: string
          description: Portal (WorkOS) submitter; omitted for API submissions.
        submitter_id:
          type: string
          description: Consumer-side submitter identifier; omitted when unset.
        feedback_key:
          type: string
          description: Replay-safe logical signal key; omitted when unset.
        source:
          type: string
          description: Capture surface, e.g. `api` or `portal`.
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    ConversationEvalScript:
      type: array
      minItems: 1
      maxItems: 8
      description: A scripted multi-turn conversation.
      items:
        type: object
        additionalProperties: false
        required: [message]
        properties:
          message:
            type: string
            description: The user message that opens the turn.
          tool_results:
            type: object
            additionalProperties:
              type: object
              additionalProperties: false
              properties:
                content:
                  description: The result content the model sees, replayed verbatim. Absent content is sent to the model as an empty string.
                is_error:
                  type: boolean
                  description: True when the recorded result was an error (a denial or an executed failure); replays verbatim.
            description: |
              Client tool NAME to the scripted result posted back when the
              turn hands that tool off. An unscripted handoff receives a
              scripted error result.
          confirmations:
            type: object
            additionalProperties: {type: boolean}
            description: |
              Confirm-gated client tool NAME to the scripted approval
              decision. Unscripted gates are denied.

    ConversationEvalRubric:
      type: object
      additionalProperties: false
      description: |
        Expected behavior for a scenario. Every non-judge field is a
        deterministic check decided in code; `judge` dimensions are scored by
        the model judge, recorded per scenario, and can never fail a scenario.
      properties:
        must_refuse:
          type: array
          items: {type: string}
          description: Guardrail rule ids that must fire with a refused decision.
        must_escalate:
          type: array
          items: {type: string}
          description: Guardrail rule ids that must fire with an escalated decision.
        must_call_tools:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [name]
            properties:
              name: {type: string}
              input_contains:
                type: string
                description: Optional substring that must appear in at least one of the tool's call inputs.
        must_emit_components:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [component]
            properties:
              component: {type: string}
        must_cite:
          type: array
          items: {type: string}
          description: |
            Source references that must appear in the final answer (in a
            citation block's refs or the final assistant prose).
        must_not_refuse:
          type: boolean
          description: |
            True asserts the conversation ended in a delivered answer: no
            refusal fired anywhere, and the final turn did not stop at
            `refusal`, `escalated`, `output_check_triggered`, or
            `max_output_tokens`.
        final_must_not_contain:
          type: array
          items: {type: string}
          description: Phrases that must not appear in the final assistant prose, compared case-insensitively.
        judge:
          type: array
          items:
            type: object
            additionalProperties: false
            required: [name, criteria]
            properties:
              name: {type: string}
              criteria: {type: string}

    ConversationEvalScenario:
      type: object
      description: One stored eval scenario.
      required: [id, name, script, rubric, origin, suite, created_at, updated_at]
      properties:
        id: {type: string, format: uuid}
        name: {type: string}
        script:
          $ref: '#/components/schemas/ConversationEvalScript'
        rubric:
          $ref: '#/components/schemas/ConversationEvalRubric'
        origin:
          type: string
          enum: [authored, promoted_from_annotation]
        suite:
          type: string
          enum: [gate, workflows, adversarial, smoke]
          description: |
            The suite the scenario belongs to. `gate` scenarios run on every
            publish; the others run only when a run of that suite is
            requested.
        annotation_id:
          type: string
          format: uuid
          description: The source annotation for promoted scenarios; omitted for authored ones.
        created_at: {type: string, format: date-time}
        updated_at: {type: string, format: date-time}

    ConversationEvalRunDetail:
      type: object
      description: A released version's recorded eval run with per-scenario verdicts and the diff vs the prior version's run.
      required: [version, config_version_hash, run, results]
      properties:
        version:
          type: string
          description: The released version as an integer string, for example `"4"`.
        config_version_hash:
          type: string
          description: The archived config version the run measured.
        run:
          type: object
          required: [id, status, pass_count, fail_count, created_at]
          properties:
            id: {type: string, format: uuid}
            status:
              type: string
              enum: [running, passed, failed, waived, error]
              description: |
                The run's status. The latest run associated with the version
                can be the publish gate run or a later on-demand run, so an
                on-demand run still executing reads `running` and one that
                broke reads `error`. `waived` means a blocking regression was
                explicitly waived.
            pass_count: {type: integer}
            fail_count: {type: integer}
            waived_by:
              type: string
              description: Who waived a blocking regression; omitted when not waived.
            waive_reason:
              type: string
              description: Why the regression was waived; omitted when not waived.
            created_at: {type: string, format: date-time}
        results:
          type: array
          items:
            type: object
            required: [scenario_id, scenario_name, passed, scores]
            properties:
              scenario_id: {type: string, format: uuid}
              scenario_name: {type: string}
              passed: {type: boolean}
              scores:
                type: object
                additionalProperties: true
                description: Per-check verdicts plus judge scores.
        diff:
          type: object
          description: Omitted when the version has no prior version with a recorded run (baseline).
          required: [prior_version, newly_failing, new_failing, newly_passing]
          properties:
            prior_version: {type: string, description: The previous released version as an integer string.}
            newly_failing:
              type: array
              items: {type: string}
              description: Regressions; scenario names that passed on the prior version's run and fail on this one.
            new_failing:
              type: array
              items: {type: string}
              description: Scenario names with no prior result that fail on this version (they block like regressions).
            newly_passing:
              type: array
              items: {type: string}

    ConverseRequest:
      type: object
      required: [messages]
      properties:
        model:
          type: string
          description: |
            Optional model id from the platform model catalog (OpenAI or
            Anthropic). Omitted means the platform default, which is
            `gpt-6-sol` on deployments that use OpenAI. An explicit id may
            name a deprecated catalog model. An unknown id, or one whose
            provider is not configured, is `400 unknown_model`.
        system:
          type: string
          description: Optional system prompt.
        max_tokens:
          type: integer
          minimum: 0
          maximum: 8192
          description: Maximum output tokens for the turn. Omitted or 0 means 1024.
        messages:
          type: array
          minItems: 1
          maxItems: 100
          description: The replayed conversation. Content is always block form.
          items:
            $ref: '#/components/schemas/ConversationMessage'
        tools:
          type: array
          maxItems: 64
          description: |
            Caller-supplied tool definitions the model may request. Tool
            names use only letters, digits, `_`, `-` and `.`, must not contain
            "__" (reserved by the provider wire encoding of dots), and must be
            at most 64 characters with each dot counted as two. A name that
            breaks these rules, an empty name, or a missing `input_schema` is
            refused with `invalid_tool`. Dots and single underscores
            round-trip unchanged.
          items:
            $ref: '#/components/schemas/ConverseTool'
    ConversationMessage:
      type: object
      required: [role, content]
      properties:
        role:
          type: string
          enum: [user, assistant]
        content:
          type: array
          minItems: 1
          items:
            $ref: '#/components/schemas/ConverseContentBlock'
    ConverseContentBlock:
      type: object
      required: [type]
      description: |
        One content block. `text` carries prose; `tool_use` replays a prior
        model tool request; `tool_result` carries the caller's local tool
        output. Tool payloads are opaque JSON passed to the model verbatim.
      properties:
        type:
          type: string
          enum: [text, tool_use, tool_result]
        text:
          type: string
        id:
          type: string
        name:
          type: string
        input:
          type: object
        tool_use_id:
          type: string
        content: {}
        is_error:
          type: boolean
    ConverseTool:
      type: object
      required: [name, input_schema]
      properties:
        name:
          type: string
        description:
          type: string
        input_schema:
          type: object
          description: JSON Schema for the tool input, passed to the model verbatim.

    Environment:
      type: object
      required: [id, slug, kind, created_at]
      properties:
        id:
          type: string
          format: uuid
        slug:
          type: string
          pattern: '^[a-z0-9_]{1,16}$'
          description: Immutable routing identifier. `live` for the live environment.
          example: dev
        kind:
          type: string
          enum: [live, sandbox]
        name:
          type: string
          maxLength: 64
          description: Optional display label. The only field that can change after creation. Omitted when no name is set.
          example: Development
        created_at:
          type: string
          format: date-time

          description: UTC, second precision, for example 2026-09-23T15:10:42Z.
    OutcomeViewedRequest:
      type: object
      description: 'An outcome event of type `viewed`. Unknown fields are rejected with `400 invalid_request`.'
      additionalProperties: false
      required: [event_id, event_type, payload]
      properties:
        event_id:
          type: string
          format: uuid
          description: A UUID version 4 in its 36-character hyphenated form. Send it in lowercase and repeat it exactly on a retry. Idempotency key, unique per organization and environment bucket. Live keys share one bucket, and test keys, every named sandbox included, share the other.
        event_type:
          type: string
          enum: [viewed]
        payload:
          $ref: '#/components/schemas/OutcomeViewedPayload'
    OutcomeAcceptedRequest:
      type: object
      description: 'An outcome event of type `accepted`. Unknown fields are rejected with `400 invalid_request`.'
      additionalProperties: false
      required: [event_id, event_type, payload]
      properties:
        event_id:
          type: string
          format: uuid
          description: A UUID version 4 in its 36-character hyphenated form. Send it in lowercase and repeat it exactly on a retry. Idempotency key, unique per organization and environment bucket. Live keys share one bucket, and test keys, every named sandbox included, share the other.
        event_type:
          type: string
          enum: [accepted]
        payload:
          $ref: '#/components/schemas/OutcomeAcceptedPayload'
    OutcomeOverriddenRequest:
      type: object
      description: 'An outcome event of type `overridden`. Unknown fields are rejected with `400 invalid_request`.'
      additionalProperties: false
      required: [event_id, event_type, payload]
      properties:
        event_id:
          type: string
          format: uuid
          description: A UUID version 4 in its 36-character hyphenated form. Send it in lowercase and repeat it exactly on a retry. Idempotency key, unique per organization and environment bucket. Live keys share one bucket, and test keys, every named sandbox included, share the other.
        event_type:
          type: string
          enum: [overridden]
        payload:
          $ref: '#/components/schemas/OutcomeOverriddenPayload'
    OutcomePlacementOutcomeRequest:
      type: object
      description: 'An outcome event of type `placement_outcome`. Unknown fields are rejected with `400 invalid_request`.'
      additionalProperties: false
      required: [event_id, event_type, payload]
      properties:
        event_id:
          type: string
          format: uuid
          description: A UUID version 4 in its 36-character hyphenated form. Send it in lowercase and repeat it exactly on a retry. Idempotency key, unique per organization and environment bucket. Live keys share one bucket, and test keys, every named sandbox included, share the other.
        event_type:
          type: string
          enum: [placement_outcome]
        payload:
          $ref: '#/components/schemas/OutcomePlacementOutcomePayload'
    OutcomeViewedPayload:
      type: object
      description: 'Payload for `viewed`. Unknown fields are rejected with `400 invalid_payload`.'
      additionalProperties: false
      required: [viewed_at]
      properties:
        viewed_at:
          type: string
          format: date-time
          description: When a person looked at the result. RFC 3339, with an offset of at most 23 hours.
        broker_id:
          type: string
          description: Optional identifier of the person who acted, in your own system.
    OutcomeAcceptedPayload:
      type: object
      description: 'Payload for `accepted`. Unknown fields are rejected with `400 invalid_payload`.'
      additionalProperties: false
      required: [accepted_at, accepted_carrier_id]
      properties:
        accepted_at:
          type: string
          format: date-time
          description: When the recommended option was taken. RFC 3339, with an offset of at most 23 hours.
        accepted_carrier_id:
          type: string
          minLength: 1
          pattern: '\S'
          description: The market that was accepted. Must not be blank.
        broker_id:
          type: string
          description: Optional identifier of the person who acted, in your own system.
    OutcomeOverriddenPayload:
      type: object
      description: |
        Payload for `overridden`. Unknown fields are rejected with `400 invalid_payload`.
        When `reason_code` is `other`, `reason_text` is required and must contain a
        non-whitespace character.
      additionalProperties: false
      required: [overridden_at, chosen_carrier_id, reason_code]
      properties:
        overridden_at:
          type: string
          format: date-time
          description: When a different option was chosen. RFC 3339, with an offset of at most 23 hours.
        chosen_carrier_id:
          type: string
          minLength: 1
          pattern: '\S'
          description: The market that was chosen instead. Must not be blank.
        reason_code:
          type: string
          enum: [appetite_mismatch, better_commission, broker_preference, carrier_appetite, claims_service_concern, client_preference, coverage_gap, customer_preference, existing_carrier_relationship, jurisdiction_issue, other, price, pricing_not_competitive, prior_bind_history]
          description: Why the recommendation was overridden.
        reason_text:
          type: string
          maxLength: 1000
          description: 'Free text, at most 1,000 characters (counted as Unicode code points). Longer text answers `400 reason_text_too_long`. Required and non-blank when `reason_code` is `other`.'
        reason_taxonomy_version:
          type: string
          enum: ['2026-08-31-unified', '']
          description: 'The reason taxonomy version. An empty string is treated as absent. Any other value answers `400 reason_taxonomy_version_unknown`. The server stamps `2026-08-31-unified` when it is absent or empty.'
        broker_id:
          type: string
          description: Optional identifier of the person who acted, in your own system.
      if:
        properties:
          reason_code:
            const: other
        required: [reason_code]
      then:
        required: [reason_text]
        properties:
          reason_text:
            minLength: 1
            pattern: '\S'
    OutcomePlacementOutcomePayload:
      type: object
      description: 'Payload for `placement_outcome`. Unknown fields are rejected with `400 invalid_payload`.'
      additionalProperties: false
      required: [outcome_at, status]
      properties:
        outcome_at:
          type: string
          format: date-time
          description: When the downstream result happened. RFC 3339, with an offset of at most 23 hours.
        status:
          type: string
          enum: [quoted, bound, lost, declined]
          description: The downstream result.
        carrier_id:
          type: string
          description: The market the result applies to.
    Error:
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          description: |
            Stable snake_case error identifier. Safe to match programmatically.
            New codes are added over time; treat an unknown code by its HTTP status.

            Known codes include:
            `invalid_request`, `invalid_input`, `unauthorized`,
            `auth_unavailable`, `rate_limited`, `missing_run_id`,
            `invalid_run_id`, `run_not_found`, `invalid_offerings`,
            `missing_input`, `queue_unreachable`, `engine_not_found`,
            `engine_slug_conflict`, `engine_archived`, `instance_not_found`,
            `instance_slug_conflict`, `instance_archived`,
            `instance_follows_canonical`, `instance_not_published`,
            `invalid_engine_type`, `validation_error`, `webhook_not_found`,
            `webhook_limit_exceeded`, `invalid_url`, `invalid_events`,
            `invalid_description`, `invalid_auth_token`,
            `missing_endpoint_id`, `invalid_endpoint_id`,
            `missing_delivery_id`, `invalid_delivery_id`,
            `delivery_not_found`, `delivery_not_resendable`,
            `insufficient_capability`, `engine_version_required`,
            `engine_version_exact_required`, `engine_version_not_found`,
            `engine_version_invalid_format`,
            `engine_version_draft_requires_sandbox_key`,
            `engine_version_none_released`, `test_scenario_sandbox_only`,
            `test_scenario_forbidden`, `test_scenario_exact_version_required`,
            `test_scenario_version_not_supported`, `invalid_test_scenario`,
            `request_bound_exceeded`, `run_cap_exceeded`,
            `scoped_key_required`, `engine_binding_forbidden`,
            `request_timeout`, `internal_error`, `idempotency_key_reused`,
            `invalid_idempotency_key`, `acting_principal_mismatch`,
            `run_requires_acting_principal`, `environment_slug_invalid`,
            `environment_slug_reserved`, `environment_slug_taken`,
            `environment_name_invalid`, `environment_limit_reached`,
            `environment_live_immutable`, `environment_not_found`,
            `environment_in_use`, `environment_operation_failed`,
            `list_environments_failed`, `engine_release_unservable`,
            `engine_config_hash_inconsistent`,
            `engine_config_hash_unavailable`,
            `engine_config_version_unavailable`,
            `engine_version_resolve_failed`,
            `engine_config_version_load_failed`,
            `engine_version_publish_mismatch`,
            `engine_version_schema_change_requires_major`,
            `engine_config_hash_missing`, `engine_config_changed`,
            `invalid_engine_config`, `cold_start_gate_not_met`,
            `cold_start_gate_failed`, `cold_start_gate_regressed`,
            `cancel_run_failed`, `event_id_reused`, `invalid_event_id`,
            `missing_event_id`, `invalid_event_type`, `invalid_payload`,
            `reason_text_too_long`, `reason_taxonomy_version_unknown`,
            `invalid_rating`, `missing_comment`, `invalid_target`,
            `invalid_time_on_task`, `scoped_annotation_required`,
            `missing_instance_slug`, `invalid_instance_config`,
            `invalid_eval_waiver`, `instance_config_hash_missing`,
            `config_changed_during_publish`,
            `scenario_set_changed_during_publish`,
            `conversation_eval_regressed`,
            `conversation_eval_execution_failed`,
            `conversation_eval_gate_unavailable`, `publish_failed`,
            `conversation_not_found`, `conversation_archived`,
            `invalid_cursor`, `message_not_found`, `invalid_turn_request`,
            `message_too_long`, `invalid_tool_result`, `invalid_confirmation`,
            `invalid_edit_target`, `turn_in_progress`, `pending_turn`,
            `turn_state_conflict`, `confirmation_environment_unpinned`,
            `instance_config_missing`, `instance_config_invalid`,
            `instance_model_unavailable`, `guardrail_evaluation_failed`,
            `guardrail_config_invalid`, `invalid_conversation_history`,
            `streaming_unsupported`, `converse_unavailable`, `provider_error`,
            `provider_unavailable`, `unknown_model`, `too_many_messages`,
            `too_many_tools`, `invalid_max_tokens`, `invalid_message_role`,
            `invalid_message`, `invalid_content_block`, `invalid_tool`,
            `attachments_not_enabled`, `attachments_unavailable`,
            `attachment_rejected`, `attachment_too_large`,
            `attachments_too_large`, `too_many_attachments`,
            `attachment_not_accepted`, `attachment_not_found`,
            `attachment_changed`, `attachment_read_failed`,
            `attachment_member_delete`, `attachment_not_reservable`,
            `invalid_offset`, `invalid_turn_id`, `invalid_comment`,
            `invalid_reason`, `invalid_feedback_key`, `turn_not_found`,
            `annotation_not_found`, `annotation_not_promotable`,
            `invalid_eval_scenario`, `conversation_eval_scenario_not_found`,
            `conversation_eval_scenario_cap`,
            `conversation_eval_run_not_found`,
            `conversation_eval_baseline_not_found`,
            `conversation_eval_unavailable`,
            `instance_version_invalid_format`, `instance_version_not_found`,
            `document_class_not_open`, `presign_failed`,
            `markets_directory_unavailable`, `book_unavailable`,
            `book_connection_ambiguous`, `identity_unmapped`,
            `identity_needs_review`, `identity_suspended`, `identity_stale`,
            `scope_unavailable`, `assertion_invalid`, `assertion_stale`,
            `cursor_filter_mismatch`, `cursor_expired`,
            `action_schema_unknown`, `action_payload_invalid`,
            `action_out_of_scope`, `action_list_too_large`,
            `overlay_read_only`, `unsupported_node_type`,
            `acting_principal_required`, `action_denied`,
            `appetite_read_denied`, `approval_required`,
            `catalog_access_denied`, `catalog_connection_ambiguous`,
            `create_webhook_failed`, `dataset_denied`, `delivery_id_reused`,
            `document_type_not_servable`, `document_unclassified`,
            `egress_manifest_version_mismatch`,
            `egress_manifest_version_required`, `event_type_not_allowed`,
            `event_type_reserved`, `execution_confirm_required`,
            `execution_prohibited`, `ingest_source_disabled`,
            `ingest_source_misconfigured`, `ingest_source_not_found`,
            `invalid_acting_assertion`, `invalid_active`,
            `invalid_credentials`, `invalid_payload_mode`,
            `invalid_signature`, `load_run_failed`, `load_solutions_failed`,
            `load_work_items_failed`, `missing_engine_slug`, `not_found`,
            `object_store_unconfigured`, `provider_not_approved`,
            `resolve_retry_exhausted`, `run_lookup_failed`,
            `service_identity_unknown`, `stale_timestamp`, `surface_denied`,
            `unresolved_market_question`, `unsupported_market_filter`.

            The full list with causes and fixes is at
            https://docs.usenexio.com/reference/errors.
        message:
          type: string
          description: Human-readable error message. May change between versions.
        details:
          description: |
            Optional request-specific details. Request-bound failures use the
            `RequestBoundDetails` object. Validation failures may use an array
            of field issues or another documented object.

            `409 environment_in_use` is the one exception to this envelope: it
            carries a top-level `blockers` object (see `EnvironmentInUseError`)
            instead of `details`.

    RequestBoundDetails:
      type: object
      additionalProperties: false
      required: [bound, path, limit, actual, measured]
      properties:
        bound:
          type: string
          enum:
            - http_envelope_bytes
            - max_canonical_bytes
            - max_string_length
            - max_array_items
            - max_object_fields
            - max_object_depth
        path:
          type: string
          description: JSON path to the first violation, for example `$.input.items`. `$` for whole-body bounds.
        limit:
          type: integer
          description: The limit that applied.
        measured:
          type: integer
          description: The measured value. For `http_envelope_bytes` it is a lower bound (limit plus one).
        actual:
          type: integer
          description: Same value as `measured`, kept for existing clients.
    RequestBoundError:
      description: The `Error` body for `413 request_bound_exceeded`, with `details` set to `RequestBoundDetails`.
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          additionalProperties: false
          required: [code, message, details]
          properties:
            code:
              type: string
              const: request_bound_exceeded
            message:
              type: string
            details:
              $ref: '#/components/schemas/RequestBoundDetails'
    EngineVersion:
      type: object
      required: [version, released_at, changelog, is_breaking_from_previous_major, gate_verified]
      properties:
        version:
          type: string
          pattern: '^\d+\.\d+$'
          description: Two-tier `major.minor` identifier (e.g. `1.0`, `1.3`, `2.0`).
        released_at:
          type: string
          format: date-time
          description: When the version row was created.
        changelog:
          type: string
          description: Operator-supplied human-readable summary of what changed.
        request_schema_hash:
          type: string
          pattern: '^[0-9a-f]{64}$'
          description: SHA-256 of the exact normalized released request schema bytes.
        response_schema_hash:
          type: string
          pattern: '^[0-9a-f]{64}$'
          description: SHA-256 of the exact normalized released response schema bytes.
        is_breaking_from_previous_major:
          type: boolean
          description: |
            True only when this is a `*.0` row AND a prior major (`(major-1).*`)
            also exists for the same engine. The first-ever release of an
            engine reports `false`.
        published_by:
          type: string
          description: |
            Actor recorded at publish: a dashboard user ID for dashboard
            publishes, `api_key:<id>` for API publishes. Omitted on
            backfilled rows with no recorded actor.
        gate_verified:
          type: boolean
          description: True only on an engine's first release when the eval gate verified the candidate against a labeled evaluation set. Later releases report `false`.
        cold_start_eval_set_id:
          type: string
          description: The evaluation set that verified the release. Present only when `gate_verified` is true.

    EngineVersionsListResponse:
      type: object
      additionalProperties: false
      required: [versions]
      properties:
        versions:
          type: array
          items:
            $ref: '#/components/schemas/EngineVersion'

    # ── Runs ───────────────────────────────────────────────────────────

    SubmitRunRequest:
      type: object
      additionalProperties: false
      required: [input]
      properties:
        input:
          type: object
          additionalProperties: true
          description: |
            Engine-specific submission. The fields depend on the engine type and
            its configuration: see [Engines overview](/engines/overview#engine-types)
            and the engine's Contract page in the portal, which renders the typed
            request schema.
        offerings:
          type: array
          description: |
            Alternatives to rank, for the types that take them: required in
            practice by `comparison` engines; optional on `matching` engines,
            which source candidates from connected data. The offering rules
            below apply to these engines. An offering without `id` or a positive
            `attributes.line_premium` is dropped. A comparison run reports each
            drop in `output.warnings`; a matching run does not list drops. A
            kept offering must also carry `provider_id`, `provider_name` and
            `category`, or the submission answers 400 `invalid_offerings`. An
            unknown key on an offering or in its `constraints` fails with
            `400 invalid_request`; `coverage` and `attributes` accept extra keys.
          items:
            $ref: '#/components/schemas/Offering'
        engine_version:
          type: string
          pattern: '^(\d+\.(\d+|x)|draft)$'
          description: |
            Pins which released engine configuration and request/response
            schemas this run uses.

            - `"1.3"`: exact released configuration and schemas.
            - `"1.x"`: auto-track the latest released minor of major 1 (you
              ride minor releases only when the engine policy permits it).
            - `"draft"`: mutable unpublished config, accepted only in a
              sandbox environment.

            Bare major (`"1"`) and three-tier semver (`"1.0.0"`) are rejected.
            Omission resolves to the latest release on engines with the default pin
            policy, and fails with `500 engine_version_none_released` when the engine
            has no release. `exact_required` engines refuse an omitted pin
            (`engine_version_required`) and an `N.x` pin
            (`engine_version_exact_required`); `draft` from a sandbox key is
            still accepted. The resolved exact version is stable for the run and is
            echoed by polling and webhooks. See [Versions and releases](/engines/versions).
          example: '1.0'
        test_scenario:
          type: string
          enum: [completed, degraded, failed]
          description: |
            Deterministic supported-version fixture. Requires a sandbox
            environment, a scoped key with `runs:test`, and an exact `N.M`
            `engine_version` present in the published registry, which today
            holds only the generic fixture engine. Fixture runs make no
            provider calls and count against the monthly run cap. See
            [Sandbox fixtures](/reference/sandbox-fixtures).

        submitted_by:
          type: string
          description: |
            An optional restatement of the acting principal. It is not an
            alternative to the `X-Nexio-Acting-Principal` header: a nonempty
            value is accepted only when the header is also sent with the same
            value (after trimming). A value without the header, or with a
            different header value, fails with `400 acting_principal_mismatch`.
            Engines of the `matching` type require the header.
    SubmitRunResponse:
      type: object
      required: [run_id, status]
      properties:
        run_id:
          type: string
          format: uuid
          description: Unique run identifier. Use this to poll for results.
        status:
          type: string
          description: |
            `queued` for a new run. On a retry with the same `Idempotency-Key`,
            the existing run's current status, lowercased (for example
            `running` or `completed`). Treat any value as "the run exists" and
            poll it.


    EnrichmentOutcomeBlock:
      type: object
      additionalProperties: false
      required: [kind]
      description: |
        Terminal outcome for one configured and enabled enrichment source.
        `status` and `attempted` are present on every run completed under the
        current outcome contract; enrichment blocks retrieved from runs
        archived before that rollout omit them, so treat both as optional. `payload` is required for `succeeded` and
        `not_mapped` and absent for all other statuses. `fetched_at` is
        optional. Block diagnostics are byte-equivalent to the matching entries
        in the top-level output diagnostics array.

        `not_mapped` means the source returned no mapped feature. It does not
        mean no flood risk. Public-source data and cache entries may be stale.

        FEMA degraded and circuit-open diagnostics use bounded
        `details.upstream` values `google_geocoding` and `nfhl`.
      properties:
        kind:
          type: string
          example: fema_nfhl
        status:
          type: string
          enum: [succeeded, not_mapped, skipped, ambiguous, unavailable]
        attempted:
          type: boolean
          description: False when no upstream attempt occurred, including a cache hit or pre-call skip.
        payload:
          type: object
          additionalProperties: true
          description: |
            Required for `succeeded` and `not_mapped`; absent otherwise. A
            successful `fema_nfhl` payload always includes `sfha`; it is a
            boolean when FEMA supplied T/F and explicit JSON null when unknown.
        fetched_at:
          type: string
          format: date-time
        diagnostics:
          type: array
          items:
            type: object
            additionalProperties: true
      allOf:
        - if:
            properties:
              status:
                enum: [succeeded, not_mapped]
            required: [status]
          then:
            required: [payload]
        - if:
            properties:
              status:
                enum: [skipped, ambiguous, unavailable]
            required: [status]
          then:
            not:
              required: [payload]
        - if:
            properties:
              kind:
                const: fema_nfhl
              status:
                const: succeeded
            required: [kind, status]
          then:
            properties:
              payload:
                required: [sfha, base_flood_elevation_feet]
                properties:
                  sfha:
                    type: [boolean, 'null']

    RunStatusResponse:
      type: object
      required: [run_id, status, environment, attempt, created_at]
      properties:
        run_id:
          type: string
          format: uuid
          description: Stable run identifier.
        engine_type:
          type: string
          description: |
            Runtime engine type: `comparison`, `matching`, `entity_analysis`,
            `diligence`, `triage` or `opportunity`. Runs recorded before the
            cutover keep `placement`. Omitted on legacy rows without engine
            metadata.
        engine_version:
          type: string
          description: |
            The engine version this run executed against, resolved at submit
            time and frozen for the run's lifetime. A released `major.minor`
            label when the run was pinned to a release (`N.M` or `N.x`) or sent
            no pin (an unpinned public submission resolves to the latest
            release). The literal `draft` when a sandbox key pinned `draft` and
            the run executed the unpublished configuration. Absent only on
            older or internal records that carry no version stamp.
          example: '1.3'
        engine_config_version_hash:
          type: string
          description: |
            Immutable hash of the archived engine configuration this run
            executed. Resolved and frozen at submission alongside
            `engine_version`. Absent on legacy unstamped runs.
          example: '297c960d80b848c9'
        status:
          type: string
          enum: [queued, processing, completed, degraded, failed, cancelled]
          description: |
            Current run status. `queued` and `processing` are non-terminal: keep
            polling. `completed`, `degraded`, `failed`, and `cancelled` are
            terminal. `degraded` carries output; read `output.degradation_reason`
            (entity analysis) or `output.partial` (matching). See
            [Runs](/engines/runs#degraded-runs).
        environment:
          type: string
          enum: [test, live]
          description: Environment the run executed in.
        parked_until:
          type: string
          format: date-time
          description: Scheduled resume from the latest warehouse wait. Present while processing until a worker heartbeat confirms resume. Keep polling past this deadline.
        park_reason:
          type: string
          description: Reason for the active park. Present alongside parked_until.
        last_parked_until:
          type: string
          format: date-time
          description: Latest warehouse wait deadline, retained after resume so clients can extend their polling budget through the park.
        stage:
          type: string
          description: Last known pipeline stage (e.g. `EVALUATE`, `FILTER`).
        output:
          type: object
          additionalProperties: true
          description: |
            Run output. Present only on `completed` and `degraded` runs. The shape
            comes from the engine's declared response schema, which its type
            derives from its configuration; see [Engine types](/engines/overview#engine-types).
            The named properties below are written by specific types.
          properties:
            output_phase:
              type: string
              enum: [final, deterministic_draft]
              description: |
                The stored block's own copy of the top-level `output_phase`. Always
                `final` on a served output. Written on `comparison` output blocks.
            enrichment:
              type: object
              description: Per-source enrichment outcome blocks keyed by handler kind.
              additionalProperties:
                $ref: '#/components/schemas/EnrichmentOutcomeBlock'
            diagnostic:
              oneOf:
                - type: string
                - type: 'null'
              description: '`comparison` type: why nothing was ranked.'
            degradation_reason:
              type: string
              enum:
                - no_requirements
                - no_offerings
                - no_combinations
                - input_quality
                - llm_degraded
                - enrichment_degraded
                - scoring_rule_failed
                - mixed
                - other
                - insufficient_corpus
              description: |
                Stable reason on `degraded` runs, and on comparison runs that completed
                with nothing ranked. Customer automation routes on this instead of
                parsing free-text from `diagnostic`. When multiple non-info diagnostics
                are present, strict precedence picks the most actionable single reason:
                `input_quality` > `llm_degraded` >
                `enrichment_degraded` > `other`. `mixed` and `scoring_rule_failed`
                are defined but not emitted. `insufficient_corpus` is set on
                `completed` runs by a decline gate that runs only when a platform
                setting enables it. Outputs of matching runs report loss in
                `output.partial` instead.
            requirement_count:
              type: integer
              description: '`comparison` type: number of required categories evaluated.'
            solutions_count:
              type: integer
              description: '`comparison` type: number of ranked results generated.'
            appetite_bucket:
              type: string
              description: '`comparison` type: the weight profile the run ranked with (`coverage_first`, `cost_sensitive`, `balanced`, `simplicity`).'
            top_label:
              oneOf:
                - type: string
                - type: 'null'
              description: '`comparison` type: label of the top-ranked result (e.g. `recommended`, `best_value`).'
            top_score:
              oneOf:
                - type: number
                - type: 'null'
              description: '`comparison` type: overall score of the top-ranked result.'
        duration_ms:
          type: integer
          description: Duration of the final execution attempt in milliseconds.
        total_duration_ms:
          type: integer
          description: Wall-clock milliseconds from creation to terminal completion.
        attempt:
          type: integer
          minimum: 0
          description: Execution attempt count. A terminal value of 1 means no worker retry.
        error:
          type: string
          description: Error message on failed runs.
        error_details:
          type: object
          additionalProperties: true
          description: Structured failure details.
        trace_id:
          type: string
          pattern: '^[0-9a-f]{32}$'
          description: Trace identifier for support correlation.
        created_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp when the run was created.
        completed_at:
          type: string
          format: date-time
          description: RFC 3339 timestamp when the run reached a terminal state.
        completed_deterministic_at:
          type: string
          format: date-time
          description: |
            When a run of the `comparison` type finished its deterministic scoring. Stamped
            mid-run, so it can appear while `status` is still `processing`. A
            timing fact only: no output is served until the run is `completed`
            or `degraded`. Absent on other engine types and on webhook payloads.
        output_phase:
          type: string
          enum: [final, deterministic_draft]
          description: |
            Which answer `output` and `solutions` carry. Present on every
            `completed` and `degraded` run, and then always `final`, even when
            `solutions` (for example on an engine that produces none) or
            `output` is absent. Absent on `queued`, `processing`, `failed` and
            `cancelled` runs, and on webhook payloads.
        solutions:
          type: array
          description: |
            Ranked results. Present on `completed` and `degraded` runs of the
            types that rank them (`comparison`, `matching`), when at least one was
            produced.
          items:
            $ref: '#/components/schemas/Solution'
        warnings:
          description: |
            Structured input-quality warnings. Present only when the run recorded
            warnings and the engine's current saved configuration sets
            `expose_warnings: true` when the run is read. The setting is read on
            each request, not from the release the run used.
          oneOf:
            - type: object
              additionalProperties: true
            - type: array
              items:
                type: object
                additionalProperties: true

        started_at:
          type: string
          format: date-time
          description: When a worker first started the run.
        input:
          type: object
          additionalProperties: true
          description: The admitted submission. Present only with `include=input`.
        computed_at_head:
          type: string
          description: The source data head the run computed against, on runs of engines Nexio operates that are recorded as a result in a connected system of record.
        served_head:
          type: string
          description: The source data head served now, on the same runs.
        stale:
          type: boolean
          description: >-
            True when the assessed subject changed between the head the run
            computed against (`computed_at_head`) and the head that now serves
            it (`served_head`). A result carried to a newer head unchanged has
            `stale: false`. Present on the same runs as `computed_at_head`.
        work_items:
          type: object
          additionalProperties: true
          description: |
            Compact rollup of the run's work items: `total`, `required`,
            `required_complete`, `required_incomplete`, `required_failed`,
            `required_cancelled`, optional `problems`, and
            `suggested_terminal`.
    Solution:
      type: object
      required: [id, offerings, requirements_met, provider_count, scorecard]
      properties:
        id:
          type: string
          format: uuid
          description: Stable solution identifier within this run.
        offerings:
          type: array
          description: Offerings included in this solution package.
          items:
            $ref: '#/components/schemas/SolutionOffering'
        requirements_met:
          type: array
          items:
            type: string
          description: Requirement markers satisfied, as `lob_<category>` (for example `lob_home`).
        provider_count:
          type: integer
          description: Number of distinct providers in this solution.
        est_cost_low:
          type: integer
          description: Estimated annual cost, low end.
        est_cost_high:
          type: integer
          description: Estimated annual cost, high end.
        consolidation_discount:
          type: number
          description: |
            Bundling discount applied to this solution, as a fraction
            (0.0 to 1.0). Derived from
            `bundle.standalone_premium_annual` and
            `bundle.joined_premium_annual` when a bundle is applied; `0`
            otherwise.
        bundle:
          $ref: '#/components/schemas/AppliedBundle'
        scorecard:
          $ref: '#/components/schemas/Scorecard'
        cluster_label:
          type: string
          description: |
            Solution label indicating where it excels: `recommended`,
            `best_value`, `best_coverage`, `simplest`.
        rank:
          type: integer
          description: Rank position (1 = best).
        headline:
          type: string
          description: A one-line summary of the solution. Omitted when the run produced none.
        narrative:
          description: A structured explanation of the solution, as JSON. Omitted when the run produced none.

    Scorecard:
      type: object
      required: [overall_level]
      additionalProperties: true
      description: |
        Evaluation scorecard. `overall_level` is always present. Dimension
        keys are configured per released engine version; the named properties
        below are the default dimensions of the `comparison` type, not an
        exhaustive set.

        A dimension the run had no signal to score is present with `level`
        0 and `method` `suppressed`, and `overall_level` is the weighted
        level over the scored dimensions only. For a scored solution,
        `overall_level` is on a 1 to 4 scale where higher is better (a level 1
        dimension counts as 4). A solution from a matching run is not scored
        this way: its scorecard carries `overall_level: 0` and no dimensions or
        `fit_score`.
      properties:
        coverage_completeness:
          $ref: '#/components/schemas/Dimension'
        pricing_competitiveness:
          $ref: '#/components/schemas/Dimension'
        provider_quality:
          $ref: '#/components/schemas/Dimension'
        placement_likelihood:
          $ref: '#/components/schemas/Dimension'
        operational_simplicity:
          $ref: '#/components/schemas/Dimension'
        risk_alignment:
          $ref: '#/components/schemas/Dimension'
        overall_level:
          type: number
          description: |
            Raw weighted level across configured dimensions. Use the
            solution's emitted `rank`, not this value, as the ordering contract.
        assessed_weight_fraction:
          type: number
          minimum: 0
          maximum: 1
          description: |
            Share of the enabled scoring weight this scorecard actually scored.
            Emitted only by engine versions that declare an assessed-weight floor.
        run_position:
          $ref: '#/components/schemas/RunPosition'

    RunPosition:
      type: object
      required: [percentile, tied, population, population_unit, basis, field_best, field_worst]
      description: |
        Where this solution sits inside its own run's field. Emitted only by
        engine versions that enable it. An absolute band does not discriminate
        on a narrow field, so position is measured against the run's own spread.

        The population is the solutions this run assembled, scored and ranked,
        excluding any that fell below the engine's assessed-weight floor: those
        did not compete, and they carry no `run_position` at all.
      properties:
        percentile:
          type: number
          description: |
            Midrank percentile of `overall_level` over the population, higher
            being better, with ties corrected. Every member of a fully tied
            field reads 50.
        margin:
          type: number
          description: |
            Gap in `basis` to the next solution down in the population. Absent
            on the lowest solution, which has no next. `0` is a real value and
            means an exact tie.
        tied:
          type: integer
          description: |
            How many solutions share this exact `basis` value, including this
            one. Counted in `population_unit`. 1 means unique.
        population:
          type: integer
          description: How many solutions the percentile was measured over, counted in `population_unit`.
        population_unit:
          type: string
          description: What `population` and `tied` count.
        basis:
          type: string
          description: What `percentile`, `margin`, `field_best` and `field_worst` measure.
        field_best:
          type: number
          description: The population's best value on `basis`.
        field_worst:
          type: number
          description: |
            The population's worst value on `basis`. With `field_best` it gives
            the field's own spread, which is what turns a margin into a
            judgment without holding the whole field.

    Dimension:
      type: object
      required: [level, label, justification, method]
      properties:
        level:
          type: integer
          minimum: 0
          maximum: 4
          description: Rating from 1 (best) to 4 (worst). 0 means the dimension was not scored (`method` is `suppressed`).
        label:
          type: string
          description: Human-readable label for this level.
        justification:
          type: string
          description: Reasoning for the assigned level.
        method:
          type: string
          enum: [deterministic, llm, fallback, suppressed]
          description: How this dimension was scored. `suppressed` means there was no signal to score it and it is left out of `overall_level`.

    SolutionOffering:
      type: object
      required: [id, provider_name, category]
      description: Abbreviated offering reference within a ranked solution.
      properties:
        id:
          type: string
          description: Line-level offering identifier (matches an entry in the submitted `offerings`).
        provider_name:
          type: string
          description: Provider display name.
        category:
          type: string
          description: Requirement category (domain-specific, e.g. `home`, `auto`, `umbrella`).
        data_currency:
          allOf:
            - $ref: '#/components/schemas/DataCurrency'
          description: |
            Freshness of this offering's underlying source data. Serialized on
            each offering object within a ranked solution (the engine's
            `Offering`); clients read it at `solutions[].offerings[].data_currency`.
            Present only for offerings loaded from a materialized connection.
        alternates:
          type: array
          description: |
            Other offerings that would have filled THIS seat: offerings whose
            answer on every per-seat axis matches the seated one exactly, so
            the engine ranks one and carries the rest here instead
            of as near-identical ranked entries. Every alternate cleared the
            same eligibility this line's seated offering did, and each carries
            its own per-seat scorecard. Read at
            `solutions[].offerings[].alternates`. Present only on engine versions
            that emit alternates; omitted otherwise. Runs of the `matching` type
            do not populate it: they list other candidates by ID at
            `offerings[].attributes.matching.alternates`.
          items:
            $ref: '#/components/schemas/SeatAlternate'

    SeatAlternate:
      type: object
      required: [offering_id, provider_name, scorecard]
      description: One offering that would have filled a seat in a ranked solution.
      properties:
        offering_id:
          type: string
          description: Line-level offering identifier, from this run's own candidates.
        provider_name:
          type: string
          description: Provider display name.
        scorecard:
          type: object
          additionalProperties: true
          description: |
            This alternate seat's own answer on the per-provider axes:
            `market_entity_key`, `line`, and one dimension per axis.

    DataCurrency:
      type: object
      description: |
        Freshness of the underlying source data for an offering loaded from a
        materialized (replicate-then-index) connection. Advisory only; it labels
        the result and never changes ranking. Present only for materialize-sourced
        offerings; omitted otherwise.
      properties:
        effective_year:
          type: integer
          description: The source schedule's effective business year, when known.
        effective_date:
          type: string
          format: date
          description: The product's effective date (YYYY-MM-DD), when known.
        stale:
          type: boolean
          description: |
            True when the source data is older than the freshness window (more than
            one year old) or undated. Show a stale offering with a caution that its
            rates can be out of date.
        undated:
          type: boolean
          description: True when no effective year or date is known at all.
        effective_date_basis:
          type: string
          enum: [stated, schedule_year, absent]
          description: |
            Where `effective_date` came from, so a synthesized day is never read
            as a stated one. `stated` means the source document reported a
            per-line date and it is the date shown. `schedule_year` means the
            document stated only a year, so the date is January 1 of that year:
            read the year, do not read the day. `absent` means no date is
            available at all. Present only on engines that emit seat facts;
            omitted otherwise.

    # ── Offerings ──────────────────────────────────────────────────────

    Offering:
      type: object
      required:
        - id
        - provider_id
        - provider_name
        - category
      properties:
        id:
          type: string
          description: Stable line-level identifier.
        provider_id:
          type: string
          description: Stable provider identifier.
        provider_name:
          type: string
          description: Provider display name.
        category:
          type: string
          description: Requirement category (domain-specific). Must match a value in `input.coverage_types`.
        categories:
          type: array
          items:
            type: string
          description: Every match category the offering belongs to, when it belongs to more than one. `category` stays the primary value.
        quality_rating:
          type: string
          description: Provider quality rating (e.g. `A+`, `A`, `B++`).
        pricing_tier:
          type: string
          description: Pricing tier (`premium`, `standard`, `economy`).
        commission:
          type: number
          description: Rate the provider pays on this offering, as a decimal (e.g. `0.14` for 14%).
        coverage:
          $ref: '#/components/schemas/CoverageDetail'
        constraints:
          $ref: '#/components/schemas/FilterConstraints'
        attributes:
          type: object
          additionalProperties: true
          description: |
            Domain-specific attributes. Structure depends on your engine's configuration. Example fields:

            - `quote_id`: line quote ID from your source system
            - `line_premium`: raw price for the term (annualized by the engine via `term_months`, which defaults to 12)
            - `package_premium_annual`: annual cost for the parent provider package
            - `package_id`: identifier tying offerings from the same provider package
            - `am_best`: AM Best rating
            - `broker`: originating intermediary name
            - `quote_generated_at`: ISO 8601 timestamp when the offering was generated
            - `quote_expires_at`: ISO 8601 timestamp when the offering expires
            - `term_months`: term length

    CoverageDetail:
      type: object
      description: Product-specific detail, pricing, and qualification data.
      properties:
        product_name:
          type: string
          description: Quoted product name.
        product_code:
          type: string
          description: Optional product code for the quoted product.
        program_type:
          type: string
          description: |
            Program type identifier (e.g. `quoted_line` for standard offerings).
        market_type:
          type: string
        distribution:
          type: string
        appetite_strength:
          type: string
        specialty_flags:
          type: array
          items:
            type: string
        limit_per_occurrence:
          type: integer
        limit_aggregate:
          type: integer
        limit_combined_single:
          type: integer
        limit_bodily_injury_per_person:
          type: integer
        limit_bodily_injury_per_accident:
          type: integer
        limit_property_damage:
          type: integer
        deductible_min:
          type: integer
        deductible_max:
          type: integer
        deductible_default:
          type: integer
        sublimits:
          type: object
          additionalProperties:
            type: integer
        endorsements_included:
          type: array
          items:
            type: string
        endorsements_available:
          type: array
          items:
            type: string
        endorsements_excluded:
          type: array
          items:
            type: string
        coverage_breadth:
          type: string
        includes_defense_costs:
          type: boolean
        tail_coverage:
          type: boolean
        bundle:
          $ref: '#/components/schemas/BundlePricing'
        rate_basis:
          type: string
        commission_renewal:
          type: number
        commission_renewal_known:
          type: boolean
        required_docs:
          type: array
          items:
            type: string
        loss_run_years:
          type: integer
        appetite_notes:
          type: string
        underwriting_notes:
          type: string

    BundlePricing:
      type: object
      description: |
        Set on a single offering when the provider sells this offering as part
        of a multi-line package. Identifies the partner offerings
        (`bundles_with`) and the provider's joined annual price
        (`joined_premium_annual`). Omit on standalone offerings.
      properties:
        bundles_with:
          type: array
          items:
            type: string
          description: |
            Offering IDs (from this same `offerings[]` array) that this
            offering is sold paired with. The bundle is recognized only when
            every listed offering, plus this one, appears in the same
            solution.
        joined_premium_annual:
          type: integer
          minimum: 0
          description: |
            Total annual price for the bundled group, in dollars per year.
            Replaces the sum of per-offering standalone annual prices when the
            bundle is fully present in the solution.
        required:
          type: boolean
          description: |
            Set to `true` if the provider will not sell any member of the
            bundle standalone. Solutions that include any bundle member
            without the rest are dropped.

    AppliedBundle:
      type: object
      description: |
        Present on a Solution when a provider-supplied bundle was applied.
        Lists the offerings that bundle together, the provider's joined annual
        price, the sum of standalone annual prices, and the dollar-per-year
        savings.
        Omitted on standalone solutions.
      required:
        - offering_ids
        - joined_premium_annual
        - standalone_premium_annual
        - savings_annual
      properties:
        offering_ids:
          type: array
          items:
            type: string
          description: IDs of every offering that participates in the applied bundle.
        joined_premium_annual:
          type: integer
          minimum: 0
        standalone_premium_annual:
          type: integer
          minimum: 0
        savings_annual:
          type: integer
          minimum: 0
          description: |
            `standalone_premium_annual` minus `joined_premium_annual`,
            clamped to zero when the provider's bundled price meets or
            exceeds the standalone sum.

    FilterConstraints:
      type: object
      additionalProperties: false
      description: |
        Eligibility constraints used by the filtering stage. Pass empty arrays `[]`
        for fields that don't apply to your domain. An unknown key fails the
        submission with `400 invalid_request`.
      properties:
        eligible_states:
          type: array
          items:
            type: string
          description: States the offering may be placed in.
        state_scope:
          type: string
        excluded_states:
          type: array
          items:
            type: string
          description: States where the offering is unavailable (can be empty `[]`).
        prohibited_naics:
          type: array
          items:
            type: string
          description: Industry classification codes that disqualify the offering. Pass `[]` if not applicable.
        avoided_naics:
          type: array
          items:
            type: string
          description: Industry classification codes the provider prefers to avoid. Pass `[]` if not applicable.
        min_employees:
          type: integer
          description: Minimum employee count for eligibility. Omit if not applicable.
        max_employees:
          type: integer
          description: Maximum employee count for eligibility. Omit if not applicable.
        min_vehicles:
          type: integer
          description: Minimum vehicle count for eligibility.
        max_vehicles:
          type: integer
          description: Maximum vehicle count for eligibility.
        min_premium:
          type: integer
          description: Minimum annual cost for eligibility.
        max_premium:
          type: integer
          description: Maximum annual cost for eligibility.
        expiration_date:
          type: string
          description: Offering expiration date.

    # ── Engine Management ─────────────────────────────────────────────

    EngineMetadata:
      type: object
      required: [id, slug, label, description, engine_type, status, group_key, created_at, updated_at]
      description: Engine metadata returned by management endpoints.
      properties:
        id:
          type: string
          description: Engine identifier.
        slug:
          type: string
          description: URL-safe engine slug, unique within the organization.
        label:
          type: string
          description: Human-readable engine name.
        description:
          type: string
          description: Optional engine description.
        engine_type:
          type: string
          enum: [comparison, matching, entity_analysis, diligence, triage, opportunity, converse, property_risk]
          description: |
            Engine type, one entry of the registry Nexio ships (see
            [Engine types](/engines/overview#engine-types)). `comparison` ranks
            alternatives supplied in the request. `matching` discovers, qualifies
            and ranks candidates from connected data. `entity_analysis` assesses
            one subject against requirements; with a declared output contract it
            is a declared-contract engine (see
            [Declared-contract engines](/engines/guides/contract-mode)).
            `diligence` verifies a claimed event against live web sources.
            `triage` classifies and ranks a whole population in one request.
            `opportunity` runs over a connected system of record on the cadence
            its configuration sets rather than per request.

            `converse` and `property_risk` are retired: they cannot be created,
            and a run against an engine that still carries one returns
            `invalid_engine_type`. They remain in this response enum because
            older engine rows are still readable.
        status:
          type: string
          enum: [active, archived]
          description: Engine status. An archived engine answers `403 engine_archived` on run submission, configuration saves, publishing and the version reads.
        group_key:
          type: string
          description: |
            Key of the engine's semantic group within the organization, or an
            empty string when ungrouped. Groups label what an engine serves
            and carry handling guidance in their description; they never gate
            any operation.
        created_at:
          type: string
          format: date-time
          description: RFC 3339 creation timestamp.
        updated_at:
          type: string
          format: date-time
          description: RFC 3339 last-update timestamp.

    EngineListResponse:
      type: object
      required: [engines]
      properties:
        engines:
          type: array
          items:
            $ref: '#/components/schemas/EngineMetadata'

    CreateEngineRequest:
      type: object
      required: [slug, label, engine_type]
      properties:
        slug:
          type: string
          minLength: 3
          maxLength: 50
          pattern: '^[a-z0-9][a-z0-9-]*[a-z0-9]$'
          description: |
            URL-safe identifier. 3 to 50 lowercase alphanumeric characters and
            hyphens. Must start and end with an alphanumeric character.
        label:
          type: string
          minLength: 1
          description: Human-readable engine name, 1 to 100 UTF-8 bytes.
        description:
          type: string
          description: Optional engine description, at most 500 UTF-8 bytes.
        engine_type:
          type: string
          enum: [comparison, matching, entity_analysis, diligence, triage, opportunity]
          description: |
            Engine type. Fixed at creation. This is the creatable set; see
            [Engines overview](/engines/overview#engine-types) for what each type
            does. Any other value is rejected with `invalid_engine_type`. The
            response enums are wider because retired types remain readable on
            existing rows.

    UpdateEngineRequest:
      type: object
      minProperties: 1
      description: At least one of `label`, `description`, `status` or `group_key` must be present with a non-null value. Unknown fields are ignored.
      properties:
        label:
          type: string
          minLength: 1
          description: Updated engine name, 1 to 100 UTF-8 bytes.
        description:
          type: string
          description: Updated description, at most 500 UTF-8 bytes.
        status:
          type: string
          enum: [active, archived]
          description: Set to `archived` to refuse run submission, configuration saves, publishing and the version reads.
        group_key:
          type: string
          description: |
            At most 100 UTF-8 bytes. Key of the engine's semantic group. Empty string clears the
            group. The key is a soft reference: it is accepted whether or
            not a matching group exists.

    EngineConfigResponse:
      type: object
      required: [config, config_updated_at, config_updated_by, engine_type, preset]
      description: |
        Engine configuration with audit metadata. The `config` structure
        varies by `engine_type`. The per-engine Contract page on
        platform.usenexio.com is the integration guide
        for your engine's typed request schema.
      properties:
        config:
          type: object
          additionalProperties: true
          description: |
            Engine configuration object. Structure depends on `engine_type`.
        config_updated_at:
          oneOf:
            - type: string
              format: date-time
            - type: 'null'
          description: When the config was last saved (creating the engine counts as a save). `null` when no save time is recorded.
        config_updated_by:
          oneOf:
            - type: string
            - type: 'null'
          description: Who last saved the config, for example the API key ID for an API save. `null` when none is recorded.
        engine_type:
          type: string
          enum: [comparison, matching, entity_analysis, diligence, triage, opportunity, converse, property_risk]
          description: |
            Engine type. Includes the retired `converse` and `property_risk` values, which
            cannot be created but are still readable on engine rows that predate their
            retirement.
        preset:
          type: [object, 'null']
          additionalProperties: true
          description: 'Default configuration template for this engine type. Null when the engine type has no registered default, which is the case for the retired `converse` and `property_risk` types.'
        request_schema:
          allOf:
            - $ref: '#/components/schemas/TypeSchemaNode'
          description: Typed request schema for engine types whose run input is a typed structure. Omitted otherwise.

    TypeSchemaNode:
      type: object
      required: [name, type, required]
      properties:
        name:
          type: string
        type:
          type: string
          enum: [string, integer, number, boolean, object, array, enum, datetime, uuid, any]
        required:
          type: boolean
        nullable:
          type: boolean
        description:
          type: string
        children:
          type: array
          items:
            $ref: '#/components/schemas/TypeSchemaNode'
        enum_values:
          type: array
          items:
            type: string
        privacy:
          type: object
          additionalProperties: true
        privacy_policy:
          type: object
          additionalProperties: true
        omit_from_example:
          type: boolean

    ConfigUpdateRequest:
      type: object
      required: [config]
      properties:
        config:
          type: object
          additionalProperties: true
          description: |
            Full replacement config. Must conform to the engine's
            type-specific schema. Use the
            [validate endpoint](/api-reference/engines/validate-engine-config)
            to check before saving.

    ValidateConfigResponse:
      type: object
      required: [valid, errors]
      properties:
        valid:
          type: boolean
          description: Whether the config passes all validation rules.
        errors:
          type: array
          description: Validation issues found. Empty when `valid` is `true`.
          items:
            $ref: '#/components/schemas/ValidationIssue'

    ValidationIssue:
      type: object
      required: [path, message]
      properties:
        path:
          type: string
          description: JSON path to the invalid field (e.g. `scoring_dimensions.0.key`).
        message:
          type: string
          description: Human-readable error description.
        code:
          type: string
          description: Machine-readable issue code, when the issue has one (for example on egress policy issues). Omitted otherwise.

    # ── Webhooks ───────────────────────────────────────────────────────

    CreateWebhookRequest:
      type: object
      additionalProperties: false
      required: [url, events]
      properties:
        url:
          type: string
          format: uri
          description: HTTPS endpoint URL to receive webhook deliveries. At most 2048 UTF-8 bytes after trimming.
        events:
          type: array
          minItems: 1
          items:
            type: string
            enum: [run.completed, run.failed, run.cancelled, run.superseded]
          description: Event types to subscribe to.
        description:
          type: string
          description: Optional human-readable description, at most 256 UTF-8 bytes after trimming.
        active:
          type: boolean
          default: true
          description: Whether the endpoint starts active. Defaults to `true`.
        auth_token:
          type: string
          description: |
            Optional bearer token included as `Authorization: Bearer <token>`
            on outbound deliveries. At most 1024 UTF-8 bytes after trimming.
            An empty value sets no token.
        payload_mode:
          type: string
          enum: [full, thin]
          description: |
            Delivery body shape. Defaults to `full`. `thin` delivers only run
            identifiers and terminal status (body stays under 1 KB); fetch the
            full run with `GET /api/v1/runs/{run_id}`.

    CreateWebhookResponse:
      allOf:
        - $ref: '#/components/schemas/WebhookEndpoint'
        - type: object
          required: [secret]
          properties:
            secret:
              type: string
              description: |
                HMAC-SHA256 signing secret (`whsec_...`). Displayed only once.
                Store it securely.

    WebhookEndpoint:
      type: object
      required: [id, url, environment, events, active, auth_token_configured, webhook_version, payload_mode, created_at]
      properties:
        id:
          type: string
          format: uuid
          description: Endpoint identifier.
        url:
          type: string
          format: uri
          description: Delivery target URL.
        environment:
          type: string
          enum: [test, live]
          description: Environment this endpoint receives events from.
        events:
          type: array
          items:
            type: string
            enum: [run.completed, run.failed, run.cancelled, run.superseded]
          description: Subscribed event types.
        description:
          type: string
          description: Human-readable description.
        active:
          type: boolean
          description: Whether the endpoint is active.
        auth_token_configured:
          type: boolean
          description: Whether an auth token is set (the token value is never returned).
        webhook_version:
          type: string
          description: Payload version string (e.g. `2026-03-22`).
        payload_mode:
          type: string
          enum: [full, thin]
          description: Delivery body shape. `full` (default) carries the complete run; `thin` carries only run identifiers and terminal status.
        previous_secret_expires_at:
          type: string
          format: date-time
          description: Exact instant when the previous secret stops being valid.
        deactivated_at:
          type: string
          format: date-time
          description: Present only while the system has deactivated the endpoint after a consecutive dead-letter streak. Cleared on re-enable.
        deactivated_by:
          type: string
          description: Actor that deactivated the endpoint (`system` for the automatic dead-letter streak deactivation). Present only while deactivated.
        deactivated_reason:
          type: string
          description: Why the endpoint was deactivated, including the triggering delivery ID. Present only while deactivated.
        created_at:
          type: string
          format: date-time
          description: RFC 3339 creation timestamp.
        updated_at:
          type: string
          format: date-time
          description: RFC 3339 last-update timestamp. Absent on the create response.

    ListWebhooksResponse:
      type: object
      required: [endpoints]
      properties:
        endpoints:
          type: array
          items:
            $ref: '#/components/schemas/WebhookEndpoint'

    UpdateWebhookRequest:
      type: object
      additionalProperties: false
      minProperties: 1
      description: At least one field is required.
      properties:
        url:
          type: string
          format: uri
        events:
          type: array
          minItems: 1
          items:
            type: string
            enum: [run.completed, run.failed, run.cancelled, run.superseded]
        description:
          oneOf:
            - type: string
            - type: 'null'
          description: At most 256 UTF-8 bytes after trimming.
        active:
          oneOf:
            - type: boolean
            - type: 'null'
          description: '`false` (or `null`, which is read as `false`) deactivates the endpoint; `true` reactivates it.'
        auth_token:
          oneOf:
            - type: string
            - type: 'null'
          description: At most 1024 UTF-8 bytes after trimming. Set to `null` or an empty string to clear the bearer token.
        payload_mode:
          type: string
          enum: [full, thin]
          description: Delivery body shape. Omit to keep the current mode; an empty value is rejected.

    RotateSecretResponse:
      type: object
      additionalProperties: false
      required: [id, secret, rotation_policy_id]
      properties:
        id:
          type: string
          format: uuid
          description: Endpoint identifier.
        secret:
          type: string
          description: New signing secret (`whsec_...`). Displayed only once.
        rotation_policy_id:
          type: string
          enum: [live-v1, sandbox-v1]
          description: '`live-v1` overlaps for 24 hours; `sandbox-v1` for five minutes.'
        previous_secret_expires_at:
          type: string
          format: date-time
          description: Exact instant when the previous secret stops being valid.

    # ── Deliveries ─────────────────────────────────────────────────────

    ListDeliveriesResponse:
      type: object
      additionalProperties: false
      required: [deliveries]
      properties:
        deliveries:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDelivery'
        next_cursor:
          type: string
          description: Pagination cursor. Omitted when there is no next page.

    WebhookDelivery:
      type: object
      additionalProperties: false
      required: [id, event_id, endpoint_id, run_id, event_type, webhook_version, target_url, status, attempt_count, max_attempts, next_attempt_at, next_attempt_trigger, created_at]
      properties:
        id:
          type: string
          format: uuid
          description: Delivery identifier.
        event_id:
          type: string
          format: uuid
          description: Deterministic event identifier.
        endpoint_id:
          type: string
          format: uuid
          description: Target endpoint identifier.
        run_id:
          type: string
          format: uuid
          description: Associated run identifier.
        event_type:
          type: string
          enum: [run.completed, run.failed, run.cancelled, run.superseded]
          description: Event type.
        webhook_version:
          type: string
          description: Payload version.
        target_url:
          type: string
          format: uri
          description: Snapshot of the endpoint URL at delivery creation time.
        status:
          type: string
          enum: [pending, in_progress, success, dead_letter, cancelled]
          description: Current delivery status.
        attempt_count:
          type: integer
          description: Number of delivery attempts made.
        max_attempts:
          type: integer
          description: Attempt limit for the current generation, from the delivery's retry policy (8 on standard-v3). A permanent failure dead-letters the delivery at that attempt without using the rest. On standard-v3 and standard-v2 a permanent failure is any status other than 2xx, 408, 425, 429 and 500 to 599; on legacy-v1 it is any status other than 2xx, 429 and 500 or above.
        next_attempt_at:
          type: string
          format: date-time
          description: Scheduled time for the next retry.
        next_attempt_trigger:
          type: string
          enum: [automatic, manual_resend]
          description: '`manual_resend` from a resend until an attempt in that generation schedules a retry; `automatic` otherwise.'
        last_attempt_at:
          type: string
          format: date-time
          description: Timestamp of the most recent attempt.
        last_status_code:
          type: integer
          description: HTTP status code from the most recent attempt.
        last_error:
          type: string
          description: Error message from the most recent failed attempt.
        created_at:
          type: string
          format: date-time
          description: When the delivery was created.
        completed_at:
          type: string
          format: date-time
          description: When the delivery reached a terminal state.

    WebhookDeliveryAttempt:
      type: object
      additionalProperties: false
      required: [id, resend_generation, attempt_ordinal, started_at, state]
      properties:
        id:
          type: string
          format: uuid
        resend_generation:
          type: integer
          minimum: 0
        attempt_ordinal:
          type: integer
          minimum: 1
        started_at:
          type: string
          format: date-time
        completed_at:
          type: string
          format: date-time
        state:
          type: string
          enum: [started, finalized, abandoned]
        outcome:
          type: string
          enum: [success, retry, dead_letter, permanent_failure, late_result_dropped]
          description: Result of this attempt. `permanent_failure` ends the delivery in `dead_letter` at once.
        status_code:
          type: integer
        status_class:
          type: string
        error_class:
          type: string
        duration_ms:
          type: integer
          format: int64
        next_retry_at:
          type: string
          format: date-time

    WebhookDeliveryDetail:
      type: object
      additionalProperties: false
      required: [delivery, attempts]
      properties:
        delivery:
          $ref: '#/components/schemas/WebhookDelivery'
        attempts:
          type: array
          items:
            $ref: '#/components/schemas/WebhookDeliveryAttempt'

    WebhookResendResponse:
      type: object
      additionalProperties: false
      required: [delivery_id, status, resend_generation]
      properties:
        delivery_id:
          type: string
          format: uuid
        status:
          type: string
          const: pending
        resend_generation:
          type: integer
          minimum: 1

    WebhookEvent:
      type: object
      additionalProperties: false
      required: [id, type, webhook_version, created_at, data]
      properties:
        id:
          type: string
          format: uuid
          description: Stable event ID, derived from the event type and the run ID. For `run.superseded` in the per-correction delivery setup it is derived from the correction instead.
        type:
          type: string
          enum: [run.completed, run.failed, run.cancelled, run.superseded]
          description: '`run.completed` covers both completed and degraded runs; read `data.run.status`. `run.superseded` is sent only when the engine''s configuration sets `notify_on_supersede: true`; which configuration Nexio reads depends on the organization''s delivery setup (see [run.superseded](/events/webhook-events#run-superseded)).'
        webhook_version:
          type: string
        created_at:
          type: string
          format: date-time
        data:
          type: object
          additionalProperties: false
          required: [run]
          properties:
            run:
              description: |
                The run's status fields, built from the stored run. Narrower
                than `GET /api/v1/runs/{run_id}`: never carries `output_phase`,
                `completed_deterministic_at`, `warnings`, `work_items`,
                `input`, `computed_at_head`, `served_head` or `stale`, and
                `output` is the stored output. On `run.superseded` in the
                per-correction delivery setup it is the corrected output and
                there are no `solutions`; in the per-run setup it is the
                original output with its `solutions`.
              $ref: '#/components/schemas/RunStatusResponse'
    HealthResponse:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [ok, degraded]
          description: '`degraded` means the database or the queue did not answer a check.'
        commit:
          type: string
          description: Git commit of the running build. Absent when the build is unstamped.

    EnvironmentInUseError:
      description: |
        The body of 409 `environment_in_use` on DELETE /api/v1/environments/{slug}.
        It extends `Error` with a top-level `blockers` object in place of `details`.
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          required: [blockers]
          properties:
            code:
              type: string
              const: environment_in_use
            blockers:
              type: object
              required: [api_keys, webhook_endpoints, runs]
              properties:
                api_keys:
                  type: integer
                  minimum: 0
                  description: API key rows that reference the environment, revoked keys included.
                webhook_endpoints:
                  type: integer
                  minimum: 0
                  description: Webhook endpoint rows that reference the environment, deleted endpoints included.
                runs:
                  type: integer
                  minimum: 0
                  description: Runs recorded in the environment.
    ConversationAttachmentReservation:
      type: object
      description: One reserved attachment record plus the write grant for its bytes.
      required: [attachment, upload_url, upload_url_expires_at]
      properties:
        attachment:
          $ref: '#/components/schemas/ConversationAttachment'
        upload_url:
          type: string
          format: uri
          description: |
            Presigned PUT URL for this one file. Send the raw bytes with no
            `Authorization` header. It cannot read and cannot reach any other
            file.
        upload_url_expires_at:
          type: string
          format: date-time
          description: When `upload_url` stops working, 15 minutes after it was issued.
        source_path:
          type: string
          description: For a folder member, the `path` your request sent for it, echoed verbatim. Absent on a standalone reservation.

    ConversationAttachmentFolderSkip:
      type: object
      description: One file in a folder that the instance will not read.
      required: [path, reason]
      properties:
        path:
          type: string
          description: The path as your request sent it.
        reason:
          type: string
          description: Why it was skipped, in words a person can act on.

    ConversationAttachmentFolder:
      type: object
      description: An opened folder container with one reservation per readable file.
      required: [container, reused, members, skipped]
      properties:
        container:
          $ref: '#/components/schemas/ConversationAttachment'
        reused:
          type: boolean
          description: True when a folder with the same roster (member paths and declared sizes; contents are not compared) was already open and ready and is handed back; `members` is empty and nothing needs uploading.
        members:
          type: array
          items:
            $ref: '#/components/schemas/ConversationAttachmentReservation'
        skipped:
          type: array
          description: Files the instance will not read. Always present; empty when nothing was skipped.
          items:
            $ref: '#/components/schemas/ConversationAttachmentFolderSkip'

    ConversationAttachmentPolicy:
      type: object
      description: The upload policy resolved from the instance's latest published config.
      required:
        - enabled
        - max_files_per_message
        - max_bytes_per_file
        - max_bytes_per_message
        - max_attachments_per_conversation
        - max_files_per_folder
        - unwrap_archives
        - accepted_extensions
        - retention_days
      properties:
        enabled:
          type: boolean
          description: Always true on a 200; a disabled instance answers `attachments_not_enabled`.
        max_files_per_message:
          type: integer
          description: Attachments one message may carry. A folder, zip, or email counts as one.
        max_bytes_per_file:
          type: integer
          format: int64
          description: Per-file limit in raw bytes. At most 39321600.
        max_bytes_per_message:
          type: integer
          format: int64
          description: Combined limit for one message, measured on the base64-encoded size (52428800).
        max_attachments_per_conversation:
          type: integer
          description: Live attachments one conversation may hold, container contents included (100).
        max_files_per_folder:
          type: integer
          description: Files one folder may carry (25).
        unwrap_archives:
          type: boolean
          description: Whether `.zip` and `.eml` containers are accepted and opened.
        accepted_extensions:
          type: array
          description: Accepted file extensions with the leading dot, sorted.
          items: {type: string}
        retention_days:
          type: [integer, 'null']
          description: Days files are kept; null follows the conversation's retention.

    ConversationEvalOnDemandRun:
      type: object
      description: One eval run, from a publish gate or started on demand.
      required: [id, suite, status, triggered_by, pass_count, fail_count, created_at]
      properties:
        id: {type: string, format: uuid}
        suite:
          type: string
          enum: [gate, workflows, adversarial, smoke]
        status:
          type: string
          enum: [running, passed, failed, waived, error]
          description: |
            `running` while queued or executing. On an on-demand run, `passed`
            when every scenario passed and `failed` when at least one failed.
            On a publish gate run, `passed` means no regression against the
            prior version's run (a first gate run, or failures that were
            already failing, still pass; read `fail_count`), and `failed`
            means a scenario newly fails or a new scenario fails. `waived` for a publish
            gate run whose regression was waived; `error` when the run cannot
            complete: at once for a permanent failure (the instance, its
            archived configuration, a stored scenario, or the eval executor is
            unavailable or unreadable), or after 5 attempts when execution
            failed transiently each time.
        triggered_by:
          type: string
          enum: [publish, manual, schedule, event]
        version:
          type: string
          description: The released instance version the run measured, as an integer string (for example `"4"`). Absent when not recorded.
        pass_count: {type: integer}
        fail_count: {type: integer}
        error:
          type: string
          description: Why the run ended in `error`. Absent otherwise.
        created_at: {type: string, format: date-time}
    ConversationEvalResult:
      type: object
      description: One scenario's verdict inside an eval run.
      required: [scenario_id, scenario_name, passed, scores]
      properties:
        scenario_id: {type: string, format: uuid}
        scenario_name: {type: string}
        passed: {type: boolean}
        scores:
          type: object
          description: Deterministic check verdicts plus judge scores.
          properties:
            checks:
              type: array
              items:
                type: object
                required: [check, passed]
                properties:
                  check:
                    type: string
                    description: The check name, for example `must_refuse:no-legal-advice` or `must_not_refuse`.
                  passed: {type: boolean}
                  detail: {type: string}
            judge:
              type: object
              additionalProperties: {type: number}
              description: Judge dimension name to score. Recorded only; never decides pass or fail.
            judge_error:
              type: string
              description: Why the judge could not score, when it failed.

    ConversationEvalOnDemandRunDetail:
      type: object
      description: One eval run with its per-scenario results.
      required: [run, results]
      properties:
        run:
          $ref: '#/components/schemas/ConversationEvalOnDemandRun'
        results:
          type: array
          items:
            $ref: '#/components/schemas/ConversationEvalResult'

    ConversationEvalRunDiff:
      type: object
      description: A run compared with a baseline run, by scenario, reported as scenario names.
      required: [run_id, baseline_run_id, diff]
      properties:
        run_id: {type: string, format: uuid}
        baseline_run_id: {type: string, format: uuid}
        diff:
          type: object
          required: [newly_failing, newly_passing, still_failing, still_passing, added, removed]
          properties:
            newly_failing:
              type: array
              items: {type: string}
              description: Passed in the baseline, fail now.
            newly_passing:
              type: array
              items: {type: string}
              description: Failed in the baseline, pass now.
            still_failing:
              type: array
              items: {type: string}
            still_passing:
              type: array
              items: {type: string}
            added:
              type: array
              items: {type: string}
              description: In this run, not in the baseline.
            removed:
              type: array
              items: {type: string}
              description: In the baseline, not in this run.
    IngestEventEnvelope:
      type: object
      description: |
        The body a `nexio` ingest source posts. Nexio sets the organization,
        `produced_by: ingest`, and `transition_cause: world_change`; a sender
        cannot set them. Unknown top-level fields are ignored.
      required: [type, subject, body]
      properties:
        type:
          type: string
          description: Event type. Must be one of the source's configured types and not a reserved type.
        subject:
          type: string
          minLength: 1
          description: What the event is about, for example `account/ACC-10442`. Must not be blank.
        body:
          type: object
          description: A JSON object of IDs and changed fields.
        occurred_at:
          type: string
          format: date-time
          description: When the change happened. Defaults to the time Nexio received the request.
        dedupe_key:
          type: string
          description: The event's identity within its type and subject. Defaults to the `X-Nexio-Delivery` value.

    IngestDeliveryResponse:
      type: object
      additionalProperties: false
      description: The result of one accepted ingest delivery.
      required: [delivery_id, outcome, inserted]
      properties:
        delivery_id:
          type: string
          description: The delivery's identity. The `X-Nexio-Delivery` value for a `nexio` source, the `X-GitHub-Delivery` value for a `github` source, and `sha256:` followed by the body's SHA-256 hex for an `ams360_ons` source.
        outcome:
          type: string
          enum: [appended, ignored]
          description: '`appended`: an event is in the log. `ignored`: valid, but no event was produced; see `reason`.'
        reason:
          type: string
          enum: [event_not_translated, status_not_configured]
          description: Present when `outcome` is `ignored`.
        event_id:
          type: string
          format: uuid
          description: The event's ID when `outcome` is `appended`. If an event with the same identity already existed, its ID.
        inserted:
          type: boolean
          description: '`false` when this delivery ID and body were already recorded. Nothing new happened.'

    RecordsReadSource:
      type: object
      properties:
        mode:
          type: string
          description: 'How the connection is served: `query_first`, which means data reads go live to the warehouse at request time. On `GET /api/v1/records/status` this is a receipt made at request time: `current` is `true`, `fetched_at` is the request time, and `freshness` is absent.'
        current:
          type: boolean
        freshness:
          type: string
          description: Not sent on this route.
        fetched_at:
          type: string
          format: date-time
        stale_since:
          type: string
          format: date-time
      required:
        - mode
        - fetched_at
    RecordsScope:
      type: object
      properties:
        kind:
          type: string
          enum:
            - Self
            - Office
            - All
            - Platform
          description: The resolved row scope.
        selection:
          type: string
          enum:
            - own
            - client_manager
            - code
            - boundary
            - whole
            - account
            - office
          description: The selection the rows were served under.
        selection_source:
          type: string
          enum:
            - request
            - rls
            - none
          description: Who chose the selection.
      required:
        - kind
        - selection
        - selection_source
      description: Appended to Records read envelopes, except `GET /records/actions`, the `/records/analyses` routes and `GET /records/status`, which carries its own scope block. States the scope the rows were served at.
    RecordsPage:
      type: object
      properties:
        limit:
          type: integer
          description: The number of rows on this page (not the requested limit).
        next_cursor:
          type:
            - string
            - 'null'
          description: Continuation cursor; null on the last page.
        offset:
          type: integer
          description: The applied offset. Present only when the request sent `offset`.
        page_size:
          type: integer
          description: The applied page size. Present only when the request sent `offset`.
      required:
        - limit
        - next_cursor
    RecordsFamilyNotice:
      type: object
      properties:
        code:
          type: string
        message:
          type: string
      required:
        - code
        - message
      description: A family refused by design for this caller, for example `org_scoped_only`.
    RecordsFamilyServing:
      type: object
      properties:
        family:
          type: string
        plane:
          type: string
        presence:
          type: string
          enum:
            - present
            - absent
            - unknown
          description: '`present`: the family exists on the connection. `absent`: it was never supplied (render absence, not "no records"). `unknown`: it could not be determined on this read.'
        batch_set_id:
          type:
            - string
            - 'null'
        published_at:
          type:
            - string
            - 'null'
          format: date-time
        gap:
          type: string
          description: A named reason the family is incomplete, when one applies.
        notice:
          $ref: '#/components/schemas/RecordsFamilyNotice'
      required:
        - family
        - plane
        - presence
        - batch_set_id
        - published_at
    RecordsActionBasis:
      type: object
      properties:
        batch_set_id:
          type: string
        overlay_rev:
          type: integer
        seq:
          type: integer
          description: The ledger position the person was looking at (a previous `action_seq`).
      description: What the person was looking at when the command was issued. Recorded and compared on replay; never a reason to refuse. Missing numbers are recorded as 0.
    RecordsActionCommand:
      type: object
      properties:
        id:
          type: string
          description: Optional command id (UUID). Generated when omitted; becomes the note or task id on a create.
        command:
          type: string
          enum:
            - note.create
            - note.edit
            - note.delete
            - task.create
            - task.edit
            - task.assign
            - task.complete
            - task.reopen
            - task.delete
            - status.set
            - source_activity.create
            - renewal_decision.set
          description: '`source_activity.create` and `renewal_decision.set` always refuse the batch with 403 `overlay_read_only`.'
        schema_rev:
          type: integer
          enum: [1]
          description: Always 1. Any other value answers 400 `action_schema_unknown`.
        client_key:
          type: string
          description: Target record by client key (a top-level record of the system of record). Send exactly one of `client_key`, `policy_key`, or `catalog_entity_type` with `catalog_id` (400 `invalid_request` otherwise).
        policy_key:
          type: string
          description: Target record by policy key.
        catalog_entity_type:
          type: string
          description: Target Catalog entity type (notes only), with `catalog_id`.
        catalog_id:
          type: string
        payload:
          type: object
          additionalProperties: true
          description: Command payload, validated strictly, at most 32 KiB. See the writes guide for each command's fields.
        basis:
          $ref: '#/components/schemas/RecordsActionBasis'
        idempotency_key:
          type: string
          description: A UUID unique across the organization's ledger. Resending it with the identical command replays the original result.
        issued_at:
          type: string
          format: date-time
      required:
        - command
        - schema_rev
        - payload
        - idempotency_key
    RecordsActionsAppendRequest:
      type: object
      properties:
        actor_principal:
          type: string
          description: The person the commands are for. Must equal `X-Nexio-Acting-Principal` when that header is sent.
        actor_type:
          type: string
          enum:
            - producer
            - agent
          description: '`producer` for a person, `agent` for an AI agent acting for that person.'
        commands:
          type: array
          minItems: 1
          maxItems: 20
          items:
            $ref: '#/components/schemas/RecordsActionCommand'
      required:
        - actor_principal
        - actor_type
        - commands
    RecordsActionResult:
      type: object
      properties:
        command_id:
          type: string
        seq:
          type: integer
        idempotent_replay:
          type: boolean
          description: Present and true only when this command was a replay of an earlier identical command; absent otherwise.
      required:
        - command_id
        - seq
    RecordsActionsAppendResponse:
      type: object
      properties:
        duration_ms:
          type: integer
        results:
          type: array
          items:
            $ref: '#/components/schemas/RecordsActionResult'
        action_seq:
          type: integer
          description: The ledger's latest sequence number.
        serving:
          allOf:
            - $ref: '#/components/schemas/RecordsServing'
          description: Absent when every command addresses a Catalog entity.
        scope:
          $ref: '#/components/schemas/RecordsScope'
      required:
        - duration_ms
        - results
        - action_seq
        - scope
    RecordsLedgerRow:
      type: object
      properties:
        seq:
          type: integer
        command_id:
          type: string
        actor_principal:
          type: string
        actor_type:
          type: string
        entity_domain:
          type: string
        connection_id:
          type: string
        client_key:
          type: string
        ams360_datasource:
          type: string
          description: The source system's tenant key for the record (a field of the source system).
        policy_id:
          type: string
        app_entity_type:
          type: string
        app_entity_id:
          type: string
        catalog_entity_type:
          type: string
        catalog_id:
          type: string
        command:
          type: string
        schema_rev:
          type: integer
        payload:
          type: object
          additionalProperties: true
        issued_at:
          type: string
          format: date-time
        recorded_at:
          type: string
          format: date-time
      required:
        - seq
        - command_id
        - actor_principal
        - actor_type
        - entity_domain
        - command
        - schema_rev
        - payload
        - recorded_at
    RecordsLedgerResponse:
      type: object
      properties:
        serving:
          $ref: '#/components/schemas/RecordsServing'
        duration_ms:
          type: integer
        data:
          type: array
          items:
            $ref: '#/components/schemas/RecordsLedgerRow'
        next_seq:
          type:
            - integer
            - 'null'
          description: Pass back as `since_seq` for the next page. Null when the page was not full; a full last page still carries a value, and the next call returns no rows.
        action_seq:
          type: integer
        limit:
          type: integer
          description: Rows returned on this page.
      required:
        - serving
        - duration_ms
        - data
        - next_seq
        - action_seq
        - limit
    RecordsNote:
      type: object
      properties:
        id:
          type: string
        entity_type:
          type: string
          enum:
            - client
            - policy
        client_key:
          type: string
        ams360_datasource:
          type: string
          description: The source system's tenant key for the record (a field of the source system).
        policy_id:
          type: string
        author_principal:
          type: string
        body:
          type: string
        attach_status:
          type: string
          enum:
            - attached
            - detached
        applied_over_concurrent:
          type: boolean
          description: True when a later command already changed the same target.
        applied_seq:
          type: integer
        issued_at:
          type: string
          format: date-time
      required:
        - id
        - entity_type
        - author_principal
        - body
        - attach_status
        - applied_seq
    RecordsTask:
      type: object
      properties:
        id:
          type: string
        entity_type:
          type: string
          enum:
            - client
            - policy
        client_key:
          type: string
        ams360_datasource:
          type: string
          description: The source system's tenant key for the record (a field of the source system).
        policy_id:
          type: string
        author_principal:
          type: string
        assignee_principal:
          type: string
        title:
          type: string
        due:
          type: string
          format: date-time
          description: The due date, sent as midnight UTC on that date.
        status:
          type: string
          enum:
            - open
            - completed
        attach_status:
          type: string
          enum:
            - attached
            - detached
        applied_over_concurrent:
          type: boolean
          description: True when a later command already changed the same target.
        applied_seq:
          type: integer
        issued_at:
          type: string
          format: date-time
      required:
        - id
        - entity_type
        - author_principal
        - title
        - status
        - attach_status
        - applied_seq
    RecordsNoteListResponse:
      type: object
      properties:
        duration_ms:
          type: integer
        data:
          type: array
          items:
            $ref: '#/components/schemas/RecordsNote'
        action_seq:
          type: integer
        serving:
          allOf:
            - $ref: '#/components/schemas/RecordsServing'
          description: The connection the rows were read from (`binding_id`). On this route `as_of` and `source.fetched_at` are not a read time and are sent as `0001-01-01T00:00:00Z`.
        scope:
          $ref: '#/components/schemas/RecordsScope'
      required:
        - duration_ms
        - data
        - action_seq
        - serving
        - scope
    RecordsTaskListResponse:
      type: object
      properties:
        duration_ms:
          type: integer
        data:
          type: array
          items:
            $ref: '#/components/schemas/RecordsTask'
        action_seq:
          type: integer
        serving:
          allOf:
            - $ref: '#/components/schemas/RecordsServing'
          description: The connection the rows were read from (`binding_id`). On this route `as_of` and `source.fetched_at` are not a read time and are sent as `0001-01-01T00:00:00Z`.
        scope:
          $ref: '#/components/schemas/RecordsScope'
      required:
        - duration_ms
        - data
        - action_seq
        - serving
        - scope
    RecordsStatusServing:
      type: object
      properties:
        binding_id:
          type: string
        overlay_rev:
          type: integer
        as_of:
          type: string
          format: date-time
        read_pin:
          type:
            - string
            - 'null'
          format: date-time
          description: Always null on this route.
        source:
          $ref: '#/components/schemas/RecordsReadSource'
        lens:
          type: object
          additionalProperties: false
          required: [target, display_name, scope_kind]
          properties:
            target: {type: string}
            display_name: {type: string}
            scope_kind: {type: string}
      required:
        - binding_id
        - overlay_rev
        - as_of
        - read_pin
        - source
    RecordsStatusScope:
      type: object
      properties:
        kind:
          type: string
          description: '`Self`, `Office`, `All` or `Platform`; empty when refused.'
        principal:
          type: string
        refused:
          type: string
          enum:
            - identity_unmapped
            - identity_needs_review
            - identity_suspended
            - identity_stale
            - scope_unavailable
          description: Set when the person cannot be served. The status read still answers 200.
        producer_code_count:
          type: integer
        selection:
          type: string
          description: The selection the request's own `mine`, `book`, `producer`, `client_manager` or `account` parameters would serve on a read across the whole scope (a label preview; no rows are narrowed). Omitted when the scope is refused or those parameters are malformed.
        selection_source:
          type: string
          description: Who chose `selection` (`request`, `rls` or `none`). Omitted with `selection`.
        home_market:
          type:
            - string
            - 'null'
        home_office:
          type:
            - string
            - 'null'
        home_office_label:
          type:
            - string
            - 'null'
        home_status:
          type: string
          enum:
            - ok
            - none
            - unavailable
      required:
        - kind
        - home_market
        - home_office
        - home_office_label
        - home_status
    RecordsReadPath:
      type: object
      properties:
        state:
          type: string
        code:
          type:
            - string
            - 'null'
          description: The refusal code paired with this state; null when available.
      required:
        - state
        - code
    RecordsStatusReads:
      type: object
      properties:
        account_register:
          allOf:
            - $ref: '#/components/schemas/RecordsReadPath'
          description: State `available`, `scope_unsupported` or `scope_unavailable`. Advisory. The account register route does not check it and serves every resolved scope, so its `code` is not a refusal the register returns today.
        producer_licensure:
          allOf:
            - $ref: '#/components/schemas/RecordsReadPath'
          description: State `available`, `not_derived` or `version_mismatch`.
      required:
        - account_register
        - producer_licensure
    RecordsDerivedSet:
      type: object
      properties:
        found:
          type: boolean
        batch_set_id:
          type: string
        behind_head:
          type: boolean
        pin_at:
          type: string
          format: date-time
        published_at:
          type: string
          format: date-time
        note:
          type: string
      required:
        - found
        - behind_head
    RecordsStatusDerived:
      type: object
      properties:
        risk_profile:
          $ref: '#/components/schemas/RecordsDerivedSet'
        org_evidence:
          $ref: '#/components/schemas/RecordsDerivedSet'
        org_retention:
          $ref: '#/components/schemas/RecordsDerivedSet'
      required:
        - risk_profile
        - org_evidence
        - org_retention
    RecordsStatusResponse:
      type: object
      properties:
        has_published:
          type: boolean
          description: False when no qualifying connection exists; every other read would answer 409 `book_unavailable`.
        status:
          type: 'null'
          description: Always null today.
        serving:
          description: The serving block. Null when no qualifying connection exists (`has_published` is false).
          anyOf:
            - $ref: '#/components/schemas/RecordsStatusServing'
            - type: 'null'
        duration_ms:
          type: integer
        read_modes:
          type: object
          additionalProperties:
            type: string
          description: Each read surface and how it is served (`query_first`).
        scope:
          $ref: '#/components/schemas/RecordsStatusScope'
        reads:
          $ref: '#/components/schemas/RecordsStatusReads'
        ams360_base_url:
          type: string
          description: Base URL for links into the source system, when configured. Omitted when not set.
        entitlement:
          type: object
          additionalProperties: true
          description: The caller's access-plane entitlement. Present when the organization's posture is `shadow` or `lit` for this person; with no qualifying connection, also for a registered service identity. It carries posture, roles, surfaces, datasets, workflows, denied fields, documents, grants and record scope, plus `related_books`, `team`, `policy_source` and `vocabulary_hash`. Lists and maps are always sent, as empty values when there is nothing in them.
        derived:
          $ref: '#/components/schemas/RecordsStatusDerived'
      required:
        - has_published
        - status
        - serving
        - duration_ms
    RecordsGenericFamilyResponse:
      type: object
      properties:
        plane:
          type: string
        family:
          type: string
        grain:
          type: string
          enum:
            - policy
            - client
            - table
            - org
        columns:
          type: array
          items:
            type: string
          description: Column names, sorted.
        data:
          type: array
          items:
            type: array
            items: {}
          description: One array per row, aligned with `columns`. Numbers are exact decimal text.
        page:
          $ref: '#/components/schemas/RecordsPage'
        serving:
          $ref: '#/components/schemas/RecordsServing'
        family_serving:
          $ref: '#/components/schemas/RecordsFamilyServing'
        scope:
          $ref: '#/components/schemas/RecordsScope'
      required:
        - plane
        - family
        - grain
        - columns
        - data
        - page
        - serving
        - family_serving
        - scope
    GraphNode:
      type: object
      properties:
        kind:
          type: string
          enum:
            - connection
            - derivation
        key:
          type: string
          description: '`<kind>:<id segments>`.'
        label:
          type: string
        summary:
          type: string
        lane:
          type: string
          enum:
            - sources
            - derivations
          description: '`sources` for a connection, `derivations` for a derivation.'
        counts:
          type: object
          description: 'Empty for a connection. For a derivation: `activeSchedules`, `pausedSchedules` and `connections`.'
          additionalProperties:
            type: integer
        freshness:
          type:
            - string
            - 'null'
        status:
          type: string
          description: 'For a connection, its registry status. For a derivation, `active` (an active schedule), `paused` (only paused schedules) or `failed` (a retained run for a scheduled connection did not succeed).'
        expandable:
          type: boolean
          description: Always false.
      required:
        - kind
        - key
        - label
        - summary
        - lane
        - counts
        - freshness
        - status
        - expandable
    GraphEdgeEvidence:
      type: object
      properties:
        statement:
          type: string
      required:
        - statement
      description: 'Evidence behind an edge. Only `statement` is sent.'
    GraphEdge:
      type: object
      properties:
        id:
          type: string
        source:
          type: string
        target:
          type: string
        kind:
          type: string
          enum:
            - feeds
          description: A connection feeds a derivation it holds a publication schedule for.
        evidence:
          $ref: '#/components/schemas/GraphEdgeEvidence'
        observed:
          type: boolean
          description: 'Always false: every edge comes from a configured schedule.'
      required:
        - id
        - source
        - target
        - kind
        - evidence
        - observed
    GraphFinding:
      type: object
      properties:
        key:
          type: string
        kind:
          type: string
        statement:
          type: string
        counts:
          type: object
          additionalProperties:
            type: integer
        nodeKeys:
          type: array
          items:
            type: string
      required:
        - key
        - kind
        - statement
        - counts
        - nodeKeys
    GraphOrg:
      type: object
      properties:
        id:
          type: string
      required:
        - id
    GraphResponse:
      type: object
      properties:
        org:
          $ref: '#/components/schemas/GraphOrg'
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/GraphNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/GraphEdge'
        findings:
          type: array
          description: Always an empty array.
          items:
            $ref: '#/components/schemas/GraphFinding'
        generatedAt:
          type: string
          description: Millisecond RFC 3339 with Z.
      required:
        - org
        - nodes
        - edges
        - findings
        - generatedAt
    GraphNodesResponse:
      type: object
      properties:
        org:
          $ref: '#/components/schemas/GraphOrg'
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/GraphNode'
        generatedAt:
          type: string
      required:
        - org
        - nodes
        - generatedAt
    GraphNodeResponse:
      type: object
      properties:
        org:
          $ref: '#/components/schemas/GraphOrg'
        node:
          $ref: '#/components/schemas/GraphNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/GraphEdge'
        generatedAt:
          type: string
      required:
        - org
        - node
        - edges
        - generatedAt
  headers:
    RetryAfter:
      description: Whole seconds to wait before retrying, at least 1.
      schema:
        type: integer
        minimum: 1
