openapi: 3.1.0
info:
  title: Liya Engine API
  version: 1.0.0
  description: |
    Multi-tenant REST API for the Liya Engine multi-domain intelligence platform.

    ## Authentication
    All `/v1/*` endpoints require an API key passed as a Bearer token:
    ```
    Authorization: Bearer liya_xxxxxxxxxxxx
    ```

    ## Admin Endpoints
    All `/admin/*` endpoints require:
    ```
    X-Admin-Secret: your-admin-secret
    ```

  contact:
    name: Liya Engine Support
    url: https://liyaengine.ai
  license:
    name: UNLICENSED

servers:
  - url: http://localhost:3007
    description: Local development
  - url: https://api.liyaengine.ai
    description: Production

tags:
  - name: Health
    description: Liveness and readiness probes
  - name: Auth
    description: Dashboard user signup, login, and password reset
  - name: Dashboard — Account
    description: Tenant self-service account and API key management
  - name: Dashboard — Usage
    description: Usage analytics for the tenant dashboard
  - name: Dashboard — Sessions
    description: AI session history for the tenant dashboard
  - name: Hiring
    description: Hiring intelligence — 12 AI-powered intents
  - name: Domains
    description: Domain discovery
  - name: Collections
    description: |
      Tenant-wide knowledge collections — organize documents, scope
      retrieval per collection, and attach to one or many domains. The
      API-key counterpart to the dashboard's Knowledge tab; both call the
      same underlying service, so behavior is identical either way.
  - name: Documents
    description: |
      The tenant-wide knowledge document pool collections reference rather
      than own. Phase 1 of the KBaaS slice — read/delete only; upload and
      async ingestion (URL crawl, file jobs) are still dashboard-only.
  - name: Agents
    description: |
      Create, configure, deploy, and delete standalone Agents (an
      orchestration layer that composes Intents/Workflows/Tools/Knowledge as
      capabilities). The API-key counterpart to the dashboard's Agent
      Runtime page; both call the same underlying service. Running an
      already-deployed agent, and reading its run/session history, are
      separate endpoints under the Core Runtime tag below.
  - name: Workflows
    description: |
      Create, configure, toggle, deploy, and delete Workflows — multi-step
      graphs of intents/agents/actions/conditions. The API-key counterpart
      to the dashboard's Workflow Builder; both call the same underlying
      service. Running an already-deployed workflow, and reading its run
      history, are separate endpoints under the Core Runtime tag below.
  - name: Core Runtime
    description: |
      The unified, domain-agnostic entry points for running any custom
      domain's intents, running a saved Agent, running a Workflow
      synchronously, or querying a domain's knowledge base directly.
  - name: Evaluations
    description: |
      Full Evaluation Studio CRUD — Datasets/Cases/Suites/Runs/Reviews — plus
      standalone Evaluation-as-a-Service scoring with no domain, intent, or
      dashboard session required. A Suite binds a Dataset to one specific
      intent; running a Suite calls that intent for real
      (`POST /v1/evals/suites/{id}/run`) and scores what it produces, while
      the EaaS endpoints below score a response you already generated
      yourself. Every run is async — creation endpoints return a run id
      immediately, poll `GET /v1/evals/runs/{id}` for status/results. Each
      judge call has a real LLM cost; see the rate limits noted on each
      endpoint, and note that run-creating endpoints for API-key callers are
      additionally gated against the tenant's monthly Evals budget
      (`429 EVALS_BUDGET_EXCEEDED` / `EVALS_NOT_AVAILABLE`, with a `details`
      object on the error).
  - name: Admin — Tenants
    description: Tenant provisioning and management (admin only)
  - name: Admin — API Keys
    description: API key management (admin only)
  - name: Admin — Usage
    description: Usage and billing data (admin only)
  - name: Admin — Stats
    description: Platform-wide analytics (admin only)

# ============================================
# SECURITY SCHEMES
# ============================================

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: API key issued per tenant. Format — liya_xxxx
    DashboardAuth:
      type: http
      scheme: bearer
      description: JWT session token for dashboard users. Obtain via POST /auth/login or POST /auth/signup
    AdminSecret:
      type: apiKey
      in: header
      name: X-Admin-Secret
      description: Shared admin secret for management endpoints

  # ============================================
  # SHARED SCHEMAS
  # ============================================

  schemas:
    SuccessEnvelope:
      type: object
      required: [success]
      properties:
        success:
          type: boolean
          example: true

    ErrorEnvelope:
      type: object
      required: [success, error]
      properties:
        success:
          type: boolean
          example: false
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              example: INVALID_INPUT
            message:
              type: string
              example: Missing required field
            details:
              type: object
            docs_url:
              type: string

    Collection:
      type: object
      properties:
        id: { type: string }
        slug: { type: string, example: contracts }
        label: { type: string, example: Contracts }
        color: { type: string, example: '#6366f1' }
        created_at: { type: string, format: date-time }
        domain_keys:
          type: array
          items: { type: string }
          description: Every domain this collection is attached to.
        tags:
          type: array
          items: { type: string }
        visibility:
          type: string
          enum: [workspace, restricted]
        last_synced_at: { type: string, format: date-time, nullable: true }
        retrieval_config:
          type: object
          nullable: true
          properties:
            scope:
              type: string
              enum: [collection_only, collection_plus_domain]
            boost_priority: { type: number }
            fallback_when_empty:
              type: string
              enum: [domain_fallback, no_results]
        default_embedding_model: { type: string, nullable: true }
        default_chunking_strategy:
          type: string
          nullable: true
          enum: [fixed, semantic, sliding_window]
        default_chunk_size: { type: integer, nullable: true }
        default_chunk_overlap: { type: integer, nullable: true }

    IngestionJobRef:
      type: object
      properties:
        success: { type: boolean, example: true }
        data:
          type: object
          properties:
            jobId: { type: string }
            status: { type: string, enum: [pending, running, completed, failed, cancelled], example: pending }

    IngestionJob:
      type: object
      properties:
        id: { type: string }
        source_type: { type: string }
        name: { type: string }
        status: { type: string, enum: [pending, running, completed, failed, cancelled] }
        stage: { type: string, nullable: true }
        progress: { type: integer, minimum: 0, maximum: 100 }
        result: { type: object, nullable: true }
        error_message: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        started_at: { type: string, format: date-time, nullable: true }
        completed_at: { type: string, format: date-time, nullable: true }

    Document:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        category: { type: string, example: document }
        chunks: { type: integer }
        sizeKb: { type: integer }
        embeddingModel: { type: string, nullable: true }
        uploadedBy: { type: string }
        uploadedAt: { type: string, format: date-time }
        collections:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              slug: { type: string }
              label: { type: string }
              color: { type: string }

    CustomIntent:
      type: object
      properties:
        id: { type: string }
        domain_key: { type: string }
        intent_key: { type: string, example: refund-status }
        display_name: { type: string }
        description: { type: string, nullable: true }
        prompt_template: { type: string }
        prompt_binding:
          type: object
          nullable: true
          description: Set when bound to a Prompt Studio library version instead of inline text.
        output_schema: { type: object, nullable: true }
        input_schema: { type: object, nullable: true }
        guardrails_config: { type: object, nullable: true }
        agent_config: { type: object, nullable: true }
        execution_config: { type: object, nullable: true }
        retrieval_config: { type: object, nullable: true }
        cache_config: { type: object, nullable: true }
        sort_order: { type: integer }
        is_active: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    CustomIntentVersionSummary:
      type: object
      properties:
        id: { type: string }
        version_number: { type: integer }
        changed_fields:
          type: array
          items: { type: string }
        change_type: { type: string, enum: [create, update, restore] }
        restored_from_version: { type: integer, nullable: true }
        created_by: { type: string, nullable: true }
        actorName: { type: string, nullable: true }
        created_at: { type: string, format: date-time }

    CreateCollectionRequest:
      type: object
      required: [slug, label, domain_keys]
      properties:
        slug:
          type: string
          pattern: '^[a-z0-9_-]+$'
          description: Lowercase letters, numbers, hyphens, and underscores.
          example: contracts
        label: { type: string, example: Contracts }
        color: { type: string, example: '#6366f1' }
        domain_keys:
          type: array
          items: { type: string }
          minItems: 1
          description: Must include at least one active domain.
        default_embedding_model: { type: string }
        default_chunking_strategy:
          type: string
          enum: [fixed, semantic, sliding_window]
        default_chunk_size: { type: integer }
        default_chunk_overlap: { type: integer }

    UpdateCollectionRequest:
      type: object
      properties:
        label: { type: string }
        color: { type: string }
        tags:
          type: array
          items: { type: string }
        visibility:
          type: string
          enum: [workspace, restricted]
        retrieval_config:
          type: object
          properties:
            scope:
              type: string
              enum: [collection_only, collection_plus_domain]
            boost_priority: { type: number }
            fallback_when_empty:
              type: string
              enum: [domain_fallback, no_results]
        default_embedding_model: { type: string }
        default_chunking_strategy:
          type: string
          enum: [fixed, semantic, sliding_window]
        default_chunk_size: { type: integer }
        default_chunk_overlap: { type: integer }

    Agent:
      type: object
      properties:
        id: { type: string }
        tenant_id: { type: string }
        agent_key: { type: string, example: support-triage }
        name: { type: string }
        description: { type: string, nullable: true }
        goal: { type: string }
        system_instructions: { type: string, nullable: true }
        model: { type: string, nullable: true }
        temperature: { type: number, nullable: true }
        status:
          type: string
          enum: [draft, active, inactive, error]
          description: The real runtime gate — only an active agent can be run or deployed further. 'inactive' also means soft-deleted.
        intent_ids: { type: array, items: { type: string } }
        workflow_ids: { type: array, items: { type: string } }
        action_ids: { type: array, items: { type: string } }
        knowledge_domain_keys: { type: array, items: { type: string } }
        tools_config:
          type: object
          nullable: true
          additionalProperties: true
          description: enabled_platform_tools, custom_tool_refs, permission_policy, mcp_servers, notify_email, notify_label, service_locations, transfer_phone_number, transfer_label.
        memory_config:
          type: object
          nullable: true
          additionalProperties: true
          description: conversation_memory, user_preferences, persistent_memory, retention_days.
        behavior_config:
          type: object
          nullable: true
          additionalProperties: true
          description: planning, execution, fallback, termination, max_steps, max_output_tokens, max_cost_usd.
        guardrail_policy_id: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        effective_runtime_config:
          type: object
          description: Configured values merged with platform defaults, with each field's source (agent | platform_default | provider_default).

    CreateAgentRequest:
      type: object
      required: [agent_key, name, goal]
      properties:
        agent_key:
          type: string
          pattern: '^[a-z0-9_-]+$'
          example: support-triage
        name: { type: string }
        description: { type: string }
        goal: { type: string }
        system_instructions: { type: string }
        model: { type: string }
        temperature: { type: number, minimum: 0, maximum: 2 }
        intent_ids: { type: array, items: { type: string } }
        workflow_ids: { type: array, items: { type: string } }
        action_ids: { type: array, items: { type: string } }
        knowledge_domain_keys: { type: array, items: { type: string } }
        tools_config: { type: object, additionalProperties: true }
        memory_config: { type: object, additionalProperties: true }
        behavior_config: { type: object, additionalProperties: true }

    UpdateAgentRequest:
      type: object
      properties:
        name: { type: string }
        description: { type: string }
        goal: { type: string }
        system_instructions: { type: string }
        model: { type: string }
        temperature: { type: number, minimum: 0, maximum: 2 }
        status:
          type: string
          enum: [draft, active, inactive, error]
          description: Setting 'active' directly is rejected (409 DEPLOY_REQUIRED) — use POST /v1/agents/{agentKey}/deploy instead.
        intent_ids: { type: array, items: { type: string } }
        workflow_ids: { type: array, items: { type: string } }
        action_ids: { type: array, items: { type: string } }
        knowledge_domain_keys: { type: array, items: { type: string } }
        tools_config: { type: object, additionalProperties: true }
        memory_config: { type: object, additionalProperties: true }
        behavior_config: { type: object, additionalProperties: true }

    Workflow:
      type: object
      properties:
        id: { type: string }
        tenant_id: { type: string }
        name: { type: string }
        workflow_key: { type: string, example: lead-intake, description: "Url-safe, tenant-unique slug — accepted anywhere workflowId appears in a path, interchangeably with the database id." }
        description: { type: string, nullable: true }
        is_active:
          type: boolean
          description: The real runtime gate — an inactive workflow's run endpoint 404s.
        status:
          type: string
          description: Dashboard authoring-lifecycle label (draft | active | inactive | error) — set together with is_active by the dashboard's Deploy action.
        trigger_type:
          type: string
          example: manual
        trigger_config:
          type: object
          nullable: true
          properties:
            slug: { type: string, nullable: true }
            has_secret:
              type: boolean
              description: The webhook secret itself is never returned after initial generation.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        steps:
          type: array
          items:
            type: object
            properties:
              id: { type: string }
              step_type: { type: string, example: ai_intent, enum: [trigger, action, condition, switch, delay, loop, ai_intent, ai_agent, approval, end] }
              name: { type: string, nullable: true }
              action_id: { type: string, nullable: true }
              action_name: { type: string, nullable: true }
              config: { type: object, additionalProperties: true }
              position_x: { type: number, nullable: true }
              position_y: { type: number, nullable: true }
              step_order: { type: integer }
              guidance: { type: string, nullable: true }
              on_success_step_id: { type: string, nullable: true }
              on_failure_step_id: { type: string, nullable: true }

    StepInput:
      type: object
      required: [step_type]
      description: Same shape a workflow's steps are returned in, plus id (real or client-temp) and on_success_ref/on_failure_ref (stable-id branch targets, resolved server-side into on_success_step_id/on_failure_step_id).
      properties:
        id: { type: string, description: A real, already-persisted step id (editing an existing node) or a client-generated temp id (a new node) — both resolve through the same id-map. }
        step_type: { type: string, enum: [trigger, action, condition, switch, delay, loop, ai_intent, ai_agent, approval, end] }
        name: { type: string }
        action_id: { type: string, description: Required when step_type is 'action'. }
        config: { type: object, additionalProperties: true }
        position_x: { type: number }
        position_y: { type: number }
        guidance: { type: string }
        on_success_ref: { type: string, nullable: true }
        on_failure_ref: { type: string, nullable: true }

    CreateWorkflowRequest:
      type: object
      required: [name]
      properties:
        name: { type: string }
        description: { type: string }
        steps:
          type: array
          items:
            $ref: '#/components/schemas/StepInput'

    UpdateWorkflowRequest:
      type: object
      properties:
        name: { type: string }
        description: { type: string }
        workflow_key:
          type: string
          pattern: '^[a-z0-9_-]+$'
          description: Freely renameable — must be unique within the tenant.
        steps:
          type: array
          items:
            $ref: '#/components/schemas/StepInput'

    RotateWebhookSecretRequest:
      type: object
      properties:
        grace_period_seconds:
          type: integer
          minimum: 0
          maximum: 3600
          default: 300
          description: How long the previous secret stays valid after rotation. 0 = immediate cutover.

    EvalDataset:
      type: object
      properties:
        id: { type: string }
        tenant_id: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        cases:
          type: array
          description: Present only on GET /v1/evals/datasets/{id} (the single-dataset fetch); list/create responses return a lighter shape instead.
          items: { $ref: '#/components/schemas/EvalCase' }

    EvalCase:
      type: object
      properties:
        id: { type: string }
        tenant_id: { type: string }
        dataset_id: { type: string, nullable: true, description: Null for an ephemeral case created inline by a run submission. }
        input: { type: object }
        message: { type: string, nullable: true }
        expected_output: { nullable: true, description: A string (substring match) or object (partial deep-equality), checked before the judge runs. }
        notes: { type: string, nullable: true }
        created_at: { type: string, format: date-time }

    EvalSuite:
      type: object
      description: Binds a Dataset to one specific (domain_key, intent_key). Custom scorer is at most one of expression or webhook.
      properties:
        id: { type: string }
        tenant_id: { type: string }
        name: { type: string }
        domain_key: { type: string }
        intent_key: { type: string }
        dataset_id: { type: string }
        custom_scorer_expression: { type: string, nullable: true }
        custom_scorer_label: { type: string, nullable: true }
        custom_scorer_webhook_url: { type: string, nullable: true }
        custom_scorer_webhook_secret_set:
          type: boolean
          description: The webhook secret itself is never returned after initial submission — only whether one is set.
        baseline_run_id: { type: string, nullable: true, description: The run pinned as this suite's baseline for automatic comparison on future run reads. }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }

    EvalRun:
      type: object
      description: |
        Always async — creation endpoints return this row in `pending`
        status immediately; poll GET /v1/evals/runs/{id} for progress and,
        once `status` is `completed`, results.
      properties:
        id: { type: string }
        tenant_id: { type: string }
        suite_id: { type: string, nullable: true, description: Null for an execution_mode "external_output" run — no suite/domain/intent is involved. }
        domain_key: { type: string, nullable: true }
        intent_key: { type: string, nullable: true }
        execution_mode:
          type: string
          enum: [liya_intent, external_output]
          description: liya_intent — engine.execute() against a real intent. external_output — the caller already supplied the response; only scoring runs.
        triggered_by: { type: string, enum: [dashboard, api] }
        status:
          type: string
          enum: [pending, running, completed, failed, cancelled]
        stage: { type: string, nullable: true, example: "case 2/5" }
        progress: { type: integer, description: 0-100. }
        mean_score: { type: number, nullable: true }
        dimension_scores: { type: object, nullable: true }
        cases_total: { type: integer }
        cases_passed: { type: integer }
        started_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time, nullable: true }
        error: { type: string, nullable: true }
        judge_total_cost_usd: { type: number, nullable: true }
        judge_model: { type: string, nullable: true }
        judge_calls: { type: integer, nullable: true }
        judge_failures: { type: integer, nullable: true }
        judge_cap_reached: { type: boolean }
        model_override: { type: string, nullable: true, description: "A force_model override this run was created with (see POST .../suites/{id}/compare-models)." }

    EvalReview:
      type: object
      description: A human reviewer's verdict on one case result's judge score, independent of who triggered the run.
      properties:
        id: { type: string }
        tenant_id: { type: string }
        run_result_id: { type: string }
        reviewer_id: { type: string }
        verdict: { type: string, enum: [agree, override, flag] }
        corrected_score: { type: number, nullable: true, description: 1-5. Only meaningful when verdict is override. }
        note: { type: string, nullable: true }
        created_at: { type: string, format: date-time }

    IntentRequest:
      type: object
      required: [input]
      properties:
        input:
          type: object
          required: [user_id]
          properties:
            user_id:
              type: string
              description: Unique identifier for the end user
              example: user_abc123
            resume_text:
              type: string
              description: Plain-text resume content
            job_title:
              type: string
              description: Target job title
            job_description:
              type: string
              description: Full job description text
            target_role:
              type: string
              description: Desired role
            current_role:
              type: string
            years_experience:
              type: number
            skills:
              type: array
              items:
                type: string
            message:
              type: string
              description: User message for conversational intents (coaching, mock interview, chat)
          additionalProperties: true
        session_id:
          type: string
          description: Continue an existing session (optional)
        metadata:
          type: object
          additionalProperties: true
        preferences:
          type: object
          additionalProperties: true

    IntentResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            output:
              type: object
              description: Structured AI output (varies by intent)
            session_id:
              type: string
            message_id:
              type: string
        metadata:
          type: object
          properties:
            intent:
              type: string
            domain:
              type: string
            model_used:
              type: string
              example: gpt-4o
            tokens_used:
              type: integer
            cost_usd:
              type: number
            latency_ms:
              type: integer
            cached:
              type: boolean
        usage:
          type: object
          properties:
            requests_remaining:
              type: integer
              nullable: true
            tokens_remaining:
              type: integer
              nullable: true

    GuardrailIssue:
      type: object
      properties:
        code:
          type: string
          example: PII_EMAIL_REDACTED
        severity:
          type: string
          enum: [info, warning, error, critical]
        message:
          type: string
        action_taken:
          type: string
          enum: [none, modified, blocked, flagged]
        details:
          type: object
          additionalProperties: true

    GuardrailCheckResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        data:
          type: object
          properties:
            passed:
              type: boolean
              description: false means at least one stage blocked this content.
            content:
              type: string
              description: Present for stage=pre_llm — the content after any redaction (e.g. PII replaced with [REDACTED]).
            response:
              description: Present for stage=post_llm — the response after any stage modified it.
              oneOf:
                - type: string
                - type: object
                  additionalProperties: true
            issues:
              type: array
              items:
                $ref: '#/components/schemas/GuardrailIssue'
            fallback:
              type: string
              description: Present for stage=pre_llm when passed is false — a user-safe message to show instead of calling an LLM.
            should_retry:
              type: boolean
              description: Present for stage=post_llm — true when a stage suggests re-generating the response with stricter prompting.

    Tenant:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
          example: acme-corp
        tenantName:
          type: string
          example: Acme Corp
        tenantType:
          type: string
          enum: [internal, partner, enterprise, trial]
        isActive:
          type: boolean
        enabledDomains:
          type: array
          items:
            type: string
          example: [hiring]
        billingPlan:
          type: string
          example: starter
        billingEmail:
          type: string
          nullable: true
        monthlyRequestQuota:
          type: integer
          nullable: true
        monthlyTokenQuota:
          type: string
          nullable: true
        requestsThisMonth:
          type: integer
        tokensThisMonth:
          type: string
        costThisMonth:
          type: string
        hasApiKey:
          type: boolean
        apiKeyLastUsedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    DashboardUser:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email
          example: dev@acme.com
        role:
          type: string
          enum: [owner, admin, developer, viewer]
          example: owner
        emailVerified:
          type: boolean
          example: false
        createdAt:
          type: string
          format: date-time

    TenantProfile:
      type: object
      properties:
        tenantId:
          type: string
          example: acme-corp
        tenantName:
          type: string
          example: Acme Corp
        billingPlan:
          type: string
          example: starter
        billingEmail:
          type: string
          nullable: true
        monthlyRequestQuota:
          type: integer
          nullable: true
        monthlyTokenQuota:
          type: string
          nullable: true
        enabledDomains:
          type: array
          items:
            type: string
          example: [hiring]
        isActive:
          type: boolean
        apiKeyPreview:
          type: string
          description: Masked API key preview (first 12 + last 4 chars)
          nullable: true
          example: liya_live_abc...xyz1

    SessionSummary:
      type: object
      properties:
        sessionId:
          type: string
        userId:
          type: string
        domain:
          type: string
          example: hiring
        status:
          type: string
          enum: [active, completed, expired, error]
        messageCount:
          type: integer
        tokensUsed:
          type: integer
        costUsd:
          type: string
          example: "0.002400"
        lastActivityAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time

    Pagination:
      type: object
      properties:
        total:
          type: integer
        limit:
          type: integer
        offset:
          type: integer

# ============================================
# PATHS
# ============================================

paths:

  # ==========================================
  # HEALTH
  # ==========================================

  /health/live:
    get:
      tags: [Health]
      summary: Liveness probe
      description: Returns 200 if the process is running. No auth required.
      responses:
        '200':
          description: Process alive
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok

  /health/ready:
    get:
      tags: [Health]
      summary: Readiness probe
      description: Returns 200 if database and Liya Core engine are ready.
      responses:
        '200':
          description: Ready to serve requests
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ready
                  engine:
                    type: boolean
                  db:
                    type: boolean
        '503':
          description: Not ready
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: not_ready
                  engine:
                    type: boolean
                  db:
                    type: boolean

  # ==========================================
  # DOMAIN DISCOVERY
  # ==========================================

  /v1/domains:
    get:
      tags: [Domains]
      summary: List enabled domains
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Domains available to this tenant
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      domains:
                        type: array
                        items:
                          type: object
                          properties:
                            domain:
                              type: string
                            available:
                              type: boolean
                            endpoint:
                              type: string
                            docs_url:
                              type: string
                      total:
                        type: integer

  # NOTE: src/routes/v1/domains.routes.ts mounts before this discovery route
  # in src/routes/v1/index.ts, so Express actually serves GET /v1/domains
  # from handleListDomains (tenant-owned custom domain CRUD list), not the
  # discovery handler this block documents — the discovery handler is dead
  # code today. Documented as-registered (the spec should describe what a
  # caller actually gets), not as the presumably-intended discovery shape.
  # Flagged, not fixed, as part of the SDK/docs-sync initiative.

  # ==========================================
  # COLLECTIONS
  # ==========================================

  /v1/collections:
    get:
      tags: [Collections]
      summary: List this tenant's collections
      security:
        - BearerAuth: []
      responses:
        '200':
          description: This tenant's collections.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      collections:
                        type: array
                        items:
                          $ref: '#/components/schemas/Collection'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Collections]
      summary: Create a collection
      description: |
        `domain_keys` must include at least one currently-active domain.
        Same validation and behavior as the dashboard's Knowledge tab — both
        call services/resources/collectionService.ts.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCollectionRequest'
      responses:
        '201':
          description: Collection created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      collection:
                        $ref: '#/components/schemas/Collection'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: One or more domain_keys don't exist or aren't active
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          $ref: '#/components/responses/Conflict'

  /v1/collections/{id}:
    get:
      tags: [Collections]
      summary: Get a single collection
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The collection.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      collection:
                        $ref: '#/components/schemas/Collection'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Collections]
      summary: Update a collection's config
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateCollectionRequest'
      responses:
        '200':
          description: Collection updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      collection:
                        $ref: '#/components/schemas/Collection'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Restricted collection — only admin/owner dashboard roles may update; not gated for API-key callers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Collections]
      summary: Delete a collection
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Collection deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Restricted collection — only admin/owner dashboard roles may delete; not gated for API-key callers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/collections/{id}/domains/{domainKey}:
    post:
      tags: [Collections]
      summary: Attach a collection to a domain
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: domainKey
          in: path
          required: true
          schema: { type: string }
      responses:
        '201':
          description: Attached.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Restricted collection — only admin/owner dashboard roles may attach; not gated for API-key callers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: Collection not found, or domain not found/inactive.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    delete:
      tags: [Collections]
      summary: Detach a collection from a domain
      description: Detach only — never deletes the collection, even if this was its last attachment.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: domainKey
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Detached (idempotent — succeeds even if not attached).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/collections/{id}/documents:
    get:
      tags: [Collections]
      summary: List documents attached to a collection
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Documents attached to this collection.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      documents:
                        type: array
                        items:
                          $ref: '#/components/schemas/Document'
                      total: { type: integer }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/collections/{id}/documents/{documentId}:
    post:
      tags: [Collections]
      summary: Attach an existing document to a collection
      description: Reference only — a collection never owns/copies a document; embeddings are never touched.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: documentId
          in: path
          required: true
          schema: { type: string }
      responses:
        '201':
          description: Attached.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Restricted collection — only admin/owner dashboard roles may attach; not gated for API-key callers.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          description: Collection not found, or document not found for this tenant.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    delete:
      tags: [Collections]
      summary: Detach a document from a collection
      description: Detach only — the document and its embeddings are untouched.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: documentId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Detached (idempotent).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/collections/{id}/analytics:
    get:
      tags: [Collections]
      summary: Get a collection's document/chunk/storage stats
      description: On-the-fly aggregation — no dedicated rollup table, so this reflects live state exactly.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Analytics.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      sources: { type: integer }
                      documents: { type: integer }
                      chunks: { type: integer }
                      storage_kb: { type: integer }
                      embedding_model: { type: string, nullable: true }
                      indexed: { type: boolean }
                      last_synced_at: { type: string, format: date-time, nullable: true }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/collections/{id}/connections:
    get:
      tags: [Collections]
      summary: Get what references this collection — domains, intents, agents
      description: |
        Domains and intents are real, direct links. Agents are indirect
        (LiyaAgent only references knowledge at the domain level, so this is
        "connected via domain", clearly labeled — not collection-exact).
        `workflows` is always `null` — no workflow-to-collection link, direct
        or indirect, exists in the schema today. That's a real answer, not
        an unimplemented stub.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Connections.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      domains:
                        type: array
                        items:
                          type: object
                          properties:
                            domain_key: { type: string }
                            display_name: { type: string }
                      intents:
                        type: array
                        items:
                          type: object
                          properties:
                            intent_key: { type: string }
                            domain_key: { type: string }
                            display_name: { type: string }
                      agents:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string }
                            name: { type: string }
                            via: { type: string, example: domain }
                      workflows:
                        nullable: true
                        type: array
                        items: { type: object }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ==========================================
  # DOCUMENTS (KBaaS, Phase 1 — read/delete only)
  # ==========================================

  /v1/documents:
    get:
      tags: [Documents]
      summary: List this tenant's knowledge documents
      description: |
        The tenant-wide Document pool collections reference rather than own
        (a document can be attached to zero, one, or many collections).
        Async ingestion (URL crawl, file jobs) is not yet on this door —
        dashboard-only for now; use POST here for a direct synchronous
        upload, or POST /v1/documents/push for URL/inline-content ingestion.
      security:
        - BearerAuth: []
      responses:
        '200':
          description: This tenant's documents.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      documents:
                        type: array
                        items:
                          $ref: '#/components/schemas/Document'
                      total: { type: integer }
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Documents]
      summary: Upload a document (base64), extract, chunk, and embed it
      description: |
        Synchronous — the request blocks until extraction/chunking/embedding
        completes. If `collectionIds` names exactly one collection, that
        collection's own `default_embedding_model`/`default_chunking_strategy`/
        `default_chunk_size`/`default_chunk_overlap` pre-fill this upload
        (ambiguous, so skipped, with more than one). Every `collectionIds`
        entry must belong to this tenant or the whole request is rejected.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fileBase64, fileName]
              properties:
                fileBase64: { type: string, description: Base64-encoded file content, max 10 MB decoded. }
                fileName: { type: string }
                category: { type: string, example: document }
                collectionIds:
                  type: array
                  items: { type: string }
      responses:
        '201':
          description: Document created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    $ref: '#/components/schemas/Document'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: File exceeds the 10 MB limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '422':
          description: No text could be extracted, or the document produced no usable chunks.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/documents/push:
    post:
      tags: [Documents]
      summary: Push a URL or inline content as a document
      description: |
        Synchronous, upserts by a deterministic `source_id` (a hash of the
        URL, or of the first 200 characters of inline content, unless you
        supply your own `sourceId`) — pushing the same URL/content twice
        re-syncs the same document rather than creating a duplicate. `url`
        must be HTTPS with no embedded credentials.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                url: { type: string, description: Mutually exclusive with content. }
                content: { type: string, description: Mutually exclusive with url. }
                title: { type: string }
                sourceId: { type: string, description: Defaults to a deterministic hash if omitted. }
                category: { type: string, example: document }
                collectionIds:
                  type: array
                  items: { type: string }
      responses:
        '200':
          description: Pushed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      source_id: { type: string }
                      chunks: { type: integer }
                      title: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          description: No extractable text found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/documents/jobs/url:
    post:
      tags: [Documents]
      summary: Enqueue an async URL crawl ingestion job
      description: |
        Asynchronous, unlike POST /v1/documents/push — crawls up to `depth`
        levels deep (0-3, sitemap-driven when available, falling back to a
        link-following crawl), chunks, and embeds every discovered page,
        then attaches the resulting document to `collectionIds` (each of
        which must belong to this tenant). Returns immediately with a job
        id; poll `GET /v1/documents/jobs/{id}` for status.

        **Known limitation**: cancellation is cooperative (checked between
        page fetches / chunk embeds) but there is no crash-recovery sweep —
        if the process running the job restarts mid-crawl, the job is left
        `running` indefinitely rather than automatically retried or failed.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url: { type: string, description: Must be HTTPS with no embedded credentials. }
                depth: { type: integer, minimum: 0, maximum: 3, default: 0 }
                category: { type: string, example: document }
                collectionIds:
                  type: array
                  items: { type: string }
      responses:
        '202':
          description: Job enqueued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestionJobRef'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/documents/jobs/file:
    post:
      tags: [Documents]
      summary: Enqueue an async file ingestion job
      description: |
        Asynchronous counterpart to POST /v1/documents — same extraction/
        chunking/embedding pipeline, but queued and processed in the
        background rather than blocking the request. Prefer this for large
        files where a synchronous upload might approach typical HTTP
        request timeouts. Same crash-recovery limitation as the URL job
        above — see its description.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fileBase64, fileName]
              properties:
                fileBase64: { type: string, description: Base64-encoded file content, max 10 MB decoded. }
                fileName: { type: string }
                category: { type: string, example: document }
                collectionIds:
                  type: array
                  items: { type: string }
      responses:
        '202':
          description: Job enqueued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IngestionJobRef'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '413':
          description: File exceeds the 10 MB limit.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/documents/jobs:
    get:
      tags: [Documents]
      summary: List ingestion jobs
      security:
        - BearerAuth: []
      parameters:
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: pageSize
          in: query
          schema: { type: integer, default: 20, maximum: 50 }
        - name: status
          in: query
          schema: { type: string, enum: [pending, running, completed, failed, cancelled] }
      responses:
        '200':
          description: Ingestion jobs, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      jobs:
                        type: array
                        items:
                          $ref: '#/components/schemas/IngestionJob'
                      pagination:
                        type: object
                        properties:
                          page: { type: integer }
                          pageSize: { type: integer }
                          total: { type: integer }
                          totalPages: { type: integer }
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/documents/jobs/{id}:
    get:
      tags: [Documents]
      summary: Get an ingestion job's status
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Job status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      jobId: { type: string }
                      status: { type: string, enum: [pending, running, completed, failed, cancelled] }
                      stage: { type: string, nullable: true }
                      progress: { type: integer, minimum: 0, maximum: 100 }
                      entry:
                        description: Present once status is "completed" — the resulting document's summary.
                        type: object
                      error: { type: string, nullable: true }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/documents/jobs/{id}/cancel:
    post:
      tags: [Documents]
      summary: Cancel a pending or running ingestion job
      description: Cooperative — the worker notices and stops at its next checkpoint, not instantly.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Cancelled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      job:
                        $ref: '#/components/schemas/IngestionJob'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Job is already in a terminal state (completed/failed/cancelled) and cannot be cancelled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/documents/{id}:
    get:
      tags: [Documents]
      summary: Get a document, its collections, and its full chunk list
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The document.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    allOf:
                      - $ref: '#/components/schemas/Document'
                      - type: object
                        properties:
                          chunkList:
                            type: array
                            items:
                              type: object
                              properties:
                                id: { type: string }
                                index: { type: integer }
                                text: { type: string }
                                metadata: { type: object }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Documents]
      summary: Delete a document
      description: Removes the document and its embeddings. Cascades collection attachments via FK.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Document deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  # ==========================================
  # CORE RUNTIME
  # ==========================================

  /v1/run:
    post:
      tags: [Core Runtime]
      summary: Run any intent, for any custom domain
      description: |
        The primary way to invoke LiyaEngine. Pass a domain and intent key
        and get back a structured, schema-validated response — no
        per-domain URL to remember. A path-based form
        (`POST /v1/{domain}/{intent}`) also exists for REST-style routing;
        both reach the same engine.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain, intent, input]
              properties:
                domain:
                  type: string
                  example: hr-policy
                intent:
                  type: string
                  example: answer_question
                input:
                  type: object
                  additionalProperties: true
                session_id:
                  type: string
                  description: Continue an existing session (optional)
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/QuotaExceeded'

  /v1/run/stream:
    post:
      tags: [Core Runtime]
      summary: Run any intent, streamed
      description: Same engine and request shape as `POST /v1/run`, delivered as a server-sent-events token stream instead of one JSON response.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [domain, intent, input]
              properties:
                domain: { type: string }
                intent: { type: string }
                input: { type: object, additionalProperties: true }
      responses:
        '200':
          description: text/event-stream of incremental tokens, ending with a final done event
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/guardrails/check:
    post:
      tags: [Core Runtime]
      summary: Run content through your Guardrail Policy, standalone
      description: |
        Runs caller-supplied content through your tenant's real
        GuardrailsPipeline — the same PII detection, content policy, schema/
        action validation, and hallucination-check stages that run inside
        `POST /v1/run` — with **no Intent, Agent, or Domain involved at
        all**. Use this to screen text from your own systems (a support
        ticket, a form submission, a third-party LLM's output) without
        routing it through an Intent first.

        Pass `policy_id` to check against one of your named Guardrail
        Policies (see the dashboard's Guardrails page), or omit it to use
        your tenant's default policy.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [stage, content]
              properties:
                stage:
                  type: string
                  enum: [pre_llm, post_llm]
                  description: pre_llm checks input-side stages (PII, content policy). post_llm checks output-side stages (schema validation, action validation, hallucination check).
                content:
                  description: A plain string (for pre_llm, or a post_llm response that's just text) or a JSON object (a post_llm response shaped like `{ message, actions }`).
                  oneOf:
                    - type: string
                    - type: object
                      additionalProperties: true
                policy_id:
                  type: string
                  description: A specific Guardrail Policy's id. Omit to use your tenant's default policy.
            examples:
              pii_check:
                summary: Check input for PII before you send it anywhere
                value:
                  stage: pre_llm
                  content: "Please refund john@example.com for order #4821"
      responses:
        '200':
          description: Check completed (passed or blocked — both are 200s; `data.passed` tells you which)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GuardrailCheckResponse'
              examples:
                blocked_pii:
                  summary: PII detected and redacted
                  value:
                    success: true
                    data:
                      passed: true
                      content: "Please refund [REDACTED] for order #4821"
                      issues:
                        - code: PII_EMAIL_REDACTED
                          severity: info
                          message: "1 email instance(s) redacted"
                          action_taken: modified
                          details: { count: 1 }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: The given policy_id was not found for your tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/agents:
    get:
      tags: [Agents]
      summary: Flat catalog of this tenant's Agents
      description: Every active Agent plus its callable endpoint — external discovery and the dashboard's API Explorer page.
      security:
        - BearerAuth: []
      responses:
        '200':
          description: This tenant's active agents.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      agents:
                        type: array
                        items:
                          type: object
                          properties:
                            agent: { type: string, description: The agent_key }
                            displayName: { type: string }
                            description: { type: string, nullable: true }
                            goal: { type: string }
                            status: { type: string }
                            endpoint: { type: string, example: /v1/agents/support-triage/run }
                            method: { type: string, example: POST }
                            inputSchema: { type: object }
                      total: { type: integer }
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Agents]
      summary: Create an Agent
      description: |
        Created in `draft` status — use `POST /v1/agents/{agentKey}/deploy`
        to activate it for execution. Same validation and behavior as the
        dashboard's Agent Runtime page; both call
        services/resources/agentService.ts.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateAgentRequest'
      responses:
        '201':
          description: Agent created, in draft status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      agent:
                        $ref: '#/components/schemas/Agent'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Agentic tool-calling isn't enabled on this plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          $ref: '#/components/responses/Conflict'

  /v1/agents/{agentKey}:
    get:
      tags: [Agents]
      summary: Get a single Agent
      security:
        - BearerAuth: []
      parameters:
        - name: agentKey
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The agent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      agent:
                        $ref: '#/components/schemas/Agent'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Agents]
      summary: Update an Agent's configuration
      security:
        - BearerAuth: []
      parameters:
        - name: agentKey
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateAgentRequest'
      responses:
        '200':
          description: Agent updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      agent:
                        $ref: '#/components/schemas/Agent'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Attempted to set status 'active' directly — use the deploy endpoint instead
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    delete:
      tags: [Agents]
      summary: Delete an Agent
      description: Soft delete — sets status to 'inactive'. There is no separate is_active flag; 'inactive' doubles as both "deleted" and "manually paused."
      security:
        - BearerAuth: []
      parameters:
        - name: agentKey
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Agent deleted (deactivated).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/agents/{agentKey}/deploy:
    post:
      tags: [Agents]
      summary: Deploy an Agent
      description: |
        Activates an agent for execution — a deliberate, separately audited
        transition distinct from an ordinary PATCH, so clients can
        distinguish routine edits from publishing an agent live.
      security:
        - BearerAuth: []
      parameters:
        - name: agentKey
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Agent activated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      agent:
                        $ref: '#/components/schemas/Agent'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/agents/{agentKey}/run:
    post:
      tags: [Core Runtime]
      summary: Run a saved Agent
      description: |
        Runs a standalone Agent (bound to a set of intents, workflows,
        actions, and knowledge domains) by its tenant-unique key — the same
        execution path the dashboard's Agent Runtime test-run uses.
      security:
        - BearerAuth: []
      parameters:
        - name: agentKey
          in: path
          required: true
          schema:
            type: string
          description: The agent's url-safe, tenant-unique key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [input]
              properties:
                input:
                  type: object
                  required: [message]
                  properties:
                    message:
                      type: string
                    user_id:
                      type: string
                      description: Optional end-user identifier for your own tracking; also accepted as input.user.id
                  additionalProperties: true
      responses:
        '200':
          description: Agent run completed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      run_id: { type: string }
                      status: { type: string }
                      output: { type: string }
                      steps: { type: integer }
                      total_cost: { type: number }
                      total_latency_ms: { type: integer, nullable: true }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          description: Blocked by a guardrail policy
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/agents/{agentKey}/runs:
    get:
      tags: [Core Runtime]
      summary: List an Agent's past runs
      description: |
        History for a saved Agent, so a caller who only holds an API key
        (no dashboard access) can look up past runs — e.g. by the
        `session_id` a prior `POST .../run` response returned.
      security:
        - BearerAuth: []
      parameters:
        - name: agentKey
          in: path
          required: true
          schema: { type: string }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: pageSize
          in: query
          schema: { type: integer, default: 20 }
          description: Clamped to a maximum of 50.
        - name: status
          in: query
          schema: { type: string }
        - name: session_id
          in: query
          schema: { type: string }
      responses:
        '200':
          description: A page of past runs, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      runs:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string }
                            session_id: { type: string, nullable: true }
                            status: { type: string }
                            trigger: { type: string }
                            started_at: { type: string, format: date-time }
                            completed_at: { type: string, format: date-time, nullable: true }
                            total_cost: { type: number }
                            total_latency_ms: { type: integer, nullable: true }
                            total_input_tokens: { type: integer }
                            total_output_tokens: { type: integer }
                            step_count: { type: integer }
                            input: { type: object }
                      pagination:
                        type: object
                        properties:
                          page: { type: integer }
                          pageSize: { type: integer }
                          total: { type: integer }
                          totalPages: { type: integer }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/agents/{agentKey}/runs/{runId}:
    get:
      tags: [Core Runtime]
      summary: Get a single past Agent run
      description: A single run's full detail, including its step-by-step trace.
      security:
        - BearerAuth: []
      parameters:
        - name: agentKey
          in: path
          required: true
          schema: { type: string }
        - name: runId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The run, with its ordered steps.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      run: { type: object }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/agents/{agentKey}/sessions:
    get:
      tags: [Core Runtime]
      summary: List an Agent's sessions
      description: One row per real conversation/call, not per turn — the public counterpart to the dashboard's session list.
      security:
        - BearerAuth: []
      parameters:
        - name: agentKey
          in: path
          required: true
          schema: { type: string }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: pageSize
          in: query
          schema: { type: integer, default: 20 }
          description: Clamped to a maximum of 50.
        - name: status
          in: query
          schema: { type: string }
      responses:
        '200':
          description: A page of sessions, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      sessions:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string }
                            session_id: { type: string }
                            status: { type: string }
                            trigger: { type: string }
                            turn_count: { type: integer }
                            started_at: { type: string, format: date-time }
                            last_activity_at: { type: string, format: date-time }
                            completed_at: { type: string, format: date-time, nullable: true }
                            total_cost: { type: number }
                            total_latency_ms: { type: integer, nullable: true }
                            total_input_tokens: { type: integer }
                            total_output_tokens: { type: integer }
                      pagination:
                        type: object
                        properties:
                          page: { type: integer }
                          pageSize: { type: integer }
                          total: { type: integer }
                          totalPages: { type: integer }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/agents/{agentKey}/sessions/{sessionId}/transcript:
    get:
      tags: [Core Runtime]
      summary: Get a session's full transcript
      description: Every turn of one conversation/call, in chronological order, as a single response.
      security:
        - BearerAuth: []
      parameters:
        - name: agentKey
          in: path
          required: true
          schema: { type: string }
        - name: sessionId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The session plus its ordered turns.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      session: { type: object }
                      turns:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string }
                            status: { type: string }
                            input: { type: object }
                            output: { type: string, nullable: true }
                            error_message: { type: string, nullable: true }
                            started_at: { type: string, format: date-time }
                            completed_at: { type: string, format: date-time, nullable: true }
                            total_cost: { type: number }
                            total_latency_ms: { type: integer, nullable: true }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/vapi/{agentKey}/chat/completions:
    post:
      tags: [Core Runtime]
      summary: Vapi Custom-LLM bridge for a saved Agent
      description: |
        OpenAI-chat-completions-shaped endpoint for Vapi's "Custom LLM"
        assistant mode — Vapi POSTs the call transcript here and expects an
        OpenAI `chat.completion` response back. Internally this runs the same
        Agent execution path as `POST /v1/agents/{agentKey}/run`, using the
        call's own id as the session id so multi-turn continuity works across
        an entire phone call. Requires a `LiyaVapiBinding` configured for this
        agent (call-volume guardrails: max call length, max calls/day, max
        calls/day per caller) — returns 404 if none exists.
      security:
        - BearerAuth: []
      parameters:
        - name: agentKey
          in: path
          required: true
          schema:
            type: string
          description: The agent's url-safe, tenant-unique key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [messages]
              properties:
                model:
                  type: string
                messages:
                  type: array
                  items:
                    type: object
                    required: [role]
                    properties:
                      role: { type: string }
                      content: { type: string, nullable: true }
                    additionalProperties: true
                call:
                  type: object
                  description: Vapi's call metadata — used as the session id and (via customer.number) the per-caller rate-limit key.
                  properties:
                    id: { type: string }
                    customer:
                      type: object
                      properties:
                        number: { type: string }
                  additionalProperties: true
              additionalProperties: true
      responses:
        '200':
          description: |
            An OpenAI `chat.completion` response. Returned even when a
            guardrail (inactive binding, daily/per-caller call cap, call time
            limit) or an internal agent failure prevents the agent from
            actually running — the assistant's spoken reply explains the
            situation instead of the caller getting a raw error mid-call.
          content:
            application/json:
              schema:
                type: object
                properties:
                  id: { type: string }
                  object: { type: string, example: chat.completion }
                  created: { type: integer }
                  model: { type: string }
                  choices:
                    type: array
                    items:
                      type: object
                      properties:
                        index: { type: integer }
                        message:
                          type: object
                          properties:
                            role: { type: string, example: assistant }
                            content: { type: string }
                        finish_reason: { type: string }
                  usage:
                    type: object
                    properties:
                      prompt_tokens: { type: integer }
                      completion_tokens: { type: integer }
                      total_tokens: { type: integer }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/workflows:
    get:
      tags: [Core Runtime]
      summary: List this tenant's workflows
      description: |
        Check what's available — and whether it's active — before triggering
        one, without dashboard access.
      security:
        - BearerAuth: []
      responses:
        '200':
          description: This tenant's workflows, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Workflow'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Workflows]
      summary: Create a workflow
      description: |
        Created in `draft` status — use `POST /v1/workflows/{workflowId}/deploy`
        to activate it for execution. Same validation and behavior as the
        dashboard's Workflow Builder; both call
        services/resources/workflowService.ts.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateWorkflowRequest'
      responses:
        '201':
          description: Workflow created, in draft status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      workflow:
                        $ref: '#/components/schemas/Workflow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/workflows/{workflowId}:
    get:
      tags: [Core Runtime]
      summary: Get a single workflow
      description: A single workflow's current definition and status.
      security:
        - BearerAuth: []
      parameters:
        - name: workflowId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The workflow.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    $ref: '#/components/schemas/Workflow'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Workflows]
      summary: Update a workflow's configuration
      description: |
        `steps` is upsert-by-id, not replace-all — steps with a matching
        existing id are updated in place, new ones (no id, or an id not
        already on this workflow) are created, and any existing step id
        omitted from the array is deleted.
      security:
        - BearerAuth: []
      parameters:
        - name: workflowId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateWorkflowRequest'
      responses:
        '200':
          description: Workflow updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      workflow:
                        $ref: '#/components/schemas/Workflow'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: workflow_key already taken by another workflow in this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
    delete:
      tags: [Workflows]
      summary: Delete a workflow
      description: A hard delete — unlike Collections and Agents, there is no soft-delete/inactive state for a removed workflow.
      security:
        - BearerAuth: []
      parameters:
        - name: workflowId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Workflow deleted.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/workflows/{workflowId}/toggle:
    patch:
      tags: [Workflows]
      summary: Toggle a workflow's runtime availability
      description: |
        Flips `is_active` on an already-deployed workflow — a pure runtime
        on/off switch, distinct from deploy. Blocked on a `draft` workflow
        (409 `DEPLOY_REQUIRED`) — deploy it first.
      security:
        - BearerAuth: []
      parameters:
        - name: workflowId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Toggled.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      workflow:
                        $ref: '#/components/schemas/Workflow'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Workflow is still in draft — deploy it before toggling
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/workflows/{workflowId}/deploy:
    post:
      tags: [Workflows]
      summary: Deploy a workflow
      description: |
        The draft→published transition — sets `status` and `is_active`
        together. On first deploy of a webhook-triggered workflow, also
        mints the webhook infrastructure (slug + secret) and returns the
        full webhook URL and plaintext secret **exactly once** — capture it
        immediately, it is never returned again.
      security:
        - BearerAuth: []
      parameters:
        - name: workflowId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deployed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      workflow:
                        $ref: '#/components/schemas/Workflow'
                      webhook_url: { type: string, description: Present only when a webhook secret was newly generated by this call. }
                      webhook_secret: { type: string, description: Plaintext, shown once. Present only when newly generated by this call. }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/workflows/{workflowId}/webhook-secret/rotate:
    post:
      tags: [Workflows]
      summary: Rotate a webhook workflow's signing secret
      description: |
        Only valid for a webhook-triggered, already-deployed workflow (409
        `NOT_A_WEBHOOK_WORKFLOW` / `DEPLOY_REQUIRED` otherwise). The
        replacement secret is returned **exactly once** — capture it
        immediately. The previous secret stays valid for `grace_period_seconds`
        (default 300, max 3600) so external senders can roll over without
        downtime; pass `0` for an immediate hard cutover.
      security:
        - BearerAuth: []
      parameters:
        - name: workflowId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RotateWebhookSecretRequest'
      responses:
        '200':
          description: Rotated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      webhook_url: { type: string }
                      webhook_secret: { type: string, description: Plaintext, shown once. }
                      previous_secret_valid_until: { type: string, format: date-time, nullable: true }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Not a webhook-triggered workflow, or not yet deployed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/workflows/{workflowId}/run:
    post:
      tags: [Core Runtime]
      summary: Run a Workflow synchronously
      description: |
        Runs a Workflow and waits for the result — the authenticated
        counterpart to the existing async, HMAC-signed webhook trigger
        (`POST /webhooks/workflows/{slug}`), which stays available for
        event-driven callers that don't hold an API key.
      security:
        - BearerAuth: []
      parameters:
        - name: workflowId
          in: path
          required: true
          schema:
            type: string
          description: The workflow's database id
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                input:
                  type: object
                  additionalProperties: true
      responses:
        '200':
          description: Workflow run completed (or paused, needing more input)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      run_id: { type: string }
                      status:
                        type: string
                        enum: [completed, needs_input, failed]
                      trace:
                        type: array
                        items:
                          type: object
                      missing_parameter:
                        type: object
                        description: Present only when status is needs_input
        '404':
          $ref: '#/components/responses/NotFound'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '500':
          description: Workflow run failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/workflows/{workflowId}/runs:
    get:
      tags: [Core Runtime]
      summary: List a Workflow's past runs
      security:
        - BearerAuth: []
      parameters:
        - name: workflowId
          in: path
          required: true
          schema: { type: string }
        - name: page
          in: query
          schema: { type: integer, default: 1 }
        - name: pageSize
          in: query
          schema: { type: integer, default: 20 }
          description: Clamped to a maximum of 50.
        - name: status
          in: query
          schema: { type: string }
      responses:
        '200':
          description: A page of past runs, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      runs:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string }
                            status: { type: string }
                            trigger: { type: string }
                            started_at: { type: string, format: date-time }
                            completed_at: { type: string, format: date-time, nullable: true }
                            total_latency_ms: { type: integer, nullable: true }
                            step_count: { type: integer }
                            input: { type: object }
                      pagination:
                        type: object
                        properties:
                          page: { type: integer }
                          pageSize: { type: integer }
                          total: { type: integer }
                          totalPages: { type: integer }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/workflows/{workflowId}/runs/{runId}:
    get:
      tags: [Core Runtime]
      summary: Get a single past Workflow run
      description: A single run's full detail, including its step-by-step trace.
      security:
        - BearerAuth: []
      parameters:
        - name: workflowId
          in: path
          required: true
          schema: { type: string }
        - name: runId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The run, with its ordered steps.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      run: { type: object }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/domains/{key}/query:
    post:
      tags: [Core Runtime]
      summary: Query a domain's knowledge base directly
      description: |
        Direct knowledge-base retrieval — no LLM call. Returns the raw
        retrieved chunks and similarity scores, the same retrieval scope an
        Agent's `document_search` tool would see for this domain, for
        building custom search/citation UX without wrapping every lookup in
        a full intent call. Requires the Growth-plan custom-domains feature.
      security:
        - BearerAuth: []
      parameters:
        - name: key
          in: path
          required: true
          schema:
            type: string
          description: The domain's key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [query]
              properties:
                query:
                  type: string
                top_k:
                  type: integer
                  description: Max results to return (default 5, max 20)
      responses:
        '200':
          description: Retrieved chunks
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      results:
                        type: array
                        items:
                          type: object
                          properties:
                            content: { type: string }
                            source: { type: string }
                            similarity: { type: number }
                      total: { type: integer }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Custom domains not enabled on this plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/domains/{key}/intents/{intentKey}:
    get:
      tags: [Domains]
      summary: Get one intent
      description: |
        No equivalent existed on either door (dashboard or v1) before the
        Domains/Intents parity slice — only the list route did.
      security:
        - BearerAuth: []
      parameters:
        - { name: key, in: path, required: true, schema: { type: string } }
        - { name: intentKey, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: The intent.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      intent:
                        $ref: '#/components/schemas/CustomIntent'
        '404':
          description: Domain or intent not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/domains/{key}/intents/{intentKey}/versions:
    get:
      tags: [Domains]
      summary: List an intent's version history
      description: |
        Full content snapshots, newest first (max 50). Every `/v1` mutation
        is already explicit — unlike the dashboard's `logHistory` flag
        (which only snapshots on an explicit Save, not silent autosave), an
        API-key caller gets a version snapshot on every update that changes
        content.
      security:
        - BearerAuth: []
      parameters:
        - { name: key, in: path, required: true, schema: { type: string } }
        - { name: intentKey, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: Version history, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      versions:
                        type: array
                        items:
                          $ref: '#/components/schemas/CustomIntentVersionSummary'

  /v1/domains/{key}/intents/{intentKey}/versions/{versionNumber}:
    get:
      tags: [Domains]
      summary: Get one version's full content snapshot
      security:
        - BearerAuth: []
      parameters:
        - { name: key, in: path, required: true, schema: { type: string } }
        - { name: intentKey, in: path, required: true, schema: { type: string } }
        - { name: versionNumber, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: The version's full content snapshot.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      version:
                        $ref: '#/components/schemas/CustomIntentVersionSummary'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/domains/{key}/intents/{intentKey}/versions/{versionNumber}/restore:
    post:
      tags: [Domains]
      summary: Restore the intent to a prior version
      description: |
        Writes the version's snapshot back onto the live intent row. The
        restore itself is versioned too (`change_type: "restore"`) — never a
        dead end, just another entry you can restore away from.
      security:
        - BearerAuth: []
      parameters:
        - { name: key, in: path, required: true, schema: { type: string } }
        - { name: intentKey, in: path, required: true, schema: { type: string } }
        - { name: versionNumber, in: path, required: true, schema: { type: integer } }
      responses:
        '200':
          description: The intent, restored.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      intent:
                        $ref: '#/components/schemas/CustomIntent'
        '404':
          $ref: '#/components/responses/NotFound'

  # ==========================================
  # EVALUATION-AS-A-SERVICE
  # ==========================================

  /v1/evals/score:
    post:
      tags: [Evaluations]
      summary: Score one (input, output) pair synchronously
      description: |
        Scores a single response directly — no domain, intent, or dataset
        required. Runs the same sequence Evaluation Studio uses: an optional
        deterministic `expected_output`/custom-scorer check first (gates the
        judge call if it fails), then an LLM judge call. Rate limited to 30
        requests/minute per tenant.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [input, output]
              properties:
                input:
                  type: object
                  description: The input your system passed to your own model/pipeline.
                output:
                  type: string
                  description: The response to score.
                expected_output:
                  description: Optional. A string (substring match) or object (partial deep-equality) checked before the judge runs.
                message:
                  type: string
                custom_scorer_expression:
                  type: string
                  description: Optional boolean expression evaluated against input/output/expected (see the dashboard's custom scorer docs for grammar).
      responses:
        '200':
          description: Score result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      passed: { type: boolean }
                      score: { type: number, nullable: true, description: Mean of the judge's 1-5 dimension scores; null if no judge dimension was scored. }
                      checks:
                        type: array
                        items:
                          type: object
                          properties:
                            check_type: { type: string }
                            passed: { type: boolean }
                            judge_score: { type: number }
                            judge_rationale: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '503':
          description: Judge model provider is not available
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/evals/datasets:
    get:
      tags: [Evaluations]
      summary: List this tenant's datasets
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Datasets, each with a case count
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      datasets:
                        type: array
                        items: { $ref: '#/components/schemas/EvalDataset' }
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Evaluations]
      summary: Create a dataset, optionally with inline cases
      description: |
        Reusable cases for POST /v1/evals/runs' dataset_id mode. Cases can
        also be submitted fully inline on a run — a dataset is only needed
        when you want to reuse the same cases across multiple runs.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string }
                description: { type: string }
                cases:
                  type: array
                  items:
                    type: object
                    required: [input]
                    properties:
                      input: { type: object }
                      message: { type: string }
                      expected_output: {}
                      notes: { type: string }
      responses:
        '201':
          description: Dataset created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      name: { type: string }
                      description: { type: string, nullable: true }
                      case_count: { type: integer }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/evals/datasets/templates:
    get:
      tags: [Evaluations]
      summary: List starter dataset templates
      description: Shared, read-only starter content every tenant can preview and clone into their own workspace via the clone endpoint below.
      security:
        - BearerAuth: []
      responses:
        '200':
          description: Templates, each with a preview of its first few cases
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      templates:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string }
                            name: { type: string }
                            description: { type: string, nullable: true }
                            case_count: { type: integer }
                            example_cases:
                              type: array
                              items: { type: object }
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/evals/datasets/templates/{id}/clone:
    post:
      tags: [Evaluations]
      summary: Clone a starter template into this tenant's own dataset
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '201':
          description: The new, tenant-owned dataset (with its cloned cases)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      dataset: { $ref: '#/components/schemas/EvalDataset' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/evals/datasets/{id}:
    get:
      tags: [Evaluations]
      summary: Get a dataset and its cases
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The dataset, including every case
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      dataset: { $ref: '#/components/schemas/EvalDataset' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Evaluations]
      summary: Rename or re-describe a dataset
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: At least one of name or description is required.
              properties:
                name: { type: string }
                description: { type: string, nullable: true }
      responses:
        '200':
          description: The updated dataset
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      dataset: { $ref: '#/components/schemas/EvalDataset' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Evaluations]
      summary: Delete a dataset (and its cases)
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/evals/datasets/{id}/import:
    post:
      tags: [Evaluations]
      summary: Bulk-add cases to a dataset from a JSON body
      description: |
        API-key counterpart to the dashboard's file-upload import — takes an
        already-structured JSON array directly rather than a multipart file
        (no CSV support here; convert client-side if needed).
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cases]
              properties:
                cases:
                  type: array
                  minItems: 1
                  items:
                    type: object
                    required: [input]
                    properties:
                      input: { type: object }
                      message: { type: string }
                      expected_output: {}
                      notes: { type: string }
      responses:
        '201':
          description: Cases imported
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      imported: { type: integer }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/evals/datasets/{id}/cases:
    post:
      tags: [Evaluations]
      summary: Add a single case to a dataset
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [input]
              properties:
                input: { type: object }
                message: { type: string, nullable: true }
                expected_output: {}
                notes: { type: string, nullable: true }
      responses:
        '201':
          description: Case created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      case: { $ref: '#/components/schemas/EvalCase' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/evals/cases/{caseId}:
    patch:
      tags: [Evaluations]
      summary: Update a case
      security:
        - BearerAuth: []
      parameters:
        - name: caseId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: At least one of input, message, expected_output, or notes is required.
              properties:
                input: { type: object }
                message: { type: string, nullable: true }
                expected_output: {}
                notes: { type: string, nullable: true }
      responses:
        '200':
          description: The updated case
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      case: { $ref: '#/components/schemas/EvalCase' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Evaluations]
      summary: Delete a case
      security:
        - BearerAuth: []
      parameters:
        - name: caseId
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/evals/suites:
    get:
      tags: [Evaluations]
      summary: List this tenant's suites
      security:
        - BearerAuth: []
      parameters:
        - name: domain_key
          in: query
          schema: { type: string }
        - name: intent_key
          in: query
          schema: { type: string }
      responses:
        '200':
          description: Suites, each with its dataset's name/case count and most recent run
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      suites:
                        type: array
                        items: { $ref: '#/components/schemas/EvalSuite' }
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Evaluations]
      summary: Create a suite
      description: |
        A suite has at most one custom scorer — either an expression or a
        webhook, not both. The webhook URL is checked at save time
        (best-effort — re-checked again on every run, since DNS/URL targets
        can change in between).
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, domain_key, intent_key, dataset_id]
              properties:
                name: { type: string }
                domain_key: { type: string }
                intent_key: { type: string }
                dataset_id: { type: string }
                custom_scorer_expression: { type: string }
                custom_scorer_label: { type: string }
                custom_scorer_webhook_url: { type: string }
                custom_scorer_webhook_secret: { type: string, description: Write-only — never returned; only a custom_scorer_webhook_secret_set boolean comes back. }
      responses:
        '201':
          description: Suite created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      suite: { $ref: '#/components/schemas/EvalSuite' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: The referenced intent or dataset was not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/evals/suites/{id}:
    patch:
      tags: [Evaluations]
      summary: Update a suite
      description: Does not allow changing domain_key/intent_key — that changes what the suite tests, which should be a new suite (and a new baseline), not an edit to this one and its run history.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              description: At least one field to update is required.
              properties:
                name: { type: string }
                dataset_id: { type: string }
                custom_scorer_expression: { type: string, nullable: true }
                custom_scorer_label: { type: string, nullable: true }
                custom_scorer_webhook_url: { type: string, nullable: true }
                custom_scorer_webhook_secret: { type: string, nullable: true }
      responses:
        '200':
          description: The updated suite
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      suite: { $ref: '#/components/schemas/EvalSuite' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [Evaluations]
      summary: Delete a suite
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/evals/suites/{id}/baseline:
    post:
      tags: [Evaluations]
      summary: Pin (or clear) a suite's baseline run
      description: Every later completed run of this suite is automatically compared against whichever run is pinned here (see the baseline_comparison field GET /v1/evals/runs/{id} returns).
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [run_id]
              properties:
                run_id: { type: string, nullable: true, description: null clears the pinned baseline. }
      responses:
        '200':
          description: The updated suite
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      suite: { $ref: '#/components/schemas/EvalSuite' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Suite or run not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: The referenced run isn't completed yet
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/evals/suites/{id}/run:
    post:
      tags: [Evaluations]
      summary: Run a suite against its real intent
      description: |
        Calls engine.execute() against the suite's configured intent for
        every case in its dataset, scoring each response with the same
        sequence Evaluation Studio uses. Returns immediately with a
        `pending` run — poll `GET /v1/evals/runs/{id}`. Rate limited (shared
        with the other run-creating endpoints below) and gated against the
        tenant's monthly Evals budget.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                model:
                  type: string
                  description: Optional force_model override for this run only — the intent's own configured model is used when omitted.
      responses:
        '202':
          description: Run accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      run: { $ref: '#/components/schemas/EvalRun' }
        '400':
          description: The suite's dataset has no cases to run
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          description: Evals budget exceeded for this billing period, or Evals isn't available on this plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/evals/suites/{id}/runs:
    get:
      tags: [Evaluations]
      summary: List a suite's runs
      description: Convenience alias of GET /v1/evals/runs?suite_id={id}.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: This suite's runs, most recent first
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      runs:
                        type: array
                        items: { $ref: '#/components/schemas/EvalRun' }
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/evals/suites/{id}/compare-models:
    post:
      tags: [Evaluations]
      summary: Run a suite twice, once per model, for a head-to-head comparison
      description: |
        Creates two ordinary runs against the same suite with different
        force_model overrides — no new comparison logic here; once both
        complete, use GET .../runs/{id}/compare or
        POST .../runs/{id}/compare-pairwise on the returned run ids.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [model_a, model_b]
              properties:
                model_a: { type: string }
                model_b: { type: string }
      responses:
        '202':
          description: Both runs accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      run_a: { $ref: '#/components/schemas/EvalRun' }
                      run_b: { $ref: '#/components/schemas/EvalRun' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          description: Evals budget exceeded for this billing period, or Evals isn't available on this plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/evals/results/{resultId}/reviews:
    post:
      tags: [Evaluations]
      summary: Record a human review of one case result
      security:
        - BearerAuth: []
      parameters:
        - name: resultId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [verdict]
              properties:
                verdict: { type: string, enum: [agree, override, flag] }
                corrected_score: { type: number, description: "1-5. Required when verdict is 'override'." }
                note: { type: string }
      responses:
        '201':
          description: Review recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      review: { $ref: '#/components/schemas/EvalReview' }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/evals/reviews/{id}:
    delete:
      tags: [Evaluations]
      summary: Delete a review
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/evals/runs:
    get:
      tags: [Evaluations]
      summary: List this tenant's runs
      security:
        - BearerAuth: []
      parameters:
        - name: suite_id
          in: query
          schema: { type: string }
          description: Filter to one suite's runs (an alias of this filter also exists at GET /v1/evals/suites/{id}/runs).
      responses:
        '200':
          description: Runs, most recent first (capped at 50)
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      runs:
                        type: array
                        items: { $ref: '#/components/schemas/EvalRun' }
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Evaluations]
      summary: Submit a batch of (input, output) pairs for async scoring
      description: |
        Accepts either `dataset_id` + `[{case_id, output}]` (cases resolved
        against a previously-created dataset) or fully inline
        `[{input, output, expected_output?}]` (persisted as ephemeral cases
        for this run). Returns immediately with a run id — poll
        `GET /v1/evals/runs/:id` for status and results. Capped at 100 cases
        per request; rate limited to 10 requests/minute per tenant.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [cases]
              properties:
                dataset_id:
                  type: string
                  description: Optional. When set, every case must include case_id instead of input.
                cases:
                  type: array
                  minItems: 1
                  maxItems: 100
                  items:
                    type: object
                    required: [output]
                    properties:
                      case_id: { type: string }
                      input: { type: object }
                      message: { type: string }
                      expected_output: {}
                      output: { type: string }
                custom_scorer_expression: { type: string }
                custom_scorer_webhook_url: { type: string }
                custom_scorer_webhook_secret: { type: string }
      responses:
        '202':
          description: Run accepted, scoring in progress
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      run_id: { type: string }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: A referenced case_id was not found in the given dataset for this tenant
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/evals/runs/{id}:
    get:
      tags: [Evaluations]
      summary: Poll an eval run's status and results
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: Run status and, once complete, per-case results
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      id: { type: string }
                      status: { type: string, description: "pending | running | completed | failed | cancelled" }
                      progress: { type: integer }
                      mean_score: { type: number, nullable: true }
                      dimension_scores: { type: object }
                      cases_total: { type: integer }
                      cases_passed: { type: integer }
                      results:
                        type: array
                        items:
                          type: object
                          properties:
                            case_id: { type: string }
                            passed: { type: boolean }
                            scores: { type: array, items: { type: object } }
                            latency_ms: { type: integer, nullable: true }
                            error: { type: string, nullable: true }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'

  /v1/evals/runs/{id}/cancel:
    post:
      tags: [Evaluations]
      summary: Cancel a pending or running run
      description: Cooperative — the worker checks for cancellation before its next case and stops there; the case in progress when cancelled is not scored.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The cancelled run
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      run: { $ref: '#/components/schemas/EvalRun' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Run is already completed/failed/cancelled and cannot be cancelled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/evals/runs/{id}/resume:
    post:
      tags: [Evaluations]
      summary: Resume a failed run
      description: |
        Only a `failed` run can be resumed. Already-scored cases are
        skipped — this continues from where the run stopped rather than
        re-billing every case from scratch. Works for both execution modes:
        a `liya_intent` run re-reads its case inputs fresh off the Suite's
        dataset; an `external_output` run re-reads the payload it was
        originally submitted with.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200':
          description: The run, reset to pending and re-triggered
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      run: { $ref: '#/components/schemas/EvalRun' }
        '401':
          $ref: '#/components/responses/Unauthorized'
        '400':
          description: The run has nothing to resume from (no cases, or a pre-resumability external_output run with no saved submission)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: Only a failed run can be resumed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '429':
          description: Evals budget exceeded for this billing period, or Evals isn't available on this plan
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/evals/runs/{id}/compare:
    get:
      tags: [Evaluations]
      summary: Statistical comparison of two completed runs
      description: Pass/fail rate, mean score, cost, and latency deltas, with a significance test — a lightweight alternative to the LLM-judge pairwise comparison below.
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: against
          in: query
          required: true
          schema: { type: string }
          description: The run id to compare against.
      responses:
        '200':
          description: Comparison result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      comparison:
                        type: object
                        properties:
                          run_a: { type: object }
                          run_b: { type: object }
                          score_delta: { type: object }
                          winner: { type: string, nullable: true }
                          summary: { type: string }
                      run_a: { type: object }
                      run_b: { type: object }
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: One or both runs were not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: Both runs must be completed before comparing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  /v1/evals/runs/{id}/compare-pairwise:
    post:
      tags: [Evaluations]
      summary: LLM-judge head-to-head comparison of two completed runs
      description: |
        Real judge calls, cost-incurring — for each case both runs share, a
        judge picks a winner (a/b/tie) directly, rather than comparing
        independent scores statistically like GET .../compare above. Both
        runs must share the same intent_key (the judge is given the intent
        as task context).
      security:
        - BearerAuth: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [against]
              properties:
                against: { type: string, description: The run id to compare against. }
      responses:
        '200':
          description: Pairwise comparison result
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      comparison:
                        type: object
                        properties:
                          cases_compared: { type: integer }
                          total_cost_usd: { type: number }
                          unmatched_cases: { type: integer }
                      run_a: { type: object }
                      run_b: { type: object }
        '400':
          description: Missing 'against', comparing a run to itself, mismatched intents, or no comparable cases
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: One or both runs were not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '409':
          description: Both runs must be completed before comparing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'
        '503':
          description: Judge model provider is not available
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorEnvelope'

  # ==========================================
  # HIRING DOMAIN
  # ==========================================

  /v1/hiring/resume-analysis:
    post:
      tags: [Hiring]
      summary: Analyze resume
      description: Analyzes resume strengths, weaknesses, and ATS compatibility.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: user_123
                resume_text: "John Smith\nSoftware Engineer with 5 years experience in React and Node.js..."
                target_role: Senior Frontend Engineer
      responses:
        '200':
          description: Resume analysis result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/IntentResponse'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/QuotaExceeded'

  /v1/hiring/resume-improvement:
    post:
      tags: [Hiring]
      summary: Improve resume
      description: Generates specific improvement suggestions for a resume.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: user_123
                resume_text: "Jane Doe\nMarketing Manager..."
                target_role: Director of Marketing
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/job-fit-analysis:
    post:
      tags: [Hiring]
      summary: Analyze job fit
      description: Analyzes how well a candidate fits a specific job posting.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: user_123
                resume_text: "Alex Johnson..."
                job_title: Staff Engineer
                job_description: "We are looking for a Staff Engineer to lead..."
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/career-assessment:
    post:
      tags: [Hiring]
      summary: Assess career readiness
      description: Evaluates career readiness, trajectory, and market positioning.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/skill-gap-analysis:
    post:
      tags: [Hiring]
      summary: Identify skill gaps
      description: Identifies missing skills for a target role and recommends learning paths.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: user_123
                current_role: Junior Developer
                target_role: Senior Backend Engineer
                skills: [JavaScript, React, Node.js]
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/career-path-planning:
    post:
      tags: [Hiring]
      summary: Plan career path
      description: Generates a personalised career path with milestones and timelines.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/career-transition-planning:
    post:
      tags: [Hiring]
      summary: Plan career transition
      description: Creates a plan for transitioning to a new role or industry.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/mock-interview:
    post:
      tags: [Hiring]
      summary: Conduct mock interview
      description: AI-powered mock interview with questions and feedback. Use session_id to continue.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: user_123
                message: "I'm ready to start the interview"
                target_role: Product Manager
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/candidate-prescreen:
    post:
      tags: [Hiring]
      summary: Prescreen candidate
      description: Pre-screens a candidate profile against job requirements with a recommendation.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: employer_123
                resume_text: "Candidate resume..."
                job_title: Senior Engineer
                job_description: "We need..."
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/candidate-scoring:
    post:
      tags: [Hiring]
      summary: Score candidate
      description: Scores and ranks a candidate against job requirements (0–100).
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/coaching-session:
    post:
      tags: [Hiring]
      summary: Career coaching session
      description: Conversational career coaching. Use session_id to maintain context across turns.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: user_123
                message: How do I negotiate a salary increase?
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/general-chat:
    post:
      tags: [Hiring]
      summary: General career chat
      description: Open-ended conversation about career, hiring, or workplace topics.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: user_123
                message: What are the most in-demand skills for engineers in 2026?
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /v1/hiring/cover-letter-generation:
    post:
      tags: [Hiring]
      summary: Generate cover letter
      description: Generates a tailored cover letter based on a resume and job description.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: user_123
                resume_text: "Jane Doe\nSoftware Engineer with 4 years experience..."
                job_title: Senior Software Engineer
                job_description: "We are looking for a Senior Software Engineer to join..."
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/QuotaExceeded'

  /v1/hiring/intro-script-generation:
    post:
      tags: [Hiring]
      summary: Generate intro script
      description: Generates a compelling candidate introduction script for interviews or networking.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: user_123
                resume_text: "John Smith\nProduct Manager with 6 years experience..."
                target_role: Director of Product
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/QuotaExceeded'

  /v1/hiring/jd-analysis:
    post:
      tags: [Hiring]
      summary: Analyse job description
      description: Analyses a job description to extract key requirements, must-haves, and red flags.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: employer_123
                job_title: Head of Engineering
                job_description: "We are seeking a Head of Engineering to lead our growing team..."
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/QuotaExceeded'

  /v1/hiring/knockout-question-generation:
    post:
      tags: [Hiring]
      summary: Generate knockout questions
      description: Generates screening knockout questions to quickly filter unqualified candidates for a role.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: employer_123
                job_title: Senior Backend Engineer
                job_description: "We need a Senior Backend Engineer with strong Go and Kubernetes experience..."
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/QuotaExceeded'

  /v1/hiring/interview-question-generation:
    post:
      tags: [Hiring]
      summary: Generate interview questions
      description: Generates structured interview questions (behavioural, technical, situational) tailored to a role.
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/IntentRequest'
            example:
              input:
                user_id: employer_123
                job_title: Data Scientist
                job_description: "Looking for a Data Scientist to build ML pipelines..."
                interview_stage: technical
      responses:
        '200':
          $ref: '#/components/responses/IntentSuccess'
        '400':
          $ref: '#/components/responses/BadRequest'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/QuotaExceeded'

  # ==========================================
  # ADMIN — TENANTS
  # ==========================================

  /admin/tenants:
    post:
      tags: [Admin — Tenants]
      summary: Create tenant
      description: Provisions a new tenant and returns their API key. Save it — it won't be shown again.
      security:
        - AdminSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tenantId, tenantName]
              properties:
                tenantId:
                  type: string
                  example: acme-corp
                tenantName:
                  type: string
                  example: Acme Corp
                tenantType:
                  type: string
                  enum: [internal, partner, enterprise, trial]
                  default: partner
                enabledDomains:
                  type: array
                  items:
                    type: string
                  example: [hiring]
                billingPlan:
                  type: string
                  example: starter
                billingEmail:
                  type: string
                  example: billing@acme.com
                monthlyRequestQuota:
                  type: integer
                  nullable: true
                  description: null = unlimited
                monthlyTokenQuota:
                  type: integer
                  nullable: true
                rateLimitRpm:
                  type: integer
                  default: 60
      responses:
        '201':
          description: Tenant created with API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      tenant:
                        $ref: '#/components/schemas/Tenant'
                      apiKey:
                        type: string
                        description: Plaintext API key — one-time display only
                        example: liya_abc123xyz...
                  message:
                    type: string
        '409':
          description: Tenant already exists

    get:
      tags: [Admin — Tenants]
      summary: List tenants
      security:
        - AdminSecret: []
      parameters:
        - in: query
          name: isActive
          schema:
            type: boolean
        - in: query
          name: tenantType
          schema:
            type: string
        - in: query
          name: limit
          schema:
            type: integer
            default: 50
        - in: query
          name: offset
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of tenants
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Tenant'
                  pagination:
                    type: object
                    properties:
                      total:
                        type: integer
                      limit:
                        type: integer
                      offset:
                        type: integer

  /admin/tenants/{tenantId}:
    get:
      tags: [Admin — Tenants]
      summary: Get tenant
      security:
        - AdminSecret: []
      parameters:
        - name: tenantId
          in: path
          required: true
          description: The tenant ID
          schema:
            type: string
          example: test-tenant
      responses:
        '200':
          description: Tenant details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    $ref: '#/components/schemas/Tenant'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      tags: [Admin — Tenants]
      summary: Update tenant
      security:
        - AdminSecret: []
      parameters:
        - name: tenantId
          in: path
          required: true
          description: The tenant ID
          schema:
            type: string
          example: test-tenant
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                tenantName:
                  type: string
                isActive:
                  type: boolean
                enabledDomains:
                  type: array
                  items:
                    type: string
                billingPlan:
                  type: string
                monthlyRequestQuota:
                  type: integer
                  nullable: true
      responses:
        '200':
          description: Updated tenant

    delete:
      tags: [Admin — Tenants]
      summary: Deactivate tenant
      security:
        - AdminSecret: []
      parameters:
        - name: tenantId
          in: path
          required: true
          description: The tenant ID
          schema:
            type: string
          example: test-tenant
      responses:
        '200':
          description: Tenant deactivated

  # ==========================================
  # ADMIN — API KEYS
  # ==========================================

  /admin/tenants/{tenantId}/rotate-api-key:
    post:
      tags: [Admin — API Keys]
      summary: Rotate API key
      description: Revokes current key and generates a new one. The old key stops working immediately.
      security:
        - AdminSecret: []
      parameters:
        - name: tenantId
          in: path
          required: true
          description: The tenant ID
          schema:
            type: string
          example: test-tenant
      responses:
        '200':
          description: New API key
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      apiKey:
                        type: string
                        description: New plaintext API key — one-time display only
                  message:
                    type: string

  /admin/tenants/{tenantId}/revoke-api-key:
    post:
      tags: [Admin — API Keys]
      summary: Revoke API key
      description: Permanently revokes the current API key. The tenant cannot make requests until a new key is rotated.
      security:
        - AdminSecret: []
      parameters:
        - name: tenantId
          in: path
          required: true
          description: The tenant ID
          schema:
            type: string
          example: test-tenant
      responses:
        '200':
          description: Key revoked

  # ==========================================
  # ADMIN — USAGE
  # ==========================================

  /admin/tenants/{tenantId}/usage:
    get:
      tags: [Admin — Usage]
      summary: Get tenant usage
      description: Returns current month usage broken down by domain and intent.
      security:
        - AdminSecret: []
      parameters:
        - name: tenantId
          in: path
          required: true
          description: The tenant ID
          schema:
            type: string
          example: test-tenant
      responses:
        '200':
          description: Usage data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      tenant:
                        type: object
                        properties:
                          tenantId:
                            type: string
                          requestsThisMonth:
                            type: integer
                          tokensThisMonth:
                            type: string
                          costThisMonth:
                            type: string
                          monthlyRequestQuota:
                            type: integer
                            nullable: true
                      byDomain:
                        type: array
                        items:
                          type: object
                          properties:
                            domain:
                              type: string
                            total_requests:
                              type: integer
                            total_tokens:
                              type: integer
                            total_cost_usd:
                              type: number
                            intents:
                              type: object

# ==========================================
# AUTH
# ==========================================

  /auth/signup:
    post:
      tags: [Auth]
      summary: Sign up
      description: |
        Creates a new company account. Returns a JWT session token and the tenant's API key.
        **Save the API key — it will not be shown again.**

        A verification email is sent automatically. In non-production environments, `data.devVerifyUrl`
        is included in the response — copy the `token` query param and paste it into
        `GET /auth/verify-email` to verify your email without needing Resend configured.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [companyName, email, password]
              properties:
                companyName:
                  type: string
                  example: Acme Corp
                email:
                  type: string
                  format: email
                  example: dev@acme.com
                password:
                  type: string
                  minLength: 8
                  example: securepassword
                fullName:
                  type: string
                  example: Jane Smith
                plan:
                  type: string
                  enum: [starter, growth, enterprise]
                  default: starter
      responses:
        '201':
          description: Account created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      user:
                        $ref: '#/components/schemas/DashboardUser'
                      tenant:
                        $ref: '#/components/schemas/TenantProfile'
                      apiKey:
                        type: string
                        description: Plaintext API key — shown once only
                        example: liya_xxxxxxxxxxxx
                      token:
                        type: string
                        description: JWT session token
                      devVerifyUrl:
                        type: string
                        description: "DEV ONLY (non-production) — full verify URL. Copy the token param and use it with GET /auth/verify-email to test the verification flow."
                        example: "http://localhost:3007/verify-email?token=abc123..."
                      devNote:
                        type: string
                        description: DEV ONLY — instructions for using devVerifyUrl
                  message:
                    type: string
        '409':
          description: Email already registered

  /auth/login:
    post:
      tags: [Auth]
      summary: Log in
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      user:
                        $ref: '#/components/schemas/DashboardUser'
                      tenant:
                        $ref: '#/components/schemas/TenantProfile'
                      token:
                        type: string
        '401':
          description: Invalid credentials

  /auth/logout:
    post:
      tags: [Auth]
      summary: Log out
      description: Clears the session cookie.
      responses:
        '200':
          description: Logged out

  /auth/me:
    get:
      tags: [Auth]
      summary: Get current user
      security:
        - DashboardAuth: []
      responses:
        '200':
          description: Current user and tenant
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      user:
                        $ref: '#/components/schemas/DashboardUser'
                      tenant:
                        $ref: '#/components/schemas/TenantProfile'

  /auth/forgot-password:
    post:
      tags: [Auth]
      summary: Request password reset
      description: Sends a reset link to the email address. Always returns 200 (no enumeration).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        '200':
          description: Reset email sent (if account exists)

  /auth/reset-password:
    post:
      tags: [Auth]
      summary: Reset password
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token, newPassword]
              properties:
                token:
                  type: string
                newPassword:
                  type: string
                  minLength: 8
      responses:
        '200':
          description: Password updated
        '400':
          description: Invalid or expired token

  /auth/verify-email:
    get:
      tags: [Auth]
      summary: Verify email address
      description: Confirms ownership of the email by validating the one-time token sent on signup. Token expires after 24 hours.
      parameters:
        - name: token
          in: query
          required: true
          schema:
            type: string
          description: Plain verification token from the email link
      responses:
        '200':
          description: Email verified successfully
        '400':
          description: Invalid or expired token

  /auth/resend-verification:
    post:
      tags: [Auth]
      summary: Resend verification email
      description: Re-sends the verification email for the authenticated user. Returns 409 if already verified.
      security:
        - DashboardAuth: []
      responses:
        '200':
          description: Verification email sent
        '409':
          description: Email is already verified

  # ==========================================
  # DASHBOARD — ACCOUNT
  # ==========================================

  /dashboard/account:
    get:
      tags: [Dashboard — Account]
      summary: Get account
      description: Returns the tenant profile, plan details, and current quota status.
      security:
        - DashboardAuth: []
      responses:
        '200':
          description: Account details
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      tenant:
                        $ref: '#/components/schemas/TenantProfile'
                      quotas:
                        type: object
                        properties:
                          monthlyRequests:
                            type: integer
                            nullable: true
                          monthlyTokens:
                            type: string
                            nullable: true
                          requestsUsed:
                            type: integer
                          tokensUsed:
                            type: string
                          resetAt:
                            type: string
                            format: date-time
                            nullable: true
                      rateLimits:
                        type: object
                        properties:
                          requestsPerMinute:
                            type: integer
    patch:
      tags: [Dashboard — Account]
      summary: Update account
      description: Update display name and billing email.
      security:
        - DashboardAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                tenantName:
                  type: string
                billingEmail:
                  type: string
                  format: email
      responses:
        '200':
          description: Account updated

  /dashboard/api-key:
    get:
      tags: [Dashboard — Account]
      summary: Get API key info
      description: Returns masked API key metadata. The plaintext key is never returned.
      security:
        - DashboardAuth: []
      responses:
        '200':
          description: API key metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      hasKey:
                        type: boolean
                      maskedKey:
                        type: string
                        nullable: true
                        example: "liya_F9y-Led6...o4k"
                      createdAt:
                        type: string
                        format: date-time
                        nullable: true
                      lastUsedAt:
                        type: string
                        format: date-time
                        nullable: true
                      revokedAt:
                        type: string
                        format: date-time
                        nullable: true

  /dashboard/api-key/rotate:
    post:
      tags: [Dashboard — Account]
      summary: Rotate API key
      description: |
        Invalidates the current API key and generates a new one.
        **Save the new key — it will not be shown again.**
      security:
        - DashboardAuth: []
      responses:
        '200':
          description: New key generated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      apiKey:
                        type: string
                        description: New plaintext key — shown once only
                      maskedKey:
                        type: string
                  message:
                    type: string

  # ==========================================
  # DASHBOARD — USAGE
  # ==========================================

  /dashboard/usage:
    get:
      tags: [Dashboard — Usage]
      summary: Current month usage
      description: Usage totals and per-domain/intent breakdown for the current billing period.
      security:
        - DashboardAuth: []
      responses:
        '200':
          description: Usage data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      period:
                        type: object
                        properties:
                          start:
                            type: string
                            example: "2026-03-01"
                          end:
                            type: string
                            example: "2026-03-31"
                      totals:
                        type: object
                        properties:
                          requests:
                            type: integer
                          tokens:
                            type: string
                          costUsd:
                            type: string
                      byDomain:
                        type: array
                        items:
                          type: object
                          properties:
                            domain:
                              type: string
                            requests:
                              type: integer
                            tokens:
                              type: string
                            costUsd:
                              type: string
                            byIntent:
                              type: array
                              items:
                                type: object

  /dashboard/usage/history:
    get:
      tags: [Dashboard — Usage]
      summary: Usage history
      description: Monthly usage totals for the last N months — used for trend charts.
      security:
        - DashboardAuth: []
      parameters:
        - name: months
          in: query
          schema:
            type: integer
            default: 6
            maximum: 12
          description: Number of months to return
      responses:
        '200':
          description: Monthly history
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      history:
                        type: array
                        items:
                          type: object
                          properties:
                            month:
                              type: string
                              example: "2026-03"
                            requests:
                              type: integer
                            tokens:
                              type: string
                            costUsd:
                              type: string

  # ==========================================
  # DASHBOARD — SESSIONS
  # ==========================================

  /dashboard/sessions:
    get:
      tags: [Dashboard — Sessions]
      summary: List sessions
      description: Recent AI sessions initiated by the tenant's users.
      security:
        - DashboardAuth: []
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
        - name: domain
          in: query
          schema:
            type: string
          description: Filter by domain (e.g. hiring)
      responses:
        '200':
          description: Session list
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      sessions:
                        type: array
                        items:
                          $ref: '#/components/schemas/SessionSummary'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

  /dashboard/sessions/{sessionId}:
    get:
      tags: [Dashboard — Sessions]
      summary: Get session
      description: Full session detail including message history.
      security:
        - DashboardAuth: []
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
          example: sess_abc123
      responses:
        '200':
          description: Session detail with messages
        '403':
          description: Session belongs to another tenant
        '404':
          description: Session not found

  # ==========================================
  # ADMIN — STATS
  # ==========================================

  /admin/stats:
    get:
      tags: [Admin — Stats]
      summary: Platform stats
      description: Platform-wide totals for the admin overview page.
      security:
        - AdminSecret: []
      responses:
        '200':
          description: Platform totals
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      tenants:
                        type: object
                        properties:
                          total:
                            type: integer
                          active:
                            type: integer
                          newThisMonth:
                            type: integer
                      usage:
                        type: object
                        properties:
                          requestsThisMonth:
                            type: integer
                          tokensThisMonth:
                            type: string
                          costThisMonth:
                            type: string
                          requestsAllTime:
                            type: integer
                      topDomains:
                        type: array
                        items:
                          type: object
                          properties:
                            domain:
                              type: string
                            requests:
                              type: integer

  /admin/stats/usage:
    get:
      tags: [Admin — Stats]
      summary: Usage time-series
      description: Time-series usage data for platform charts.
      security:
        - AdminSecret: []
      parameters:
        - name: granularity
          in: query
          schema:
            type: string
            enum: [day, month]
            default: day
        - name: months
          in: query
          schema:
            type: integer
            default: 3
            maximum: 12
      responses:
        '200':
          description: Time-series data
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  data:
                    type: object
                    properties:
                      granularity:
                        type: string
                      series:
                        type: array
                        items:
                          type: object
                          properties:
                            date:
                              type: string
                            requests:
                              type: integer
                            tokens:
                              type: string
                            costUsd:
                              type: string

  /admin/tenants/{tenantId}/usage/history:
    get:
      tags: [Admin — Usage]
      summary: Tenant usage history
      description: Multi-month usage breakdown for a specific tenant.
      security:
        - AdminSecret: []
      parameters:
        - name: tenantId
          in: path
          required: true
          description: The tenant ID
          schema:
            type: string
          example: test-tenant
        - name: months
          in: query
          schema:
            type: integer
            default: 6
            maximum: 12
      responses:
        '200':
          description: Monthly history for tenant

  /admin/tenants/{tenantId}/sessions:
    get:
      tags: [Admin — Usage]
      summary: Tenant sessions
      description: Lists AI sessions for a specific tenant — for debugging and support.
      security:
        - AdminSecret: []
      parameters:
        - name: tenantId
          in: path
          required: true
          description: The tenant ID
          schema:
            type: string
          example: test-tenant
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Session list for tenant

# ============================================
# REUSABLE PARAMETERS
# ============================================

  parameters:
    TenantId:
      name: tenantId
      in: path
      required: true
      schema:
        type: string
      example: acme-corp

# ============================================
# REUSABLE RESPONSES
# ============================================

  responses:
    IntentSuccess:
      description: Intent executed successfully
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/IntentResponse'

    BadRequest:
      description: Invalid input
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            success: false
            error:
              code: INVALID_INPUT
              message: "Missing required field: input.user_id"

    Unauthorized:
      description: Missing or invalid API key
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            success: false
            error:
              code: INVALID_API_KEY
              message: API key not found or invalid

    QuotaExceeded:
      description: Monthly quota exceeded
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            success: false
            error:
              code: QUOTA_EXCEEDED
              message: Monthly quota exceeded

    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'

    Conflict:
      description: A resource with this identifier already exists
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorEnvelope'
          example:
            success: false
            error:
              code: SLUG_CONFLICT
              message: "A collection named 'contracts' already exists."
