openapi: 3.1.0
info:
  title: Zespan Public API
  version: 1.0.0
  description: >
    The Zespan Public API covers the endpoints that customers call directly or
    through the Zespan SDKs: trace ingestion (native and OpenTelemetry), prompt
    management, datasets and dataset runs, and the runtime guardrails check.

    All endpoints authenticate with a project API key sent in the `x-api-key`
    header. Create and manage API keys from the project settings in the Zespan
    dashboard.
servers:
  - url: https://api.zespan.com
    description: Zespan production API
security:
  - ApiKeyAuth: []
tags:
  - name: Ingestion
    description: Send traces and events to Zespan.
  - name: OpenTelemetry
    description: OTLP-compatible ingestion endpoints.
  - name: Prompts
    description: Manage versioned prompts and their labels, tags, and folders.
  - name: Datasets
    description: Read datasets and manage dataset runs used for experiments and scoring.
  - name: Guardrails
    description: Runtime guardrail evaluation.
  - name: SDK / CLI support
    description: >
      Small support endpoints consumed by the SDKs and @zespan/cli rather than
      called directly by application code.
  - name: Blast Radius
    description: >
      The prompt/agent/model/policy/evaluator/alert dependency graph backing the
      pre-release impact check and the evaluator-delete gate in the dashboard.
      Session-authenticated (dashboard cookie), not `x-api-key`.
  - name: Outcomes
    description: >
      Report business outcomes (a deflected ticket, an avoided refund, an SLA
      met) attributed to a trace, and read them back summarized by agent or
      model, joined to real trace cost. Backs the Value dashboard page. The
      ingest endpoint is `x-api-key`-authenticated like the rest of ingestion;
      the two read endpoints are session-authenticated (dashboard cookie) like
      Blast Radius.
  - name: Compliance
    description: >
      Generate audit-ready evidence documents (a per-agent Compliance Card, or
      SOC 2 control evidence) from recorded platform data, and re-verify a
      generated document's citations against live data. Session-authenticated
      (dashboard cookie), not `x-api-key`, gated by `compliance:read` /
      `compliance:generate` permissions and the Pro plan or above (the framework
      listing is the one exception — no project scope and no plan gate, since a
      customer deciding whether to upgrade needs to see what they'd get).
  - name: Models
    description: >
      Per-model usage, cost, latency, and error-rate rollups for a project,
      including the lifecycle overlay described under the Model Lifecycle tag.
      Session-authenticated (dashboard cookie), not `x-api-key`, gated by
      `dashboard:read`.
  - name: Model Lifecycle
    description: >
      Findings from the daily model deprecation scan, which matches models a
      project actually calls against a curated, bundled catalogue of
      provider-announced deprecation and retirement dates. Every figure on a
      finding (call volume, cost, affected agents/prompts, cost comparison
      against a named successor) is measured from real trace data — there is no
      quality-delta or regression-comparison endpoint, because nothing in this
      API invokes a model on the caller's behalf. Session-authenticated
      (dashboard cookie), not `x-api-key`: reading findings and the catalogue
      requires `dashboard:read`, dismissing a finding requires `alerts:manage`.
paths:
  /v1/ingest:
    post:
      operationId: ingestEvents
      tags:
        - Ingestion
      summary: Ingest trace events
      description: >
        Ingest a batch of trace/span events. The request body is
        newline-delimited JSON (`application/x-ndjson`): one JSON event object
        per line. A maximum of 100 events per request and a 1 MB body size limit
        apply. Events are validated and queued asynchronously; invalid lines are
        skipped rather than failing the whole batch.


        Authenticated with `x-api-key`. Rate limited to 300 requests/minute per
        API key.
      requestBody:
        required: true
        content:
          application/x-ndjson:
            schema:
              type: string
              description: |
                Newline-delimited JSON. Each line is a single event object.
              example: >
                {"trace_id":"3b1c...","span_id":"a91f...","name":"chat.completions","provider":"openai","model":"gpt-4o","operation":"chat.completions","input_tokens":812,"output_tokens":146,"latency_ms":1240,"status":"success","timestamp":"2026-07-05T12:00:00.000Z"}

                {"trace_id":"3b1c...","span_id":"c72d...","parent_span_id":"a91f...","name":"vector.search","operation":"retrieval.query","latency_ms":54,"status":"success","timestamp":"2026-07-05T12:00:00.050Z"}
      responses:
        '202':
          description: Batch accepted for asynchronous processing.
          content:
            application/json:
              schema:
                type: object
                properties:
                  accepted:
                    type: integer
                    description: Number of events accepted from the batch.
                    example: 2
                  cv:
                    type: integer
                    description: >-
                      Current project config version (used by SDKs for config
                      propagation).
                    example: 7
                required:
                  - accepted
                  - cv
        '400':
          description: No valid events found, or more than 100 events in the batch.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: >-
            Rate limit or trace quota exceeded. Includes a `Retry-After` header
            for rate limiting.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: >-
            Service temporarily unavailable (ingest hot path dependency down).
            Retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/traces:
    post:
      operationId: otelTraces
      tags:
        - OpenTelemetry
      summary: Export OTLP traces
      description: >
        OTLP-compatible trace export endpoint. Accepts an
        `ExportTraceServiceRequest` payload as JSON (`application/json`) or
        protobuf (`application/x-protobuf`). Spans are converted to Zespan
        events and queued asynchronously.


        Authenticated with `x-api-key`. Rate limited to 300 requests/minute per
        API key, capped at 1 MB and 100 spans per request, and counted against
        the organization's monthly event quota. Spans beyond the 100-span cap
        are reported in `partialSuccess.rejectedSpans`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/OtelExportTraceServiceRequest'
          application/x-protobuf:
            schema:
              type: string
              format: binary
              description: Protobuf-encoded ExportTraceServiceRequest.
      responses:
        '202':
          description: Spans accepted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OtelTracePartialSuccess'
        '400':
          description: Invalid trace data.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '413':
          description: Request body exceeded 1 MB.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded. Includes a `Retry-After` header.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: >
            Quota state could not be determined for a Free-plan organization.
            The request was not accepted; retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/metrics:
    post:
      operationId: otelMetrics
      tags:
        - OpenTelemetry
      summary: Export OTLP metrics (not implemented)
      description: >
        Not implemented. This endpoint authenticates the API key and returns
        `501`; no metrics are stored. Send traces to `/v1/traces` instead.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: OTLP ExportMetricsServiceRequest payload.
          application/x-protobuf:
            schema:
              type: string
              format: binary
      responses:
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '501':
          description: OTLP metrics ingestion is not implemented.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/logs:
    post:
      operationId: otelLogs
      tags:
        - OpenTelemetry
      summary: Export OTLP logs (not implemented)
      description: >
        Not implemented. This endpoint authenticates the API key and returns
        `501`; no logs are stored. Send traces to `/v1/traces` instead.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
              description: OTLP ExportLogsServiceRequest payload.
          application/x-protobuf:
            schema:
              type: string
              format: binary
      responses:
        '401':
          description: Missing or invalid API key.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '501':
          description: OTLP logs ingestion is not implemented.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/prompts:
    get:
      operationId: listPrompts
      tags:
        - Prompts
      summary: List prompts
      description: >
        List prompts for the authenticated project. When using an API key the
        project is inferred from the key, so `projectId` is optional.
      parameters:
        - name: name
          in: query
          required: false
          schema:
            type: string
          description: Filter to a single prompt family by name.
        - $ref: '#/components/parameters/ProjectIdQuery'
      responses:
        '200':
          description: List of prompts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  prompts:
                    type: array
                    items:
                      $ref: '#/components/schemas/Prompt'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      operationId: createPrompt
      tags:
        - Prompts
      summary: Create a prompt version
      description: >
        Create a new version of a prompt. If the named prompt already exists a
        new incremented version is created; otherwise version 1 is created. The
        `latest` label is always applied automatically.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
                - type
                - prompt
              properties:
                projectId:
                  type: string
                  format: uuid
                  description: Optional when authenticating with an API key.
                name:
                  type: string
                  minLength: 1
                  example: support-reply
                type:
                  type: string
                  enum:
                    - text
                    - chat
                  example: chat
                prompt:
                  description: >-
                    Prompt content. A string for `text` prompts, or an array of
                    chat messages for `chat` prompts.
                  oneOf:
                    - type: string
                    - type: array
                      items:
                        type: object
                        additionalProperties: true
                  example:
                    - role: system
                      content: You are a concise support assistant.
                    - role: user
                      content: '{{question}}'
                config:
                  type: object
                  additionalProperties: true
                  description: Model configuration (temperature, max_tokens, etc.).
                  example:
                    temperature: 0.2
                    max_tokens: 512
                labels:
                  type: array
                  items:
                    type: string
                  example:
                    - production
                tags:
                  type: array
                  items:
                    type: string
                  example:
                    - support
                commitMessage:
                  type: string
                  example: Tighten system instructions
                folder:
                  type: string
                  nullable: true
                  example: support/inbound
      responses:
        '201':
          description: Prompt version created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prompt'
        '400':
          description: Validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/prompts/folders:
    get:
      operationId: listPromptFolders
      tags:
        - Prompts
      summary: List prompt folders
      description: List the distinct folder paths used across the project's prompts.
      parameters:
        - $ref: '#/components/parameters/ProjectIdQuery'
      responses:
        '200':
          description: List of folder paths.
          content:
            application/json:
              schema:
                type: object
                properties:
                  folders:
                    type: array
                    items:
                      type: string
                    example:
                      - support/inbound
                      - checkout/onboarding
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/prompts/{name}:
    get:
      operationId: getPrompt
      tags:
        - Prompts
      summary: Get a prompt
      description: >
        Fetch a prompt by name. Without `version` or `label` the latest version
        is returned. The response includes `resolvedPrompt` with prompt
        dependencies inlined.
      parameters:
        - $ref: '#/components/parameters/PromptName'
        - name: version
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
          description: Fetch a specific version.
        - name: label
          in: query
          required: false
          schema:
            type: string
          description: Fetch the version currently carrying this label (e.g. `production`).
        - $ref: '#/components/parameters/ProjectIdQuery'
      responses:
        '200':
          description: The resolved prompt.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prompt'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/prompts/{name}/versions:
    get:
      operationId: listPromptVersions
      tags:
        - Prompts
      summary: List prompt versions
      description: List every version of a prompt family, newest first.
      parameters:
        - $ref: '#/components/parameters/PromptName'
        - $ref: '#/components/parameters/ProjectIdQuery'
      responses:
        '200':
          description: List of versions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  versions:
                    type: array
                    items:
                      $ref: '#/components/schemas/Prompt'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/prompts/{name}/versions/{version}/labels:
    patch:
      operationId: setPromptVersionLabels
      tags:
        - Prompts
      summary: Set labels on a prompt version
      description: >
        Replace the set of labels on a specific prompt version. Assigning the
        `production` label promotes that version and triggers a background
        quality regression check.
      parameters:
        - $ref: '#/components/parameters/PromptName'
        - name: version
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
          description: Version number.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - labels
              properties:
                labels:
                  type: array
                  items:
                    type: string
                  example:
                    - production
                projectId:
                  type: string
                  format: uuid
                  description: Optional when authenticating with an API key.
      responses:
        '200':
          description: Updated prompt version.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Prompt'
        '400':
          description: Validation error (e.g. protected-label conflict).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/prompts/{name}/tags:
    patch:
      operationId: setPromptTags
      tags:
        - Prompts
      summary: Set tags on a prompt family
      description: Replace the set of tags across all versions of a prompt family.
      parameters:
        - $ref: '#/components/parameters/PromptName'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - tags
              properties:
                tags:
                  type: array
                  maxItems: 50
                  items:
                    type: string
                  example:
                    - support
                    - inbound
                projectId:
                  type: string
                  format: uuid
                  description: Optional when authenticating with an API key.
      responses:
        '200':
          description: Updated versions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  versions:
                    type: array
                    items:
                      $ref: '#/components/schemas/Prompt'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/prompts/{name}/folder:
    patch:
      operationId: movePromptToFolder
      tags:
        - Prompts
      summary: Move a prompt to a folder
      description: >
        Move a prompt family into a folder path, or to the root by passing
        `null`. The folder is applied to every version of the named prompt.
      parameters:
        - $ref: '#/components/parameters/PromptName'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - folder
              properties:
                folder:
                  type: string
                  nullable: true
                  description: Folder path, or `null` for the root.
                  example: support/inbound
                projectId:
                  type: string
                  format: uuid
                  description: Optional when authenticating with an API key.
      responses:
        '200':
          description: Move result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  folder:
                    type: string
                    nullable: true
                    example: support/inbound
                  updatedCount:
                    type: integer
                    example: 3
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/by-name/{name}:
    get:
      operationId: getDatasetByName
      tags:
        - Datasets
      summary: Resolve a dataset by name
      description: >
        Resolve a dataset id from its name within the authenticated project.
        This is the SDK's entry point, since customer code refers to datasets by
        name.
      parameters:
        - name: name
          in: path
          required: true
          schema:
            type: string
            minLength: 1
          description: Dataset name.
        - $ref: '#/components/parameters/ProjectIdQuery'
      responses:
        '200':
          description: The resolved dataset.
          content:
            application/json:
              schema:
                type: object
                properties:
                  dataset:
                    type: object
                    properties:
                      id:
                        type: string
                        format: uuid
                      name:
                        type: string
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/{datasetId}/items:
    get:
      operationId: listDatasetItems
      tags:
        - Datasets
      summary: List dataset items
      description: List the items in a dataset (up to 1000), oldest first.
      parameters:
        - $ref: '#/components/parameters/DatasetId'
        - $ref: '#/components/parameters/ProjectIdQuery'
      responses:
        '200':
          description: The dataset's items.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/DatasetItem'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/{datasetId}/runs:
    get:
      operationId: listDatasetRuns
      tags:
        - Datasets
      summary: List dataset runs
      description: >
        List runs for a dataset (newest first), each enriched with its latest
        scoring status and average score if it has been scored.
      parameters:
        - $ref: '#/components/parameters/DatasetId'
        - $ref: '#/components/parameters/ProjectIdQuery'
      responses:
        '200':
          description: Dataset runs.
          content:
            application/json:
              schema:
                type: object
                properties:
                  runs:
                    type: array
                    items:
                      $ref: '#/components/schemas/DatasetRunEnriched'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    post:
      operationId: createDatasetRun
      tags:
        - Datasets
      summary: Create or fetch a dataset run
      description: >
        Create a named run for a dataset, or return the existing run with that
        name. Idempotent: the SDK calls this every time a job starts.
      parameters:
        - $ref: '#/components/parameters/DatasetId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - name
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 120
                  example: nightly-eval-2026-07-05
                description:
                  type: string
                  maxLength: 500
                projectId:
                  type: string
                  format: uuid
                  description: Optional when authenticating with an API key.
      responses:
        '201':
          description: The created or existing run.
          content:
            application/json:
              schema:
                type: object
                properties:
                  run:
                    $ref: '#/components/schemas/DatasetRun'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/{datasetId}/runs/compare:
    get:
      operationId: compareDatasetRuns
      tags:
        - Datasets
      summary: Compare two dataset runs
      description: >
        Compare two runs of a dataset. Returns each dataset item shared by both
        runs with each run's trace id and score side by side, plus each run's
        average score over the shared items.
      parameters:
        - $ref: '#/components/parameters/DatasetId'
        - name: a
          in: query
          required: true
          schema:
            type: string
            format: uuid
          description: First run id.
        - name: b
          in: query
          required: true
          schema:
            type: string
            format: uuid
          description: Second run id.
        - $ref: '#/components/parameters/ProjectIdQuery'
      responses:
        '200':
          description: Comparison result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  runA:
                    $ref: '#/components/schemas/DatasetRunCompareSide'
                  runB:
                    $ref: '#/components/schemas/DatasetRunCompareSide'
                  rows:
                    type: array
                    items:
                      type: object
                      properties:
                        datasetItemId:
                          type: string
                          format: uuid
                        input: {}
                        a:
                          $ref: '#/components/schemas/DatasetRunCompareCell'
                        b:
                          $ref: '#/components/schemas/DatasetRunCompareCell'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/{datasetId}/runs/{runId}:
    get:
      operationId: getDatasetRun
      tags:
        - Datasets
      summary: Get dataset run detail
      description: >
        Fetch a run with every linked item joined to its dataset item content
        and, if the run has been scored, its evaluation result.
      parameters:
        - $ref: '#/components/parameters/DatasetId'
        - $ref: '#/components/parameters/RunId'
        - $ref: '#/components/parameters/ProjectIdQuery'
      responses:
        '200':
          description: Run detail.
          content:
            application/json:
              schema:
                type: object
                properties:
                  run:
                    $ref: '#/components/schemas/DatasetRun'
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/DatasetRunItemEnriched'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/{datasetId}/runs/{runId}/items:
    post:
      operationId: linkDatasetRunItem
      tags:
        - Datasets
      summary: Link an item to a dataset run
      description: >
        Link a dataset item to a run by recording the trace produced for it.
        Idempotent: re-linking updates the stored trace pointer.
      parameters:
        - $ref: '#/components/parameters/DatasetId'
        - $ref: '#/components/parameters/RunId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - datasetItemId
                - traceId
              properties:
                datasetItemId:
                  type: string
                  format: uuid
                traceId:
                  type: string
                  minLength: 1
                  description: OpenTelemetry trace id for the run over this item.
                observationId:
                  type: string
                  minLength: 1
                  description: Optional span/observation id within the trace.
                projectId:
                  type: string
                  format: uuid
                  description: Optional when authenticating with an API key.
      responses:
        '201':
          description: The linked run item.
          content:
            application/json:
              schema:
                type: object
                properties:
                  item:
                    $ref: '#/components/schemas/DatasetRunItem'
        '400':
          description: The dataset item does not belong to this dataset.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/{datasetId}/runs/{runId}/score:
    post:
      operationId: scoreDatasetRun
      tags:
        - Datasets
      summary: Score a dataset run
      description: >
        Trigger scoring of a run's linked traces with an evaluator. Creates a
        fresh evaluation run over the run's trace ids and enqueues it for
        asynchronous scoring.
      parameters:
        - $ref: '#/components/parameters/DatasetId'
        - $ref: '#/components/parameters/RunId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - evaluatorId
              properties:
                evaluatorId:
                  type: string
                  format: uuid
                  description: Evaluator definition to score with.
                projectId:
                  type: string
                  format: uuid
                  description: Optional when authenticating with an API key.
      responses:
        '202':
          description: Scoring enqueued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  evaluationRun:
                    $ref: '#/components/schemas/EvaluationRun'
        '400':
          description: Invalid evaluator, or the run has no linked items to score.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/datasets/{datasetId}/runs/http-target:
    post:
      operationId: runDatasetOverHttpTarget
      tags:
        - Datasets
      summary: Run a dataset against a registered HTTP Target
      description: >
        Start a Zespan-executed run: for every item in the dataset, Zespan
        hydrates the target's request template with the item's `input` and POSTs
        it directly to the registered HTTP Target's endpoint, capturing the raw
        response as a trace tagged `sdk_name: "zespan-http-endpoint"`. Unlike
        every other dataset-run endpoint on this page, Zespan itself makes the
        call — no SDK integration on the target's side is required.


        The target itself is created, updated, and deleted from the dashboard
        (**Project Settings → HTTP Targets**), not through this public API — see
        [HTTP Targets](/dashboard/http-targets).


        The run is created (or re-attached to, if `runName` matches an existing
        run) and its execution is enqueued asynchronously; poll `GET
        /v1/datasets/{datasetId}/runs/{runId}` for progress and to see each item
        link as it completes.
      parameters:
        - $ref: '#/components/parameters/DatasetId'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - httpTargetId
              properties:
                httpTargetId:
                  type: string
                  format: uuid
                  description: Id of a registered HTTP Target in this project.
                runName:
                  type: string
                  minLength: 1
                  maxLength: 120
                  description: >
                    Optional run name. Reusing an existing run's name
                    re-attaches to it instead of creating a duplicate. Defaults
                    to an auto-generated `http-target-<id>` name.
                projectId:
                  type: string
                  format: uuid
                  description: Optional when authenticating with an API key.
      responses:
        '202':
          description: >-
            Run created and execution enqueued (or created but not enqueued —
            see `note`).
          content:
            application/json:
              schema:
                type: object
                properties:
                  runId:
                    type: string
                    format: uuid
                  name:
                    type: string
                  status:
                    type: string
                    example: queued
                  total:
                    type: integer
                    description: Number of dataset items this run will process.
                  note:
                    type: string
                    description: >
                      Present only when the execution queue was unavailable —
                      the run was created but not enqueued.
        '400':
          description: The dataset has no items to run against.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: >-
            Dataset not found, or `httpTargetId` does not resolve to a target in
            this project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/guardrails/check:
    post:
      operationId: guardrailsCheck
      tags:
        - Guardrails
      summary: Run a guardrails check
      description: >
        Evaluate text against the authenticated project's enabled guardrails at
        runtime. Returns whether the text is allowed, the per-guardrail results,
        and any modified (e.g. redacted) text. Requires `x-api-key`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - text
                - phase
              properties:
                text:
                  type: string
                  minLength: 1
                  maxLength: 50000
                  example: My card number is 4111 1111 1111 1111.
                phase:
                  type: string
                  enum:
                    - pre
                    - post
                  description: Whether this is checking input (`pre`) or output (`post`).
                traceId:
                  type: string
                spanId:
                  type: string
                model:
                  type: string
                  example: gpt-4o
                operation:
                  type: string
                estimatedCost:
                  type: number
                inputTokens:
                  type: integer
                agentName:
                  type: string
                  maxLength: 200
                toolName:
                  type: string
                  maxLength: 200
                recentToolCalls:
                  type: array
                  maxItems: 100
                  items:
                    type: object
                    properties:
                      toolName:
                        type: string
                        maxLength: 200
                      args:
                        type: string
                        maxLength: 10000
      responses:
        '200':
          description: Guardrail evaluation result.
          content:
            application/json:
              schema:
                type: object
                properties:
                  allowed:
                    type: boolean
                    example: false
                  results:
                    type: array
                    items:
                      $ref: '#/components/schemas/GuardrailResult'
                  modifiedText:
                    type: string
                    nullable: true
                    example: My card number is [REDACTED].
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/sdk/whoami:
    get:
      operationId: sdkWhoami
      tags:
        - SDK / CLI support
      summary: Resolve project identity from an API key
      description: >
        Answers "which project does this API key belong to?" — the one thing
        `GET /v1/sdk/config` can't answer, since that route requires the caller
        to already know the `projectId` and rejects a mismatch. This is `zespan
        doctor`'s first call, and how it establishes project identity before
        anything else in the doctor run.


        Authenticated with `x-api-key`, same as every other endpoint on this
        page. Returns identity and the project's resolved SDK config only —
        never a secret: no API key hash, no raw key, no provider credentials.
      responses:
        '200':
          description: Project identity and resolved SDK config.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SdkWhoamiResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /v1/projects/{id}/ingest-health:
    get:
      operationId: getIngestHealth
      tags:
        - SDK / CLI support
      summary: Check whether spans are actually arriving for a project
      description: >
        Reports the most recent span timestamp, a 24h span count, and any
        recorded ingest rejections for a project — the data `zespan doctor`'s
        Data flow check reads to tell "your integration is broken" apart from
        "this project hasn't sent its first trace yet."


        `lastSpanAt` is bounded to a 90-day lookback (so the underlying
        ClickHouse query can prune partitions on large, long-lived projects). A
        project whose most recent span is older than that window reports
        `lastSpanAt: null` — identically to a project that has never sent
        anything. The two cases are not distinguishable from this response
        alone.


        `rejections` is currently always an empty array: nothing in the ingest
        path persists rejection reasons yet, so the field is shipped honestly
        empty rather than backed by invented bookkeeping.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`** — unlike every other endpoint on this page. It requires
        the `dashboard:read` permission on the project's organization. A request
        carrying only a project API key does not satisfy this and receives
        `403`.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
      responses:
        '200':
          description: Ingest health for the project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestHealthResponse'
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `dashboard:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/projects/{id}/blast-radius:
    get:
      operationId: getBlastRadius
      tags:
        - Blast Radius
      summary: What breaks if this resource changes
      description: >
        Walks the dependency graph from one root node and returns every
        dependent found (breadth-first, bounded by `maxDepth`/`maxNodes`),
        summarized into an impact rollup. Backs the pre-release impact card on
        the Prompts page and the delete-blocking check on Evaluations.


        Dependency direction: an edge's `to` depends on its `from` — walking
        dependents from a node answers "what would be affected if I changed
        this," which is the reverse of the call direction for prompt/model edges
        (an agent *calls* a prompt or model, but the *prompt or model change* is
        what affects the agent, so the edge points prompt→agent / model→agent).


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires the `dashboard:read` permission. There is no
        server-side cache in front of this endpoint today — every request
        rebuilds the graph, so `computedAt` in the response is effectively
        "now."
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: node
          in: query
          required: true
          schema:
            type: string
          description: >
            The root node id, `<kind>:<key>` — e.g. `prompt:support-reply` or
            `evaluator:3f9c2b1a-...`. Keys are names for `prompt`/`agent`/
            `model`, UUIDs for row-backed kinds (`policy`, `evaluator`,
            `dataset`, `alert`). Always passed as a query parameter, never a
            path segment, since a key can itself contain a colon or slash.
          example: prompt:support-reply
        - name: maxDepth
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 5
            default: 3
          description: Maximum hops from the root to traverse.
        - name: windowDays
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 90
            default: 30
          description: Lookback window for observed (ClickHouse) call volume and cost.
        - name: maxNodes
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 2000
            default: 500
          description: Maximum total dependents returned before truncating.
      responses:
        '200':
          description: The root node, its dependents, and the summarized impact.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlastRadiusResponse'
        '400':
          description: |
            Malformed `node` — missing the `<kind>:<key>` separator.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: >-
                      Malformed node id "support-reply" — expected
                      "<kind>:<key>"
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `dashboard:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: >
            `node` is well-formed but doesn't exist in this project's graph —
            including a well-formed id that belongs to a different project. The
            two cases are indistinguishable in the response on purpose, so a
            cross-project id can't be used to probe for a resource's existence.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Node "prompt:does-not-exist" not found in this project
  /v1/projects/{id}/graph:
    get:
      operationId: getBlastRadiusGraph
      tags:
        - Blast Radius
      summary: The raw dependency graph for a project
      description: >
        Returns every node and edge in the project's dependency graph,
        optionally filtered to a subset of node kinds — the same graph `GET
        /v1/projects/{id}/blast-radius` traverses from a single root, but
        unfiltered by root. Sized for a future graph visualization; nothing in
        the dashboard renders it today.


        Authenticated the same way as `GET /v1/projects/{id}/blast-radius`
        (dashboard session, `dashboard:read`).
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: kinds
          in: query
          schema:
            type: string
          description: >
            Comma-separated node kinds to include (e.g. `prompt,agent`). Omit to
            return every kind.
          example: prompt,agent
        - name: windowDays
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 90
            default: 30
          description: Lookback window for observed (ClickHouse) call volume and cost.
      responses:
        '200':
          description: The full (optionally kind-filtered) graph.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BlastRadiusGraphResponse'
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `dashboard:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/ingest/outcomes:
    post:
      operationId: ingestOutcomes
      tags:
        - Outcomes
      summary: Report business outcomes attributed to traces
      description: >
        Report a batch of business outcomes (a deflected ticket, an avoided
        refund, an SLA met) attributed to traces your SDK already emitted. This
        is a direct, synchronous write to ClickHouse — not routed through the
        same async queue as `POST /v1/ingest` — since it's a single low-volume
        out-of-band call rather than a batched stream of trace events.


        The trace an outcome names does not need to exist in Zespan yet.
        Outcomes and traces are joined at query time (`GET
        /v1/projects/{id}/outcomes/summary`), not at write time, so an outcome
        that arrives before its trace has finished ingesting is stored and
        joined correctly once the trace shows up. This is the expected shape,
        not an edge case: an outcome is usually known minutes or hours after the
        trace that produced it has already finished, from a customer's own
        backend (a support-desk webhook, a billing reconciliation job) — not
        from the same process that ran the agent.


        Re-reporting the same `(kind, traceId)` pair is how you correct an
        earlier outcome — for example, a ticket that reopens after you'd already
        reported `ticket_deflected: true`. It is not an update to the earlier
        row; it's a new one, and the most recently ingested value is what reads
        (including the summary endpoint below) return.


        **Authenticated with `x-api-key`**, the same as the rest of ingestion —
        this route is never reachable with a dashboard session.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                outcomes:
                  type: array
                  minItems: 1
                  maxItems: 100
                  description: >
                    Up to 100 outcomes per request — the same per-batch cap
                    `POST /v1/ingest` uses for native SDK events, reused rather
                    than inventing a second limit for a second ingest endpoint.
                  items:
                    $ref: '#/components/schemas/Outcome'
              required:
                - outcomes
      responses:
        '201':
          description: Batch accepted and written.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutcomeIngestResponse'
        '400':
          description: >
            Validation error — e.g. more than 100 outcomes in one batch, an
            empty `outcomes` array, or an item failing its own field validation
            (`kind` over 100 characters, missing `traceId`, etc.).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: >
            Missing or invalid API key. Unlike the generic `Error` shape used
            elsewhere on this page, this route's 401 body is `{ "message": "API
            key required" }`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: API key required
  /v1/projects/{id}/outcomes/summary:
    get:
      operationId: getOutcomeSummary
      tags:
        - Outcomes
      summary: Outcome summary by agent or model, joined to cost
      description: >
        Returns one row per distinct value of `dimension` (agent name or model
        name), summarizing every outcome reported in `[now - range, now]`: total
        outcomes, successes, total value, and the real LLM cost of the traces
        those outcomes are attributed to, plus two derived ratios
        (`costPerSuccess`, `valuePerDollar`). Backs the Value dashboard page's
        stat bar, chart, and breakdown table.


        Cost is joined at read time from `llm_events` and is **not** windowed by
        `range` — a trace's cost is fixed at ingestion and never corrected,
        unlike an outcome, so only which outcomes count toward a row is affected
        by the time window; the cost attributed to those outcomes' traces is
        not.


        A trace touching more than one model has its outcome attributed to an
        arbitrary one of them when `dimension=model` — outcomes are recorded at
        the trace level, not the span level, so a genuinely multi-model trace's
        outcome is not fanned out across every model it used.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires the `dashboard:read` permission — the same
        auth model as Blast Radius.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: range
          in: query
          schema:
            type: string
            enum:
              - 24h
              - 7d
              - 30d
              - 90d
            default: 30d
          description: Lookback window for which outcomes are counted.
        - name: dimension
          in: query
          schema:
            type: string
            enum:
              - agent
              - model
            default: agent
          description: Which dimension to group rows by.
      responses:
        '200':
          description: Outcome summary rows for the project, grouped by `dimension`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutcomeSummaryResponse'
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `dashboard:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/projects/{id}/outcomes/kinds:
    get:
      operationId: getOutcomeKinds
      tags:
        - Outcomes
      summary: Distinct outcome kinds reported for a project
      description: >
        Returns the distinct `kind` values reported for this project in `[now -
        range, now]`, sorted alphabetically. Used to populate kind filters in
        the dashboard.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires the `dashboard:read` permission — the same
        auth model as Blast Radius.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: range
          in: query
          schema:
            type: string
            enum:
              - 24h
              - 7d
              - 30d
              - 90d
            default: 30d
          description: Lookback window for which outcomes are scanned.
      responses:
        '200':
          description: Distinct outcome kinds reported in the window.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/OutcomeKindsResponse'
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `dashboard:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/compliance/frameworks:
    get:
      operationId: listComplianceFrameworks
      tags:
        - Compliance
      summary: List available compliance frameworks
      description: >
        A static capability listing — the frameworks Zespan can map evidence to
        today, each with its controls. `soc2` is the only registered framework
        as of this release; EU AI Act and ISO/IEC 42001 are not yet available.
        Not project-scoped and not plan-gated: a customer deciding whether to
        upgrade needs to see what they'd get before they commit.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires the `compliance:read` permission.
      security: []
      responses:
        '200':
          description: The registered frameworks and their controls.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComplianceFrameworksResponse'
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `compliance:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/projects/{id}/evidence-packs:
    post:
      operationId: generateEvidencePack
      tags:
        - Compliance
      summary: Generate an evidence pack
      description: >
        Creates an `EvidencePack` row in `pending` status and enqueues
        generation on a background worker — this endpoint returns immediately,
        before the document exists. Poll `GET
        /v1/projects/{id}/evidence-packs/{packId}` (or list packs) to watch
        `status` move through `processing` to `completed` (or `failed`).


        `kind: "control_evidence"` requires `framework`; `kind: "agent_card"`
        ignores it. `periodStart` must be before `periodEnd`, and the period may
        not exceed 400 days. There is no `"pdf"` value for `format` — this
        deployment has no headless-browser rendering path, so a PDF request is
        rejected here rather than silently downgraded to HTML.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires both the `compliance:generate` permission
        (owner/admin only — editor, viewer, and billing roles can read and
        download but not generate) and the Pro plan or above.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GenerateEvidencePackRequest'
      responses:
        '202':
          description: >
            The pack row was created and generation was enqueued. The document
            itself is not ready yet — `status` is `pending`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GenerateEvidencePackResponse'
        '400':
          description: >
            `periodStart` is not before `periodEnd`, the period exceeds 400
            days, `framework` is missing for `kind: "control_evidence"`, or
            another body validation error.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: >
            Not authenticated with a dashboard session, lacks
            `compliance:generate`, or the organization's plan is below Pro
            (`code: "PLAN_REQUIRED"` in the error body for the plan case).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '503':
          description: >-
            Evidence pack generation is temporarily unavailable (queue not wired
            up).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    get:
      operationId: listEvidencePacks
      tags:
        - Compliance
      summary: List evidence packs for a project
      description: >
        Paginated list, newest first. `evidenceIndex` (the citation list) is
        deliberately excluded from every row here — it can hold hundreds of
        entries per pack and this view never renders them; fetch a single pack
        to read it.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires `compliance:read` plus the Pro plan or
        above.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: kind
          in: query
          schema:
            type: string
            enum:
              - agent_card
              - control_evidence
        - name: framework
          in: query
          schema:
            type: string
            enum:
              - soc2
        - name: status
          in: query
          schema:
            type: string
            enum:
              - pending
              - processing
              - completed
              - failed
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: pageSize
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
      responses:
        '200':
          description: A page of evidence packs.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EvidencePackListResponse'
        '403':
          description: >-
            Not authenticated with a dashboard session, lacks `compliance:read`,
            or plan below Pro.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/projects/{id}/evidence-packs/{packId}:
    get:
      operationId: getEvidencePack
      tags:
        - Compliance
      summary: Get one evidence pack, optionally its content
      description: >
        Returns the pack row plus, once `status` is `completed`, either a
        presigned `downloadUrl` (object-storage backend) or the raw `content`
        string (local-disk backend, when `download=true` is passed) — never
        both. A pack belonging to a different project than `id` returns `404`,
        the same non-enumerable behavior used elsewhere in the API for
        cross-project access.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires `compliance:read` plus the Pro plan or
        above.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: packId
          in: path
          required: true
          schema:
            type: string
          description: Evidence pack id.
        - name: download
          in: query
          schema:
            type: boolean
            default: false
          description: >
            When true and the pack is stored on the local-disk backend (no
            presigned URL available), returns the document's raw content inline
            instead of just metadata.
      responses:
        '200':
          description: The pack, and its content or download URL if completed.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/EvidencePackDetailResponse'
        '403':
          description: >-
            Not authenticated with a dashboard session, lacks `compliance:read`,
            or plan below Pro.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/projects/{id}/evidence-packs/{packId}/verify:
    get:
      operationId: verifyEvidencePack
      tags:
        - Compliance
      summary: Re-verify a generated evidence pack
      description: >
        Re-hashes the stored document and re-resolves every citation it made
        against live data, scoped to the pack's project or organization. Never
        errors on a data condition — a deleted record, a deleted storage object,
        or a pack that never finished generating are all reported as *results*,
        not exceptions. See
        [Verification](https://docs.zespan.com/compliance/verification) for the
        full field reference, in particular what `unverifiableRefs` means and
        why it must be read alongside `evidenceStillPresent` rather than
        ignored.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires `compliance:read` plus the Pro plan or
        above.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: packId
          in: path
          required: true
          schema:
            type: string
          description: Evidence pack id.
      responses:
        '200':
          description: The verification result.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VerifyResult'
        '403':
          description: >-
            Not authenticated with a dashboard session, lacks `compliance:read`,
            or plan below Pro.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          $ref: '#/components/responses/NotFound'
  /v1/projects/{id}/compliance/coverage:
    get:
      operationId: getComplianceCoverage
      tags:
        - Compliance
      summary: Coverage preview for a framework and period, without generating
      description: >
        Runs the same evidence queries a control-evidence document would for the
        given framework and period, and reports per-control record counts —
        without rendering or storing anything. This is what the Compliance page
        calls before you commit to generating a pack, so a gap in the underlying
        data is visible before a document leaves the building, not after.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires `compliance:read` plus the Pro plan or
        above.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: framework
          in: query
          required: true
          schema:
            type: string
            enum:
              - soc2
        - name: periodStart
          in: query
          required: true
          schema:
            type: string
            format: date-time
        - name: periodEnd
          in: query
          required: true
          schema:
            type: string
            format: date-time
      responses:
        '200':
          description: Per-control coverage for the requested period.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ComplianceCoverageResponse'
        '400':
          description: '`periodStart` is not before `periodEnd`.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: >-
            Not authenticated with a dashboard session, lacks `compliance:read`,
            or plan below Pro.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/projects/{id}/models:
    get:
      operationId: getModels
      tags:
        - Models
      summary: Per-model usage, cost, latency, and error rate
      description: >
        Aggregates every model called in the project over the selected range,
        ranked by the chosen sort. Backs the Models dashboard page's table.


        Each row also carries a `lifecycle` overlay: `null` when the model has
        neither a catalogue entry nor an open finding, otherwise the same
        countdown/urgency fields as a [Model Lifecycle](#tag/Model-Lifecycle)
        finding, joined from the project's `ModelLifecycleFinding` rows (open
        findings) and the global `ModelLifecycle` catalogue (dates for models
        that haven't produced a finding yet, e.g. because the retirement is
        further out than the detection horizon).


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires the `dashboard:read` permission.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: range
          in: query
          schema:
            type: string
            enum:
              - 7d
              - 30d
              - 90d
            default: 30d
        - name: sort
          in: query
          schema:
            type: string
            enum:
              - cost
              - calls
              - errors
              - latency
            default: cost
      responses:
        '200':
          description: One row per model called in range.
          content:
            application/json:
              schema:
                type: object
                properties:
                  models:
                    type: array
                    items:
                      $ref: '#/components/schemas/ModelUsageRow'
                required:
                  - models
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `dashboard:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/projects/{id}/model-lifecycle:
    get:
      operationId: getModelLifecycleFindings
      tags:
        - Model Lifecycle
      summary: List model deprecation findings for a project
      description: >
        Returns findings from the daily deprecation scan for this project,
        soonest deadline first (an already-retired model, with a negative
        `daysRemaining`, sorts to the very top). Backs the Model Lifecycle
        dashboard page, the Overview widget, and the Models page banner.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires the `dashboard:read` permission.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: status
          in: query
          schema:
            type: string
            enum:
              - open
              - dismissed
              - all
            default: open
      responses:
        '200':
          description: Up to 200 findings, ordered by urgency then call volume.
          content:
            application/json:
              schema:
                type: object
                properties:
                  findings:
                    type: array
                    items:
                      $ref: '#/components/schemas/ModelLifecycleFinding'
                required:
                  - findings
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `dashboard:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/projects/{id}/model-lifecycle/{findingId}/dismiss:
    post:
      operationId: dismissModelLifecycleFinding
      tags:
        - Model Lifecycle
      summary: Dismiss a model deprecation finding
      description: >
        Suppresses a finding at its current urgency band. It reopens
        automatically — exactly once, and notifies again — the next time the
        daily scan finds the band has tightened (90 → 30 → 7 days, or into
        retired). A band that widens, which only happens when a feed correction
        pushes the retirement date further out, never reopens a dismissal.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires the `alerts:manage` permission — dismissing
        a finding is the same shape of decision as editing an alert rule.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
        - name: findingId
          in: path
          required: true
          schema:
            type: string
          description: The finding's id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  minLength: 1
                  maxLength: 500
                  description: >
                    Required. A dismissal with no stated reason is exactly what
                    a later reviewer needs and can't reconstruct.
              required:
                - reason
      responses:
        '200':
          description: The updated, now-dismissed finding.
          content:
            application/json:
              schema:
                type: object
                properties:
                  finding:
                    $ref: '#/components/schemas/ModelLifecycleFinding'
                required:
                  - finding
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `alerts:manage`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: No finding with this id in this project.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/projects/{id}/model-lifecycle/alert-rule:
    get:
      operationId: getModelLifecycleAlertRule
      tags:
        - Model Lifecycle
      summary: Get the project's model-lifecycle notification settings
      description: >
        Returns the project's opt-in alert rule for Model Lifecycle findings, or
        `null` if the project has never opted in — absence is a valid, expected
        state (a `null` rule), not a `404`. Backs the notification settings card
        on the [Model Lifecycle](/dashboard/model-lifecycle) page.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires the `dashboard:read` permission — same as
        the findings list above.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
      responses:
        '200':
          description: The current rule, or `null` if the project has never opted in.
          content:
            application/json:
              schema:
                type: object
                properties:
                  rule:
                    allOf:
                      - $ref: '#/components/schemas/ModelLifecycleAlertRule'
                    nullable: true
                required:
                  - rule
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `dashboard:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    put:
      operationId: putModelLifecycleAlertRule
      tags:
        - Model Lifecycle
      summary: Create or update the project's model-lifecycle notification settings
      description: >
        Idempotent upsert: creates the rule on the first call, updates it on
        every later call (there is no separate create/delete — toggling the
        card's switch off and saving again just sets `enabled: false` on the
        same row). Fields omitted from the request body are left untouched
        rather than cleared, so turning notifications off doesn't discard a
        saved email list or webhook you'll want back the next time you turn them
        on. `notifyWebhook` is validated against the same SSRF-safety guard as
        every other alert-rule webhook URL in the platform — a URL that resolves
        to a private or internal address is rejected with `400`.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires the `alerts:manage` permission — same as
        every other alert-rule write in this API.
      security: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Project id.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled:
                  type: boolean
                notifyEmails:
                  type: array
                  maxItems: 20
                  items:
                    type: string
                    format: email
                notifyWebhook:
                  type: string
                  nullable: true
              required:
                - enabled
      responses:
        '200':
          description: The rule after the upsert.
          content:
            application/json:
              schema:
                type: object
                properties:
                  rule:
                    $ref: '#/components/schemas/ModelLifecycleAlertRule'
                required:
                  - rule
        '400':
          description: >
            Invalid request body, or `notifyWebhook` resolves to a private or
            internal address.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `alerts:manage`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
  /v1/model-catalogue:
    get:
      operationId: getModelCatalogue
      tags:
        - Model Lifecycle
      summary: The full bundled model lifecycle feed
      description: >
        Returns Zespan's curated, bundled catalogue of provider-announced
        deprecation and retirement dates in full — not scoped to a project,
        since the catalogue is the same for every tenant. This is the raw data
        [Model Lifecycle](/dashboard/model-lifecycle) findings and the Models
        page's Lifecycle column are matched against. See the [feed
        reference](/reference/model-lifecycle-feed) for the field semantics.


        **Authenticated with a dashboard session (browser cookie), not
        `x-api-key`**, and requires the `dashboard:read` permission — not
        project-scoped, but still member-gated, since the curation is part of
        what a paying customer gets.
      security: []
      responses:
        '200':
          description: The full bundled feed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  feedVersion:
                    type: string
                    description: Release identifier, e.g. `2026-08-09`.
                  checkedAt:
                    type: string
                    description: >
                      `YYYY-MM-DD` — when a human last verified every entry
                      against its `sourceUrl`.
                  entries:
                    type: array
                    items:
                      $ref: '#/components/schemas/ModelLifecycleFeedEntry'
                required:
                  - feedVersion
                  - checkedAt
                  - entries
        '403':
          description: >-
            Not authenticated with a dashboard session, or lacks
            `dashboard:read`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: >-
        Project API key. Manage keys in the Zespan dashboard under project
        settings.
  parameters:
    ProjectIdQuery:
      name: projectId
      in: query
      required: false
      schema:
        type: string
        format: uuid
      description: >
        Project id. Optional and ignored when authenticating with an API key,
        since the key already scopes the request to one project.
    PromptName:
      name: name
      in: path
      required: true
      schema:
        type: string
      description: Prompt name.
    DatasetId:
      name: datasetId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Dataset id.
    RunId:
      name: runId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Dataset run id.
  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NotFound:
      description: Resource not found.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          description: Human-readable error message.
          example: Unauthorized
        code:
          type: string
          description: Machine-readable error code, when present.
          example: rate_limit_exceeded
      required:
        - error
    SdkWhoamiResponse:
      type: object
      description: Project identity and resolved SDK config for the authenticated API key.
      properties:
        projectId:
          type: string
          format: uuid
        projectName:
          type: string
          example: Checkout Agent
        orgId:
          type: string
          format: uuid
        configVersion:
          description: >-
            Monotonic version of the resolved SDK config, as an integer or
            string depending on how it was stored.
          oneOf:
            - type: integer
            - type: string
          example: 4
        config:
          type: object
          description: >
            The project's resolved SDK config — a free-form, server-pushed
            settings bag — plus two fields the CLI's `zespan doctor` reads
            directly: `pii` (the project's actual redaction policy) and
            `latestSdkVersion` (omitted entirely, not sent as null, when it
            can't be resolved).
          properties:
            pii:
              type: object
              properties:
                enabled:
                  type: boolean
                preset:
                  type: string
                  example: gdpr
            latestSdkVersion:
              type: string
              example: 1.8.2
          additionalProperties: true
      required:
        - projectId
        - projectName
        - orgId
        - configVersion
        - config
    IngestHealthRejection:
      type: object
      properties:
        reason:
          type: string
          example: quota_exceeded
        count:
          type: integer
          example: 12
        lastAt:
          type: string
          format: date-time
    IngestHealthResponse:
      type: object
      properties:
        lastSpanAt:
          type: string
          format: date-time
          nullable: true
          description: >
            Timestamp of the most recent span in the last 90 days, or `null` if
            none — which covers both a project that has never sent a span and
            one whose last span is older than the 90-day lookback window.
        spanCount24h:
          type: integer
          example: 214
        rejections:
          type: array
          items:
            $ref: '#/components/schemas/IngestHealthRejection'
          description: Currently always empty — see the endpoint description.
      required:
        - lastSpanAt
        - spanCount24h
        - rejections
    Prompt:
      type: object
      description: A single prompt version.
      properties:
        id:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        name:
          type: string
          example: support-reply
        version:
          type: integer
          example: 3
        type:
          type: string
          enum:
            - text
            - chat
        prompt:
          description: >-
            Prompt content — a string for text prompts, or an array of chat
            messages.
          oneOf:
            - type: string
            - type: array
              items:
                type: object
                additionalProperties: true
        config:
          type: object
          additionalProperties: true
        labels:
          type: array
          items:
            type: string
          example:
            - production
            - latest
        tags:
          type: array
          items:
            type: string
        folder:
          type: string
          nullable: true
        commitMessage:
          type: string
          nullable: true
        createdBy:
          type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        resolvedPrompt:
          description: >-
            Prompt content with dependencies inlined (present on single-prompt
            fetch).
          nullable: true
    DatasetItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        datasetId:
          type: string
          format: uuid
        input:
          description: Item input (arbitrary JSON).
        expectedOutput:
          description: Optional expected output (arbitrary JSON).
          nullable: true
        metadata:
          type: object
          additionalProperties: true
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    DatasetRun:
      type: object
      properties:
        id:
          type: string
          format: uuid
        datasetId:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        latestEvaluationRunId:
          type: string
          format: uuid
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
        _count:
          type: object
          properties:
            items:
              type: integer
    DatasetRunEnriched:
      allOf:
        - $ref: '#/components/schemas/DatasetRun'
        - type: object
          properties:
            scoring:
              type: object
              nullable: true
              properties:
                status:
                  type: string
                  example: completed
                avgScore:
                  type: number
                  nullable: true
                  example: 0.82
    DatasetRunItem:
      type: object
      properties:
        id:
          type: string
          format: uuid
        datasetRunId:
          type: string
          format: uuid
        datasetItemId:
          type: string
          format: uuid
        traceId:
          type: string
        observationId:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    DatasetRunItemEnriched:
      allOf:
        - $ref: '#/components/schemas/DatasetRunItem'
        - type: object
          properties:
            datasetItem:
              type: object
              nullable: true
              properties:
                id:
                  type: string
                  format: uuid
                input: {}
                expectedOutput:
                  nullable: true
            result:
              type: object
              nullable: true
              description: Evaluation result for this item's trace, if scored.
    DatasetRunCompareSide:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        avgScore:
          type: number
          nullable: true
    DatasetRunCompareCell:
      type: object
      properties:
        traceId:
          type: string
        score:
          type: number
          nullable: true
    EvaluationRun:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        projectId:
          type: string
          format: uuid
        evaluatorId:
          type: string
          format: uuid
        datasetId:
          type: string
          format: uuid
        traceIds:
          type: array
          items:
            type: string
        status:
          type: string
          example: pending
        createdAt:
          type: string
          format: date-time
    GuardrailResult:
      type: object
      properties:
        guardrailSlug:
          type: string
          example: pii-detection
        passed:
          type: boolean
          example: false
        action:
          type: string
          enum:
            - allowed
            - blocked
            - redacted
            - warned
        reason:
          type: string
          nullable: true
          example: Detected credit card number
        modifiedText:
          type: string
          nullable: true
        latencyMs:
          type: integer
          example: 12
    OtelTracePartialSuccess:
      type: object
      properties:
        partialSuccess:
          type: object
          properties:
            rejectedSpans:
              type: integer
              example: 0
            errorMessage:
              type: string
              example: ''
    OtelExportTraceServiceRequest:
      type: object
      description: OTLP ExportTraceServiceRequest (JSON encoding).
      properties:
        resourceSpans:
          type: array
          items:
            type: object
            properties:
              resource:
                type: object
                properties:
                  attributes:
                    type: array
                    items:
                      $ref: '#/components/schemas/OtelKeyValue'
              scopeSpans:
                type: array
                items:
                  type: object
                  properties:
                    scope:
                      type: object
                      properties:
                        name:
                          type: string
                        version:
                          type: string
                    spans:
                      type: array
                      items:
                        $ref: '#/components/schemas/OtelSpan'
    OtelSpan:
      type: object
      required:
        - traceId
        - spanId
        - name
        - startTimeUnixNano
      properties:
        traceId:
          type: string
        spanId:
          type: string
        parentSpanId:
          type: string
        name:
          type: string
          example: chat.completions
        kind:
          type: integer
        startTimeUnixNano:
          type: string
          example: '1751716800000000000'
        endTimeUnixNano:
          type: string
        attributes:
          type: array
          items:
            $ref: '#/components/schemas/OtelKeyValue'
        status:
          type: object
          properties:
            code:
              type: integer
            message:
              type: string
    OtelKeyValue:
      type: object
      properties:
        key:
          type: string
          example: gen_ai.system
        value:
          type: object
          description: 'OTLP AnyValue (e.g. `{ "stringValue": "openai" }`).'
          additionalProperties: true
    BlastRadiusNodeKind:
      type: string
      enum:
        - prompt
        - agent
        - policy
        - evaluator
        - dataset
        - model
        - tool
        - alert
      description: >
        `tool` is a reserved value in the type — no node of this kind is emitted
        yet (MCP tool calls aren't part of the graph today).
    BlastRadiusGraphNode:
      type: object
      properties:
        id:
          type: string
          description: '`<kind>:<key>` — stable across rebuilds.'
          example: prompt:support-reply
        kind:
          $ref: '#/components/schemas/BlastRadiusNodeKind'
        name:
          type: string
          example: support-reply
        href:
          type: string
          description: Relative deep link into the dashboard.
          example: prompts/support-reply
        environments:
          type: array
          items:
            type: string
          description: >
            Environment slugs this node is known to be associated with. An empty
            array means genuinely unknown, never "not production."
          example:
            - prod
            - staging
      required:
        - id
        - kind
        - name
        - href
        - environments
    BlastRadiusGraphEdge:
      type: object
      properties:
        from:
          type: string
          description: The upstream node id. `to` depends on `from`.
        to:
          type: string
          description: The downstream node id — what would be affected if `from` changed.
        origin:
          type: string
          enum:
            - declared
            - observed
          description: >
            `declared` — configured in your project (Postgres). `observed` —
            actually called, from real ClickHouse trace data in the queried
            window.
        callVolume:
          type: number
          description: Call count over the window, for `observed` edges.
        costUsd:
          type: number
          description: Cost in USD over the window, for `observed` edges.
        lastSeenAt:
          type: string
          format: date-time
      required:
        - from
        - to
        - origin
    BlastRadiusDependentEntry:
      type: object
      properties:
        node:
          $ref: '#/components/schemas/BlastRadiusGraphNode'
        depth:
          type: integer
          description: Hops from the root node.
          example: 1
        via:
          type: array
          items:
            $ref: '#/components/schemas/BlastRadiusGraphEdge'
          description: >
            The edge chain from the root to this node. On a node reachable
            through more than one path, this is only the first inbound edge the
            server's breadth-first walk happened to record — use
            `totalCallVolume30d`/`totalCostUsd30d`/`hasObservedTraffic` (below)
            to decide whether this node has real traffic, not this field's last
            edge.
        totalCallVolume30d:
          type: number
          description: >
            Sum of call volume across EVERY inbound edge feeding this node from
            within the blast radius, not just `via`'s last edge. Per-entry
            values across the response sum to `impact.callVolume30d`.
        totalCostUsd30d:
          type: number
          description: >-
            Same aggregation as `totalCallVolume30d`, for cost. Sums to
            `impact.cost30dUsd`.
        hasObservedTraffic:
          type: boolean
          description: >
            True when at least one inbound edge within the blast radius is
            `observed` — real call traffic — even if `via`'s own last edge is
            `declared`.
      required:
        - node
        - depth
        - via
        - totalCallVolume30d
        - totalCostUsd30d
        - hasObservedTraffic
    BlastRadiusImpact:
      type: object
      properties:
        agents:
          type: integer
          description: Count of distinct agent-kind dependents.
        environments:
          type: array
          items:
            type: string
          description: Union of every dependent's (and the root's) known environments.
        productionAffected:
          type: boolean
          description: >-
            True if any affected environment is marked production for this
            project.
        callVolume30d:
          type: number
          description: >-
            Total observed call volume across every dependent, over the queried
            window.
        cost30dUsd:
          type: number
          description: Total observed cost across every dependent, over the queried window.
        slosAtRisk:
          type: array
          items:
            type: string
          description: >
            Always an empty array today — reserved for an SLO model that hasn't
            shipped. Present so the response shape won't need to change later.
        gates:
          type: array
          items:
            type: string
          description: Same status as `slosAtRisk` — always empty today, reserved.
      required:
        - agents
        - environments
        - productionAffected
        - callVolume30d
        - cost30dUsd
        - slosAtRisk
        - gates
    BlastRadiusResponse:
      type: object
      properties:
        root:
          $ref: '#/components/schemas/BlastRadiusGraphNode'
        dependents:
          type: array
          items:
            $ref: '#/components/schemas/BlastRadiusDependentEntry'
        impact:
          $ref: '#/components/schemas/BlastRadiusImpact'
        truncated:
          type: boolean
          description: >
            True only when the walk hit `maxDepth`/`maxNodes` AND the graph
            genuinely continues beyond that point — not merely because the walk
            stopped exactly at the cap with nothing left beyond it.
        computedAt:
          type: string
          format: date-time
          description: >
            When this response's graph was built. There is no server-side cache
            in front of this endpoint today, so this is effectively "now" on
            every request.
      required:
        - root
        - dependents
        - impact
        - truncated
        - computedAt
    BlastRadiusGraphResponse:
      type: object
      properties:
        nodes:
          type: array
          items:
            $ref: '#/components/schemas/BlastRadiusGraphNode'
        edges:
          type: array
          items:
            $ref: '#/components/schemas/BlastRadiusGraphEdge'
        computedAt:
          type: string
          format: date-time
      required:
        - nodes
        - edges
        - computedAt
    Outcome:
      type: object
      description: >
        A single business outcome to report, attributed to one trace. Sent as an
        array wrapped in `{ outcomes: [...] }` — a batch of 1 to 100 per
        request.
      properties:
        kind:
          type: string
          minLength: 1
          maxLength: 100
          description: >
            A free-form label for what this outcome measures — no fixed enum.
            Whatever strings you report show up as filterable dimensions on the
            Value dashboard page.
          example: ticket_deflected
        success:
          type: boolean
          description: Whether this outcome was actually achieved.
        valueUsd:
          type: number
          minimum: 0
          description: >
            Dollar value of this outcome, if known. Omit entirely for "unknown"
            rather than sending `0`.
          example: 12.5
        attributes:
          type: object
          additionalProperties:
            oneOf:
              - type: string
              - type: number
              - type: boolean
          description: >
            Free-form metadata, stored as given. The TypeScript SDK's
            `outcome()` coerces every value to a string before sending, to match
            how every other Zespan SDK surface treats attributes; the raw ingest
            API itself accepts string, number, or boolean.
        traceId:
          type: string
          minLength: 1
          description: >
            The trace this outcome is attributed to. The trace does not need to
            exist in Zespan yet — outcomes and traces are joined at query time,
            not write time.
        sessionId:
          type: string
        agentName:
          type: string
          description: >
            The agent that produced the trace. Populating this is what lets the
            Value dashboard's "By agent" breakdown group correctly.
        environment:
          type: string
          maxLength: 50
          description: >
            Environment slug (e.g. `prod`, `staging`). Historical free-text
            values are aliased the same way the rest of ingestion aliases them
            (`production` -> `prod`, etc.).
        occurredAt:
          type: string
          format: date-time
          description: >
            When this outcome actually occurred. Defaults to the time the
            request is received.
      required:
        - kind
        - success
        - traceId
    OutcomeIngestResponse:
      type: object
      properties:
        accepted:
          type: integer
          description: Number of outcomes accepted from the batch.
          example: 1
      required:
        - accepted
    OutcomeSummary:
      type: object
      properties:
        name:
          type: string
          description: >
            The dimension's value for this row — an agent name or a model name,
            depending on the request's `dimension`.
        totalOutcomes:
          type: integer
        successes:
          type: integer
        valueUsd:
          type: number
        costUsd:
          type: number
        costPerSuccess:
          type: number
          nullable: true
          description: Null when there are zero successes to divide cost across.
        valuePerDollar:
          type: number
          nullable: true
          description: Null when cost is zero.
      required:
        - name
        - totalOutcomes
        - successes
        - valueUsd
        - costUsd
        - costPerSuccess
        - valuePerDollar
    OutcomeSummaryResponse:
      type: object
      properties:
        rows:
          type: array
          items:
            $ref: '#/components/schemas/OutcomeSummary'
      required:
        - rows
    OutcomeKindsResponse:
      type: object
      properties:
        kinds:
          type: array
          items:
            type: string
      required:
        - kinds
    ComplianceFrameworkControl:
      type: object
      properties:
        controlId:
          type: string
          example: CC6.1
        title:
          type: string
          example: Logical access controls
      required:
        - controlId
        - title
    ComplianceFramework:
      type: object
      properties:
        id:
          type: string
          enum:
            - soc2
        name:
          type: string
          example: SOC 2
        label:
          type: string
          description: >
            Rendered as the document subtitle. Describes evidence, never asserts
            compliance — e.g. "Evidence mapped to SOC 2 controls".
        reviewedOn:
          type: string
          format: date
          description: >-
            Date of the last review of this framework mapping. Rendered on every
            generated document.
        reviewedBy:
          type: string
          description: >
            Who reviewed this mapping, stated plainly — including when the
            review was internal rather than by a licensed external auditor.
          example: Zespan engineering — not reviewed by a licensed auditor
        controls:
          type: array
          items:
            $ref: '#/components/schemas/ComplianceFrameworkControl'
      required:
        - id
        - name
        - label
        - reviewedOn
        - reviewedBy
        - controls
    ComplianceFrameworksResponse:
      type: object
      properties:
        frameworks:
          type: array
          items:
            $ref: '#/components/schemas/ComplianceFramework'
      required:
        - frameworks
    GenerateEvidencePackRequest:
      type: object
      properties:
        kind:
          type: string
          enum:
            - agent_card
            - control_evidence
          description: >
            `agent_card` produces a Compliance Card for one agent (or all
            agents); `control_evidence` produces a framework-mapped document and
            requires `framework`.
        framework:
          type: string
          enum:
            - soc2
          description: Required when `kind` is `control_evidence`; ignored otherwise.
        scope:
          type: object
          properties:
            agentName:
              type: string
              minLength: 1
              maxLength: 200
              description: >-
                Restricts an `agent_card` document to one agent. Omit for all
                agents in the project.
          default: {}
        periodStart:
          type: string
          format: date-time
        periodEnd:
          type: string
          format: date-time
          description: Must be after `periodStart`; the period may not exceed 400 days.
        format:
          type: string
          enum:
            - json
            - html
          default: html
          description: >
            No `pdf` value — this deployment has no headless-browser rendering
            path, so a PDF request is rejected here rather than silently
            downgraded to HTML.
      required:
        - kind
        - periodStart
        - periodEnd
    GenerateEvidencePackResponse:
      type: object
      properties:
        packId:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - pending
            - processing
            - completed
            - failed
          description: >-
            Always `pending` on this response — generation has just been
            enqueued.
      required:
        - packId
        - status
    EvidencePack:
      type: object
      properties:
        id:
          type: string
          format: uuid
        kind:
          type: string
          enum:
            - agent_card
            - control_evidence
        framework:
          type: string
          nullable: true
          enum:
            - soc2
            - null
        scope:
          type: object
          properties:
            agentName:
              type: string
        periodStart:
          type: string
          format: date-time
        periodEnd:
          type: string
          format: date-time
        format:
          type: string
          enum:
            - json
            - html
        status:
          type: string
          enum:
            - pending
            - processing
            - completed
            - failed
        error:
          type: string
          nullable: true
          description: Set when `status` is `failed`.
        sha256:
          type: string
          nullable: true
          description: >-
            SHA-256 of the stored document's exact bytes, once generation has
            completed.
        generatedBy:
          type: string
          description: User id that requested generation.
        generatedAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time
          nullable: true
      required:
        - id
        - kind
        - framework
        - scope
        - periodStart
        - periodEnd
        - format
        - status
        - error
        - sha256
        - generatedBy
        - generatedAt
        - completedAt
    EvidencePackListResponse:
      type: object
      properties:
        packs:
          type: array
          items:
            $ref: '#/components/schemas/EvidencePack'
        pagination:
          type: object
          properties:
            page:
              type: integer
            pageSize:
              type: integer
            total:
              type: integer
            totalPages:
              type: integer
          required:
            - page
            - pageSize
            - total
            - totalPages
      required:
        - packs
        - pagination
    EvidencePackDetailResponse:
      type: object
      properties:
        pack:
          $ref: '#/components/schemas/EvidencePack'
        downloadUrl:
          type: string
          nullable: true
          description: >
            A presigned, short-lived (15 minute) URL when the pack is stored in
            object storage. Null on the local-disk backend — use `content` with
            `?download=true` instead.
        content:
          type: string
          nullable: true
          description: >
            The document's raw content, only populated when `?download=true` was
            passed and the object is on the local-disk backend (no presigned URL
            available).
      required:
        - pack
        - downloadUrl
        - content
    ComplianceDivergence:
      type: object
      properties:
        source:
          type: string
          description: >-
            The `EvidenceRef` source kind this citation came from (e.g.
            `guardrail_config`, `audit_log`, `incident`).
        id:
          type: string
        label:
          type: string
        reason:
          type: string
          enum:
            - missing
            - wrong_project
          description: >
            `missing` — the record doesn't exist anywhere; it was deleted.
            `wrong_project` — the record still exists, just no longer under this
            project.
        detail:
          type: string
      required:
        - source
        - id
        - label
        - reason
        - detail
    VerifyResult:
      type: object
      properties:
        sha256Matches:
          type: boolean
          description: >-
            True when the stored document's recomputed hash equals the hash
            recorded at generation time.
        storedSha256:
          type: string
          nullable: true
        recomputedSha256:
          type: string
          nullable: true
        evidenceStillPresent:
          type: boolean
          description: >
            True only when zero divergences were found among **checked**
            citations. Citations counted in `unverifiableRefs` are excluded from
            this check entirely — they are neither verified nor failed.
        checkedRefs:
          type: integer
          description: Citations actually re-resolved against a live record.
        unverifiableRefs:
          type: integer
          description: >
            Citations that structurally cannot be re-checked at record level
            (ClickHouse aggregates, certain change-timeline event kinds). Always
            read alongside `evidenceStillPresent` — a high `unverifiableRefs`
            count means most of the document was not re-checked, even if every
            checkable citation passed.
        divergences:
          type: array
          items:
            $ref: '#/components/schemas/ComplianceDivergence'
        objectMissing:
          type: boolean
          description: True when the stored document itself could not be read back.
      required:
        - sha256Matches
        - storedSha256
        - recomputedSha256
        - evidenceStillPresent
        - checkedRefs
        - unverifiableRefs
        - divergences
        - objectMissing
    ControlCoverage:
      type: object
      properties:
        controlId:
          type: string
          example: CC6.1
        title:
          type: string
        status:
          type: string
          enum:
            - evidence
            - no_evidence
        recordCount:
          type: integer
        sectionsWithEvidence:
          type: array
          items:
            type: string
        sectionsWithoutEvidence:
          type: array
          items:
            type: string
      required:
        - controlId
        - title
        - status
        - recordCount
        - sectionsWithEvidence
        - sectionsWithoutEvidence
    ComplianceCoverageResponse:
      type: object
      properties:
        framework:
          type: string
          enum:
            - soc2
        label:
          type: string
        controls:
          type: array
          items:
            $ref: '#/components/schemas/ControlCoverage'
      required:
        - framework
        - label
        - controls
    ModelLifecycleSummary:
      type: object
      nullable: true
      description: >
        The `lifecycle` overlay on a `ModelUsageRow`. `null` when the model has
        neither an open finding nor a catalogue entry.
      properties:
        findingId:
          type: string
          nullable: true
          description: >
            Set only when this project has an open `ModelLifecycleFinding` for
            the model. `null` when the model has a catalogue entry (a known
            retirement date) but no finding yet — e.g. the retirement is further
            out than the detection horizon.
        retiresAt:
          type: string
          format: date-time
          nullable: true
        daysRemaining:
          type: integer
          nullable: true
          description: Negative when the model has already retired.
        band:
          type: integer
          nullable: true
          enum:
            - 90
            - 30
            - 7
            - 0
          description: >-
            The urgency band derived from `daysRemaining`, or `null` outside
            every band.
        retired:
          type: boolean
        successorModel:
          type: string
          nullable: true
        lifecycleSource:
          type: string
          nullable: true
          enum:
            - announced
            - inferred
            - manual
      required:
        - findingId
        - retiresAt
        - daysRemaining
        - band
        - retired
        - successorModel
        - lifecycleSource
    ModelUsageRow:
      type: object
      description: One row of the Models dashboard page's table, per model called in range.
      properties:
        model:
          type: string
        provider:
          type: string
        total_calls:
          type: integer
        total_cost:
          type: number
        avg_latency:
          type: number
          description: Milliseconds.
        p50_latency:
          type: number
        p95_latency:
          type: number
        total_input_tokens:
          type: integer
        total_output_tokens:
          type: integer
        total_cached_tokens:
          type: integer
        error_count:
          type: integer
        trace_count:
          type: integer
        unique_users:
          type: integer
        endpoint_count:
          type: integer
        agent_count:
          type: integer
        endpoints:
          type: array
          items:
            type: string
        agents:
          type: array
          items:
            type: string
        lifecycle:
          $ref: '#/components/schemas/ModelLifecycleSummary'
      required:
        - model
        - provider
        - total_calls
        - total_cost
        - lifecycle
    ModelLifecycleFinding:
      type: object
      description: >
        One (project, model) deprecation finding. `callVolume30d`, `cost30dUsd`,
        `successorCallVolume30d`, and `costDeltaPerCallUsd` are all measured
        from real ClickHouse trace data over the trailing 30 days — never
        estimated, and never a quality or regression figure.
      properties:
        id:
          type: string
        projectId:
          type: string
          format: uuid
        environmentId:
          type: string
          nullable: true
        environmentSlugs:
          type: array
          items:
            type: string
        model:
          type: string
        provider:
          type: string
          nullable: true
        kind:
          type: string
          enum:
            - deprecation
          description: Reserved for a future `drift` kind — not written by anything today.
        deprecatedAt:
          type: string
          format: date-time
          nullable: true
        retiresAt:
          type: string
          format: date-time
          nullable: true
        daysRemaining:
          type: integer
          nullable: true
          description: Negative when the model has already retired.
        band:
          type: integer
          nullable: true
          enum:
            - 90
            - 30
            - 7
            - 0
        retired:
          type: boolean
        callVolume30d:
          type: integer
          description: Real call count over the trailing 30 days.
        cost30dUsd:
          type: number
          description: Real spend in USD over the trailing 30 days.
        affectedAgents:
          type: array
          items:
            type: string
        affectedPrompts:
          type: array
          items:
            type: string
          description: >-
            `"name@vN"` — the prompt name and version, e.g.
            `"refund-policy@v4"`.
        successorModel:
          type: string
          nullable: true
        successorCallVolume30d:
          type: integer
          nullable: true
        costDeltaPerCallUsd:
          type: number
          nullable: true
          description: >
            Average cost per call on `successorModel` minus this model, over
            your own organic traffic on both over the same 30 days. `null` when
            `comparisonBasis` is `none`.
        comparisonBasis:
          type: string
          enum:
            - organic_traffic
            - none
          description: >-
            `none` means there is no traffic on the successor to compare against
            yet — not that the comparison failed.
        lifecycleSource:
          type: string
          nullable: true
          enum:
            - announced
            - inferred
            - manual
        lifecycleCheckedAt:
          type: string
          format: date-time
          nullable: true
        status:
          type: string
          enum:
            - open
            - dismissed
        dismissedReason:
          type: string
          nullable: true
        dismissedAt:
          type: string
          format: date-time
          nullable: true
        detectedAt:
          type: string
          format: date-time
      required:
        - id
        - projectId
        - environmentId
        - environmentSlugs
        - model
        - provider
        - kind
        - deprecatedAt
        - retiresAt
        - daysRemaining
        - band
        - retired
        - callVolume30d
        - cost30dUsd
        - affectedAgents
        - affectedPrompts
        - successorModel
        - successorCallVolume30d
        - costDeltaPerCallUsd
        - comparisonBasis
        - lifecycleSource
        - lifecycleCheckedAt
        - status
        - dismissedReason
        - dismissedAt
        - detectedAt
    ModelLifecycleAlertRule:
      type: object
      description: >
        The project's opt-in notification settings for Model Lifecycle findings
        — a distinct `AlertRule` of `type: "model-lifecycle"`, managed through
        this dedicated GET/PUT pair rather than the general [Create
        Alert](/dashboard/alerts) flow, since a lifecycle rule has no metric,
        condition, or threshold.
      properties:
        enabled:
          type: boolean
        notifyEmails:
          type: array
          items:
            type: string
            format: email
        notifyWebhook:
          type: string
          nullable: true
      required:
        - enabled
        - notifyEmails
        - notifyWebhook
    ModelLifecycleFeedEntry:
      type: object
      description: >-
        One row of the bundled model lifecycle catalogue. See the feed reference
        for full field semantics.
      properties:
        provider:
          type: string
        model:
          type: string
        deprecatedAt:
          type: string
          nullable: true
          description: '`YYYY-MM-DD`, or `null`.'
        retiresAt:
          type: string
          nullable: true
          description: >-
            `YYYY-MM-DD`, or `null`. A finding is only produced for a non-null
            date.
        successorModel:
          type: string
          nullable: true
        lifecycleSource:
          type: string
          enum:
            - announced
            - inferred
            - manual
        lifecycleNote:
          type: string
          nullable: true
        sourceUrl:
          type: string
          nullable: true
          description: The provider page this entry was verified against.
      required:
        - provider
        - model
        - deprecatedAt
        - retiresAt
        - successorModel
        - lifecycleSource
        - lifecycleNote
        - sourceUrl
