openapi: 3.0.3
info:
  title: Aximo API
  version: 1.0.0
  description: |
    The Aximo REST API lets you create and monitor AI test sessions, trigger test case
    and plan runs, and manage your organization's workspaces programmatically — perfect
    for CI/CD pipelines, scripts, or any HTTP client.

    ## Authentication

    All endpoints (except `/api/health`) require an API key passed in the `x-api-key`
    request header. Generate keys from **Settings → API Keys** in the Aximo dashboard.

    ```
    x-api-key: YOUR_API_KEY
    ```

    ## Quick Start: Desktop Test

    Run an ad-hoc desktop browser test in three steps:

    ```bash
    # 1. Get your workspace ID
    curl -H "x-api-key: $KEY" https://aximo.autify.com/api/organizations/projects

    # 2. Create a session
    curl -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
      https://aximo.autify.com/api/sessions \
      -d '{"title":"Login test","prompt":"Go to https://example.com, click Login, verify dashboard loads.","projectId":"YOUR_PROJECT_ID"}'

    # 3. Poll until done
    curl -H "x-api-key: $KEY" https://aximo.autify.com/api/sessions/SESSION_ID
    ```

    Poll until `status` is `succeeded`, `failed`, `terminated`, or `error`.

    ## Quick Start: Mobile Test

    Mobile tests require uploading an app and selecting a device first.

    ```bash
    # 1. List available devices
    curl -H "x-api-key: $KEY" https://aximo.autify.com/api/devices?platform=ANDROID

    # 2. Create an upload record
    curl -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
      https://aximo.autify.com/api/uploads \
      -d '{"fileName":"my-app.apk"}'

    # 3. Upload the binary to the presigned URL
    curl -X PUT -H "Content-Type: application/octet-stream" \
      --data-binary @my-app.apk "PRESIGNED_URL"

    # 4. Submit for processing
    curl -X POST -H "x-api-key: $KEY" \
      https://aximo.autify.com/api/uploads/UPLOAD_ID/submit

    # 5. Poll until the upload is processed
    curl -H "x-api-key: $KEY" https://aximo.autify.com/api/uploads/UPLOAD_ID
    # Wait for status=succeeded, then grab the uploadArn token from the response

    # 6. Create a mobile session (deviceArn and uploadArn are encrypted tokens)
    curl -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
      https://aximo.autify.com/api/sessions \
      -d '{"title":"App login test","prompt":"Open the app, tap Sign In, verify home screen.","projectId":"YOUR_PROJECT_ID","platform":"android","deviceArn":"DEVICE_TOKEN","deviceName":"Google Pixel 7","uploadArn":"UPLOAD_TOKEN","appName":"My App"}'

    # 7. Poll session status
    curl -H "x-api-key: $KEY" https://aximo.autify.com/api/sessions/SESSION_ID
    ```

    Once an app is uploaded, you can reuse its `uploadArn` token for multiple sessions
    without re-uploading. Uploads expire on Device Farm after 30 days but are
    automatically re-uploaded from storage when listed via `GET /api/uploads`.

    > **Note:** The `arn`, `deviceArn`, and `uploadArn` fields returned by the API
    > are encrypted tokens, not raw AWS ARNs. Pass them as-is to other endpoints.

    ## Triggering Existing Test Cases

    If you've created test cases in the Aximo dashboard, trigger them by ID.
    The case's stored prompt, platform, and configuration are used automatically.

    ```bash
    # Run a web test case
    curl -X POST -H "x-api-key: $KEY" \
      https://aximo.autify.com/api/triggers/cases/CASE_ID/run

    # Run a test case with a specific model
    curl -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
      https://aximo.autify.com/api/triggers/cases/CASE_ID/run \
      -d '{"model": "sonnet"}'

    # Run a mobile test case (override device/app)
    curl -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
      https://aximo.autify.com/api/triggers/cases/CASE_ID/run \
      -d '{"model": "sonnet", "deviceArn":"DEVICE_TOKEN","deviceName":"Google Pixel 7","uploadArn":"UPLOAD_TOKEN"}'

    # Poll case session status
    curl -H "x-api-key: $KEY" \
      https://aximo.autify.com/api/triggers/cases/CASE_ID/sessions/SESSION_ID
    ```

    Test cases must have **API access enabled** in their settings.

    ## Triggering Test Plans

    Run all cases in a test plan with a single call.

    ```bash
    # Run a plan
    curl -X POST -H "x-api-key: $KEY" \
      https://aximo.autify.com/api/triggers/plans/PLAN_ID/run

    # Run a plan with a specific model
    curl -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
      https://aximo.autify.com/api/triggers/plans/PLAN_ID/run \
      -d '{"model": "sonnet"}'

    # Run a plan with mobile config (applies to all mobile cases)
    curl -X POST -H "x-api-key: $KEY" -H "Content-Type: application/json" \
      https://aximo.autify.com/api/triggers/plans/PLAN_ID/run \
      -d '{"model": "sonnet", "mobileConfig":{"deviceArn":"DEVICE_TOKEN","deviceName":"Google Pixel 7","uploadArn":"UPLOAD_TOKEN"}}'

    # Poll plan run status
    curl -H "x-api-key: $KEY" \
      https://aximo.autify.com/api/triggers/plans/PLAN_ID/runs/PLAN_RUN_ID
    ```

    The plan run response includes per-case status breakdowns.

    ## Polling & Terminal Statuses

    Sessions and plan runs are asynchronous. Poll the status endpoint until
    a terminal status is reached:

    | Resource | Terminal statuses |
    |----------|-----------------|
    | Sessions | `succeeded`, `failed`, `terminated`, `error`, `archived` |
    | Plan runs | `succeeded`, `failed`, `error` |

    A recommended polling interval is 5 seconds. The `result` field on sessions
    indicates the test outcome: `passed`, `failed`, or `error`.

servers:
  - url: /
    description: Current environment

security:
  - ApiKeyAuth: []

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: x-api-key
      description: Organization API key generated from Settings → API Keys.

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: Unauthorized

    SessionStatus:
      type: string
      enum:
        - queued
        - created
        - preparing
        - running
        - succeeded
        - failed
        - terminated
        - error
        - archived
      description: |
        Terminal statuses (stop polling): `succeeded`, `failed`, `terminated`, `error`, `archived`.

    SessionResult:
      type: string
      nullable: true
      enum:
        - passed
        - failed
        - error

    Platform:
      type: string
      enum: [desktop, android, ios]

    MobileSession:
      type: object
      nullable: true
      properties:
        deviceArn:
          type: string
          description: Encrypted device identifier token.
        deviceName:
          type: string
        devicePlatform:
          type: string
        uploadArn:
          type: string
          nullable: true
          description: Encrypted upload identifier token.
        uploadType:
          type: string
          nullable: true
          enum: [own_app, existing, sample]
        appName:
          type: string
          nullable: true
        bundleId:
          type: string
          nullable: true

    Session:
      type: object
      required: [id, title, prompt, platform, model, status, tags, screenshotsCount, toolCount, failedToolCount, llmCallCount, createdAt]
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        prompt:
          type: string
        platform:
          $ref: '#/components/schemas/Platform'
        model:
          type: string
          enum: [haiku, sonnet, kimi]
        status:
          $ref: '#/components/schemas/SessionStatus'
        result:
          $ref: '#/components/schemas/SessionResult'
        summary:
          type: string
          nullable: true
          description: AI-generated summary of what happened during the session.
        observations:
          type: array
          nullable: true
          items:
            type: object
            properties:
              description:
                type: string
              messageId:
                type: string
        tags:
          type: array
          items:
            type: string
        screenshotsCount:
          type: integer
        toolCount:
          type: integer
        failedToolCount:
          type: integer
        llmCallCount:
          type: integer
        creditsUsed:
          type: number
          nullable: true
          description: Credits consumed by this session.
        startedAt:
          type: string
          format: date-time
          nullable: true
        completedAt:
          type: string
          format: date-time
          nullable: true
        duration:
          type: integer
          nullable: true
          description: Execution duration in milliseconds.
        createdAt:
          type: string
          format: date-time
        mobileSession:
          $ref: '#/components/schemas/MobileSession'

    Project:
      type: object
      required: [id, name, createdAt]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time

    Device:
      type: object
      properties:
        arn:
          type: string
          description: Encrypted device identifier token. Pass this value as `deviceArn` when creating sessions.
        name:
          type: string
        manufacturer:
          type: string
        model:
          type: string
        modelId:
          type: string
        platform:
          type: string
          enum: [ANDROID, IOS]
        os:
          type: string
        formFactor:
          type: string
          enum: [PHONE, TABLET]
        availability:
          type: string
        heapSize:
          type: integer
          nullable: true
        memory:
          type: integer
          nullable: true
        resolution:
          type: object
          nullable: true
          properties:
            width:
              type: integer
            height:
              type: integer

    UploadStatus:
      type: string
      enum: [initialized, processing, succeeded, failed]
      description: |
        - `initialized` — record created, waiting for file upload and submission.
        - `processing` — worker is uploading to Device Farm.
        - `succeeded` — ready to use in sessions.
        - `failed` — processing failed (see `message` for details).

    Upload:
      type: object
      required: [id, fileName, fileType, status, uploadedAt, isExpired]
      properties:
        id:
          type: string
          format: uuid
        uploadArn:
          type: string
          nullable: true
          description: Encrypted upload identifier token. Available once status is `succeeded`. Pass this value as `uploadArn` when creating sessions.
        fileName:
          type: string
        fileSize:
          type: integer
          nullable: true
          description: File size in bytes.
        fileType:
          type: string
          enum: [ANDROID_APP, IOS_APP]
        appName:
          type: string
          nullable: true
          description: Human-readable app name (auto-extracted from APK/IPA).
        appVersion:
          type: string
          nullable: true
        identifier:
          type: string
          nullable: true
          description: Bundle ID (iOS) or package name (Android).
        iconData:
          type: string
          nullable: true
          description: Base64-encoded PNG app icon.
        status:
          $ref: '#/components/schemas/UploadStatus'
        message:
          type: string
          nullable: true
          description: Status details (e.g. error message on failure).
        uploadedAt:
          type: string
          format: date-time
        isExpired:
          type: boolean
          description: Whether the upload has expired on Device Farm (auto-expires after 30 days).

    SessionListItem:
      type: object
      required: [id, title, prompt, platform, status, createdAt]
      properties:
        id:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
          nullable: true
        caseId:
          type: string
          format: uuid
          nullable: true
        title:
          type: string
        prompt:
          type: string
          description: Truncated to 200 characters.
        platform:
          $ref: '#/components/schemas/Platform'
        status:
          $ref: '#/components/schemas/SessionStatus'
        result:
          $ref: '#/components/schemas/SessionResult'
        startedAt:
          type: string
          format: date-time
          nullable: true
        completedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        durationMs:
          type: integer
          nullable: true

    SessionList:
      type: object
      required: [items, totalCount, page, pageSize, totalPages]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/SessionListItem'
        totalCount:
          type: integer
        page:
          type: integer
        pageSize:
          type: integer
        totalPages:
          type: integer

    SessionMessage:
      type: object
      required: [id, role, content, createdAt]
      properties:
        id:
          type: string
          format: uuid
        role:
          type: string
          description: One of `user`, `assistant`, `system`, `tool`.
        content:
          type: string
        metadata:
          type: object
          nullable: true
          description: |
            Tool-call metadata when `role` is `tool` or `assistant`. Common keys:
            `toolName`, `toolInput`, `toolOutput`, `toolCallId`, `toolCalls`.
          additionalProperties: true
        createdAt:
          type: string
          format: date-time

    SessionMessagesPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/SessionMessage'
        nextCursor:
          type: string
          nullable: true
          description: Pass as `cursor` on the next request. `null` when no more pages.

    SessionScreenshot:
      type: object
      required: [id, url, capturedAt]
      properties:
        id:
          type: string
          format: uuid
        url:
          type: string
          description: |
            Resolved screenshot URL. May be a presigned S3 URL (expires in 1 hour),
            a relative `/api/screenshots/...` path, or a `data:` URI.
        messageId:
          type: string
          format: uuid
          nullable: true
        width:
          type: integer
          nullable: true
        height:
          type: integer
          nullable: true
        resizedWidth:
          type: integer
          nullable: true
        resizedHeight:
          type: integer
          nullable: true
        capturedAt:
          type: string
          format: date-time

    SessionScreenshotsPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/SessionScreenshot'
        nextCursor:
          type: string
          nullable: true

    TimelineStep:
      type: object
      required: [index, messageId, role, createdAt, screenshots]
      properties:
        index:
          type: integer
        messageId:
          type: string
        role:
          type: string
        toolName:
          type: string
          nullable: true
        toolInput:
          nullable: true
          description: Tool call arguments (shape depends on tool).
        toolOutput:
          nullable: true
        text:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        screenshots:
          type: array
          items:
            $ref: '#/components/schemas/SessionScreenshot'

    SessionTimeline:
      type: object
      required: [sessionId, stepCount, steps]
      properties:
        sessionId:
          type: string
          format: uuid
        stepCount:
          type: integer
        steps:
          type: array
          items:
            $ref: '#/components/schemas/TimelineStep'

    SessionCapture:
      type: object
      required: [sessionId, platform, snapshotCount, truncated]
      properties:
        sessionId:
          type: string
          format: uuid
        platform:
          type: string
          example: desktop
        startedAt:
          type: string
          format: date-time
          nullable: true
        endedAt:
          type: string
          format: date-time
          nullable: true
        browserContext:
          type: object
          nullable: true
          description: UA, viewport, locale, timezone captured at session start.
          additionalProperties: true
        events:
          type: array
          description: |
            Ordered, timestamped events: action_started/completed,
            page_navigated, page_lifecycle, request, response, console,
            exception, dialog_opened, snapshot_captured, target_attached,
            target_detached.
          items:
            type: object
            required: [type, ts]
            properties:
              type:
                type: string
              ts:
                type: integer
                description: Epoch millis (Date.now()) when the event was recorded inside the VM.
            additionalProperties: true
        storageState:
          type: object
          nullable: true
          description: Cookies and localStorage captured at session end.
          additionalProperties: true
        snapshotCount:
          type: integer
          description: Number of DOM+AX snapshots captured (one per `screenshot` tool call and per page-load).
        snapshotsPrefix:
          type: string
          nullable: true
          description: S3 prefix for snapshot blobs. Null while snapshots are persisted inline.
        snapshots:
          type: array
          description: |
            Inline DOM + accessibility-tree snapshots, in capture order. Each
            entry pairs a pixel-level event (a `screenshot` tool call or a
            `Page.lifecycleEvent: load`) with the structural state of the page
            at that moment. Large — expect single-MB responses on long
            sessions.
          items:
            type: object
            required: [id, ts, trigger, targetId, url]
            properties:
              id: { type: string, format: uuid }
              ts: { type: integer, description: Epoch ms when the snapshot was taken }
              messageId:
                type: string
                format: uuid
                nullable: true
                description: AgentMessage that triggered the snapshot, when trigger was `screenshot`.
              trigger: { type: string, enum: [screenshot, page_load] }
              targetId: { type: string, description: CDP target (tab/iframe) id }
              url: { type: string }
              title: { type: string }
              dom:
                description: DOMSnapshot.captureSnapshot payload (computed styles excluded for size).
              axTree:
                description: Accessibility.getFullAXTree payload — what `getByRole`/`getByLabel` index into.
        truncated:
          type: boolean
          description: True if the event buffer reached its cap and dropped tail events.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CaseListItem:
      type: object
      required: [id, projectId, type, createdAt, updatedAt]
      properties:
        id:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        type:
          type: string
          enum: [web, mobile]
        title:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
          description: Truncated to 200 characters.
        startUrl:
          type: string
          nullable: true
        lastRunAt:
          type: string
          format: date-time
          nullable: true
        lastStatus:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CaseList:
      type: object
      required: [items, totalCount, page, pageSize, totalPages]
      properties:
        items:
          type: array
          items:
            $ref: '#/components/schemas/CaseListItem'
        totalCount:
          type: integer
        page:
          type: integer
        pageSize:
          type: integer
        totalPages:
          type: integer

    CaseDetail:
      type: object
      required: [id, projectId, type, scenario, format, createdAt, updatedAt]
      properties:
        id:
          type: string
          format: uuid
        projectId:
          type: string
          format: uuid
        type:
          type: string
          enum: [web, mobile]
        title:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        scenario:
          type: string
          description: Test case content (interpretation depends on `format`).
        format:
          type: string
          enum: [gherkin, markdown, plain_text]
        startUrl:
          type: string
          nullable: true
        apiEnabled:
          type: boolean
        lastRunAt:
          type: string
          format: date-time
          nullable: true
        lastStatus:
          type: string
          nullable: true
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    PlanRunCase:
      type: object
      properties:
        id:
          type: string
          format: uuid
        caseId:
          type: string
          format: uuid
        title:
          type: string
          nullable: true
        type:
          type: string
          nullable: true
        status:
          type: string
        startedAt:
          type: string
          format: date-time
          nullable: true
        completedAt:
          type: string
          format: date-time
          nullable: true

    PlanRun:
      type: object
      required: [id, status, totalCases, completedCases, passedCases, failedCases, createdAt, cases]
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum: [pending, running, succeeded, failed, error]
        triggerSource:
          type: string
          nullable: true
        totalCases:
          type: integer
        completedCases:
          type: integer
        passedCases:
          type: integer
        failedCases:
          type: integer
        startedAt:
          type: string
          format: date-time
          nullable: true
        completedAt:
          type: string
          format: date-time
          nullable: true
        createdAt:
          type: string
          format: date-time
        cases:
          type: array
          items:
            $ref: '#/components/schemas/PlanRunCase'

  responses:
    Unauthorized:
      description: Missing or invalid API key.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Unauthorized
    RateLimited:
      description: Daily session limit reached for the organization.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Rate limit exceeded. Try again later.
    InternalError:
      description: Unexpected server error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Internal server error

tags:
  - name: Health
    description: Check application availability.
  - name: Sessions
    description: Create and monitor AI test sessions.
  - name: Triggers – Cases
    description: Trigger individual test case runs and poll their status.
  - name: Triggers – Plans
    description: Trigger test plan runs and poll their status.
  - name: Devices
    description: List available mobile devices from AWS Device Farm.
  - name: Uploads
    description: Upload and manage mobile app binaries (APK/IPA) for testing.
  - name: Workspaces
    description: List workspaces available to your organization.
  - name: Test Cases
    description: List and inspect saved test cases.

paths:
  /api/health:
    get:
      operationId: getHealth
      summary: Health check
      description: Returns `200 OK` when the application is up. No authentication required.
      security: []
      tags: [Health]
      responses:
        '200':
          description: Application is healthy.

  /api/devices:
    get:
      operationId: listDevices
      summary: List available devices
      description: |
        Returns mobile devices available for testing from AWS Device Farm.
        Filter by platform or availability. Use a device's `arn` when creating
        mobile sessions via `POST /api/sessions`.
      tags: [Devices]
      parameters:
        - name: platform
          in: query
          schema:
            type: string
            enum: [ANDROID, IOS]
          description: Filter devices by platform.
        - name: availableOnly
          in: query
          schema:
            type: boolean
            default: false
          description: Only return devices currently available.
      responses:
        '200':
          description: List of devices.
          content:
            application/json:
              schema:
                type: object
                required: [devices]
                properties:
                  devices:
                    type: array
                    items:
                      $ref: '#/components/schemas/Device'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/uploads:
    post:
      operationId: createUpload
      summary: Create an app upload
      description: |
        Creates an upload record and returns a presigned URL for uploading the app binary.

        **Flow:**
        1. Call this endpoint to get `uploadId` and `presignedUrl`.
        2. `PUT` the binary file to `presignedUrl`.
        3. Call `POST /api/uploads/{uploadId}/submit` to start processing.
        4. Poll `GET /api/uploads/{uploadId}` until `status` is `succeeded`.
        5. Use the `uploadArn` token in `POST /api/sessions` to start a mobile test.
      tags: [Uploads]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fileName]
              properties:
                fileName:
                  type: string
                  minLength: 1
                  description: Original file name (must end in `.apk`, `.ipa`, or `.zip`).
                  example: my-app.apk
                fileSize:
                  type: integer
                  description: File size in bytes (informational).
                platform:
                  type: string
                  enum: [ANDROID, IOS]
                  description: Platform hint for `.zip` files.
            example:
              fileName: my-app.apk
              fileSize: 52428800
      responses:
        '201':
          description: Upload record created.
          content:
            application/json:
              schema:
                type: object
                required: [uploadId, presignedUrl, fileType]
                properties:
                  uploadId:
                    type: string
                    format: uuid
                  presignedUrl:
                    type: string
                    description: URL to PUT the binary file to.
                  fileType:
                    type: string
                    enum: [ANDROID_APP, IOS_APP]
              example:
                uploadId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                presignedUrl: "https://s3.amazonaws.com/..."
                fileType: ANDROID_APP
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

    get:
      operationId: listUploads
      summary: List uploaded apps
      description: |
        Returns previously uploaded apps that are ready to use or currently processing.
        Automatically validates uploads against Device Farm and re-uploads expired entries.
      tags: [Uploads]
      parameters:
        - name: fileType
          in: query
          schema:
            type: string
            enum: [ANDROID_APP, IOS_APP]
          description: Filter by app type.
      responses:
        '200':
          description: List of uploads.
          content:
            application/json:
              schema:
                type: object
                required: [uploads]
                properties:
                  uploads:
                    type: array
                    items:
                      $ref: '#/components/schemas/Upload'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/uploads/{uploadId}:
    get:
      operationId: getUploadStatus
      summary: Get upload status
      description: |
        Returns the current processing status of an upload. Poll this endpoint
        after calling `POST /api/uploads/{uploadId}/submit` until `status` is
        `succeeded` or `failed`.
      tags: [Uploads]
      parameters:
        - name: uploadId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID returned by `POST /api/uploads`.
      responses:
        '200':
          description: Upload details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Upload'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Upload not found or belongs to another organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Upload not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

    put:
      operationId: uploadFile
      summary: Upload app binary
      description: |
        Upload the app binary (APK/IPA) for a previously created upload record.
        Use this endpoint when the `presignedUrl` from `POST /api/uploads` points to
        your Aximo instance (local development). In production, upload directly to the
        S3 presigned URL instead.
      tags: [Uploads]
      parameters:
        - name: uploadId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        '200':
          description: File uploaded successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
        '400':
          description: No body provided.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Upload not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/uploads/{uploadId}/submit:
    post:
      operationId: submitUpload
      summary: Submit upload for processing
      description: |
        Signals that the file upload is complete and triggers background processing.
        The worker will parse the app metadata, upload it to AWS Device Farm, and
        update the status.

        Poll `GET /api/uploads/{uploadId}` until `status` is `succeeded` or `failed`.
      tags: [Uploads]
      parameters:
        - name: uploadId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '202':
          description: Upload submitted for processing.
          content:
            application/json:
              schema:
                type: object
                required: [success, workflowId]
                properties:
                  success:
                    type: boolean
                    example: true
                  workflowId:
                    type: string
                    description: Background workflow ID for tracking.
        '400':
          description: Upload not in the correct state (must be `initialized` with a file uploaded).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Upload not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/organizations/projects:
    get:
      operationId: listProjects
      summary: List workspaces
      description: Returns all active (non-archived) workspaces for your organization, sorted by creation date.
      tags: [Workspaces]
      responses:
        '200':
          description: List of workspaces.
          content:
            application/json:
              schema:
                type: object
                required: [projects]
                properties:
                  projects:
                    type: array
                    items:
                      $ref: '#/components/schemas/Project'
              example:
                projects:
                  - id: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
                    name: "My Web App"
                    description: "End-to-end tests for the marketing site"
                    createdAt: "2024-01-15T10:00:00Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sessions:
    get:
      operationId: listSessions
      summary: List sessions
      description: |
        Returns sessions visible to your organization, newest first.
        Filter by project, test case, or status. Results are paginated.
      tags: [Sessions]
      parameters:
        - name: projectId
          in: query
          schema:
            type: string
            format: uuid
          description: Filter by workspace.
        - name: caseId
          in: query
          schema:
            type: string
            format: uuid
          description: Filter to sessions linked to a specific test case.
        - name: status
          in: query
          schema:
            $ref: '#/components/schemas/SessionStatus'
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: pageSize
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
      responses:
        '200':
          description: Paginated session list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionList'
        '400':
          description: Invalid query parameters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

    post:
      operationId: createSession
      summary: Create a session
      description: |
        Triggers a new AI test session and returns its ID. The session is queued immediately
        and will begin executing as soon as a worker is available.

        Poll `GET /api/sessions/{sessionId}` until `status` reaches a terminal value:
        `succeeded`, `failed`, `terminated`, `error`, or `archived`.
      tags: [Sessions]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, prompt, projectId]
              properties:
                title:
                  type: string
                  minLength: 1
                  maxLength: 500
                  description: Session title.
                  example: Login flow smoke test
                prompt:
                  type: string
                  minLength: 1
                  maxLength: 10000
                  description: Test instructions for the AI agent.
                  example: "Go to https://example.com, click Login, enter test@example.com / password123, and verify the dashboard loads."
                projectId:
                  type: string
                  format: uuid
                  description: ID of the workspace to run the session under.
                platform:
                  $ref: '#/components/schemas/Platform'
                  default: desktop
                model:
                  type: string
                  enum: [haiku, sonnet, kimi]
                  default: sonnet
                  description: Model to use for this session. Claude models (haiku, sonnet, opus) or Kimi K2.5 via DeepInfra (kimi).
                startUrl:
                  type: string
                  format: uri
                  description: Starting URL for desktop sessions.
                caseId:
                  type: string
                  format: uuid
                  description: Link this session to an existing test case.
                tags:
                  type: array
                  maxItems: 10
                  items:
                    type: string
                    maxLength: 64
                  default: []
                  description: Labels for filtering sessions.
                deviceArn:
                  type: string
                  description: Encrypted device token from `GET /api/devices`. Required for mobile sessions.
                deviceName:
                  type: string
                  description: Human-readable device name (mobile only).
                uploadArn:
                  type: string
                  description: Encrypted upload token from `GET /api/uploads/{id}`. Required for mobile sessions with own app.
                uploadType:
                  type: string
                  enum: [own_app, existing, sample]
                  description: App source type (mobile only).
                appName:
                  type: string
                  description: Display name of the app used in test instructions (mobile only).
                bundleId:
                  type: string
                  description: App bundle identifier (mobile only).
            example:
              title: Login flow smoke test
              prompt: "Go to https://example.com, click Login, enter test@example.com / password123, and verify the dashboard loads."
              projectId: "3fa85f64-5717-4562-b3fc-2c963f66afa6"
      responses:
        '201':
          description: Session created and queued.
          content:
            application/json:
              schema:
                type: object
                required: [success, sessionId]
                properties:
                  success:
                    type: boolean
                    example: true
                  sessionId:
                    type: string
                    format: uuid
              example:
                success: true
                sessionId: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Invalid request body
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Forbidden (e.g. billing limit reached or feature not available on current plan).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sessions/{sessionId}:
    get:
      operationId: getSession
      summary: Get session status
      description: |
        Returns the current state and full results of a session. Poll this endpoint
        until `status` is a terminal value.

        **Terminal statuses** (stop polling): `succeeded`, `failed`, `terminated`, `error`, `archived`.
      tags: [Sessions]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: Session UUID returned by `POST /api/sessions`.
      responses:
        '200':
          description: Session details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Session'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Session not found or belongs to another organization.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Session not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sessions/{sessionId}/messages:
    get:
      operationId: getSessionMessages
      summary: Get session messages
      description: |
        Returns the message timeline (tool calls, assistant text, user input) for a
        session. Cursor-paginated; pass `nextCursor` to fetch subsequent pages.
      tags: [Sessions]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: cursor
          in: query
          schema:
            type: string
          description: Cursor returned by a prior call (`nextCursor`).
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
      responses:
        '200':
          description: Paginated messages.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionMessagesPage'
        '400':
          description: Invalid query parameters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Session not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sessions/{sessionId}/screenshots:
    get:
      operationId: getSessionScreenshots
      summary: Get session screenshots
      description: |
        Returns screenshots captured during a session, ordered by capture time.
        URLs are presigned (expire after 1 hour for S3) or relative paths.
      tags: [Sessions]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: cursor
          in: query
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
      responses:
        '200':
          description: Paginated screenshots.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionScreenshotsPage'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Session not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sessions/{sessionId}/timeline:
    get:
      operationId: getSessionTimeline
      summary: Get session timeline
      description: |
        Returns the merged step timeline: each step combines a message (tool call,
        assistant turn, or user input) with any screenshots captured for that step.

        This is the recommended endpoint for downstream consumers (e.g. generating
        a Playwright spec from the session) — it gives ordered, paired data without
        having to join messages and screenshots manually.
      tags: [Sessions]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Session timeline.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionTimeline'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Session not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sessions/{sessionId}/capture:
    get:
      operationId: getSessionCapture
      summary: Get session capture metadata
      description: |
        Returns CDP-derived capture metadata recorded during a session: ordered
        events (actions, navigations, network XHR/fetch, console, exceptions,
        snapshot triggers), the browser context, and the post-session storage
        state. Only available when the organization has the CDP capture feature
        flag enabled and the session ran on the desktop VM.

        `404` is returned when no capture exists for the session.
      tags: [Sessions]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
        - name: includeSnapshots
          in: query
          required: false
          description: |
            Controls how `snapshots[]` is returned.
            - `none`: empty array.
            - `metadata` (default): each entry has `id`, `ts`, `messageId`,
              `trigger`, `targetId`, `url`, `title`, `domBytes`, `axBytes` —
              fetch full content per-snapshot via
              `GET /api/sessions/{id}/snapshots/{snapshotId}`.
            - `full`: includes the full `dom` and `axTree` inline (can be
              multi-MB; avoid for routine MCP/agent calls).
          schema:
            type: string
            enum: [none, metadata, full]
            default: metadata
      responses:
        '200':
          description: Session capture blob.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SessionCapture'
        '400':
          description: Invalid query parameter (e.g. unknown `includeSnapshots` value).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Session or capture not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/sessions/{sessionId}/snapshots/{snapshotId}:
    get:
      operationId: getSessionSnapshot
      summary: Get one DOM + AX snapshot
      description: |
        Returns a single snapshot by id (one entry from `snapshots[]` on the
        capture). Use this when the capture blob would be too large and you
        only need one page's structure.
      tags: [Sessions]
      parameters:
        - name: sessionId
          in: path
          required: true
          schema: { type: string, format: uuid }
        - name: snapshotId
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Single snapshot.
          content:
            application/json:
              schema:
                type: object
                required: [id, ts, trigger, targetId, url]
                properties:
                  id: { type: string, format: uuid }
                  ts: { type: integer }
                  messageId: { type: string, format: uuid, nullable: true }
                  trigger: { type: string, enum: [screenshot, page_load] }
                  targetId: { type: string }
                  url: { type: string }
                  title: { type: string }
                  dom: { description: DOMSnapshot.captureSnapshot payload }
                  axTree: { description: Accessibility.getFullAXTree payload }
        '400': { description: Invalid sessionId or snapshotId. }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404':
          description: Session, capture or snapshot not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429': { $ref: '#/components/responses/RateLimited' }
        '500': { $ref: '#/components/responses/InternalError' }

  /api/cases:
    get:
      operationId: listCases
      summary: List test cases
      description: List saved test cases for a workspace.
      tags: [Test Cases]
      parameters:
        - name: projectId
          in: query
          required: true
          schema:
            type: string
            format: uuid
        - name: type
          in: query
          schema:
            type: string
            enum: [web, mobile]
        - name: search
          in: query
          schema:
            type: string
          description: Free-text match on title, description, or scenario.
        - name: directoryId
          in: query
          schema:
            type: string
          description: |
            Filter cases by directory. Pass a UUID to filter to that directory, or an
            empty string (`?directoryId=`) to filter to cases not in any directory.
            Omit the parameter entirely to include all cases.
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: pageSize
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
      responses:
        '200':
          description: Paginated case list.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CaseList'
        '400':
          description: Invalid query parameters.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Project not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/cases/{caseId}:
    get:
      operationId: getCase
      summary: Get a test case
      description: Returns the full scenario text and metadata for a test case.
      tags: [Test Cases]
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Test case.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CaseDetail'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Test case not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/triggers/cases/{caseId}/run:
    post:
      operationId: triggerCaseRun
      summary: Trigger a case run
      description: |
        Starts a new test session for the specified test case. The case must have
        **API access enabled** in its settings.

        Returns a `sessionId` — poll `GET /api/triggers/cases/{caseId}/sessions/{sessionId}`
        to track progress.
      tags: [Triggers – Cases]
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the test case to run.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                model:
                  type: string
                  enum: [default, haiku, sonnet, kimi]
                  description: Override the model for this run. Defaults to the case's configured model.
                hostnameOverrideIds:
                  type: array
                  items:
                    type: string
                    format: uuid
                  description: IDs of hostname overrides to apply for this run.
                deviceArn:
                  type: string
                  description: Encrypted device token from `GET /api/devices` (mobile only).
                deviceName:
                  type: string
                  description: Override the device name (mobile only).
                platform:
                  type: string
                  enum: [ANDROID, IOS]
                  description: Override the mobile platform.
                uploadArn:
                  type: string
                  description: Encrypted upload token from `GET /api/uploads/{id}` (mobile only).
                uploadType:
                  type: string
                  enum: [own_app, existing, sample]
                  description: Override the upload type (mobile only).
                appName:
                  type: string
                  description: Override the app name (mobile only).
      responses:
        '201':
          description: Case run triggered.
          content:
            application/json:
              schema:
                type: object
                required: [success, sessionId]
                properties:
                  success:
                    type: boolean
                    example: true
                  sessionId:
                    type: string
                    format: uuid
              example:
                success: true
                sessionId: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API access is not enabled for this case.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: API access is not enabled for this case
        '404':
          description: Test case not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Test case not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/triggers/cases/{caseId}/sessions/{sessionId}:
    get:
      operationId: getCaseSessionStatus
      summary: Get case session status
      description: Returns the status and result of a session that was triggered for a specific test case.
      tags: [Triggers – Cases]
      parameters:
        - name: caseId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the test case.
        - name: sessionId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the session returned by the trigger endpoint.
      responses:
        '200':
          description: Session status.
          content:
            application/json:
              schema:
                type: object
                required: [id, status, platform, screenshotsCount]
                properties:
                  id:
                    type: string
                    format: uuid
                  status:
                    $ref: '#/components/schemas/SessionStatus'
                  result:
                    $ref: '#/components/schemas/SessionResult'
                  platform:
                    $ref: '#/components/schemas/Platform'
                  screenshotsCount:
                    type: integer
                  startedAt:
                    type: string
                    format: date-time
                    nullable: true
                  completedAt:
                    type: string
                    format: date-time
                    nullable: true
              example:
                id: "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
                status: succeeded
                result: passed
                platform: desktop
                screenshotsCount: 12
                startedAt: "2024-03-01T10:00:00Z"
                completedAt: "2024-03-01T10:02:30Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Test case or session not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/triggers/plans/{planId}/run:
    post:
      operationId: triggerPlanRun
      summary: Trigger a plan run
      description: |
        Starts a new run for the specified test plan. The plan must have **API access enabled**
        in its settings.

        Returns a `planRunId` — poll `GET /api/triggers/plans/{planId}/runs/{planRunId}`
        to track overall progress.
      tags: [Triggers – Plans]
      parameters:
        - name: planId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the test plan to run.
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                model:
                  type: string
                  enum: [default, haiku, sonnet, kimi]
                  default: sonnet
                  description: Model to use for all cases in this plan run. Claude models (haiku, sonnet, opus) or Kimi K2.5 via DeepInfra (kimi).
                mobileConfig:
                  type: object
                  description: Override mobile device configuration for this run.
                  required: [deviceArn, deviceName]
                  properties:
                    deviceArn:
                      type: string
                      description: Encrypted device token from `GET /api/devices`.
                    deviceName:
                      type: string
                    platform:
                      type: string
                      enum: [ANDROID, IOS]
                    uploadArn:
                      type: string
                      description: Encrypted upload token from `GET /api/uploads/{id}`.
                    uploadType:
                      type: string
                      enum: [own_app, existing, sample]
                    appName:
                      type: string
      responses:
        '201':
          description: Plan run triggered.
          content:
            application/json:
              schema:
                type: object
                required: [success, planRunId, totalCases]
                properties:
                  success:
                    type: boolean
                    example: true
                  planRunId:
                    type: string
                    format: uuid
                  totalCases:
                    type: integer
                    description: Number of test cases in this plan run.
              example:
                success: true
                planRunId: "1d4b6f8a-2c3e-4a5b-8f9d-0e1f2a3b4c5d"
                totalCases: 5
        '400':
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: API access is not enabled for this plan.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: API access is not enabled for this plan
        '404':
          description: Test plan not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Test plan not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'

  /api/triggers/plans/{planId}/runs/{planRunId}:
    get:
      operationId: getPlanRunStatus
      summary: Get plan run status
      description: Returns the overall status and per-case breakdown of a plan run.
      tags: [Triggers – Plans]
      parameters:
        - name: planId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the test plan.
        - name: planRunId
          in: path
          required: true
          schema:
            type: string
            format: uuid
          description: UUID of the plan run returned by the trigger endpoint.
      responses:
        '200':
          description: Plan run details.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PlanRun'
              example:
                id: "1d4b6f8a-2c3e-4a5b-8f9d-0e1f2a3b4c5d"
                status: succeeded
                triggerSource: api
                totalCases: 5
                completedCases: 5
                passedCases: 4
                failedCases: 1
                startedAt: "2024-03-01T10:00:00Z"
                completedAt: "2024-03-01T10:15:00Z"
                createdAt: "2024-03-01T09:59:55Z"
                cases:
                  - id: "aaa00000-0000-0000-0000-000000000001"
                    caseId: "bbb00000-0000-0000-0000-000000000001"
                    title: "Login flow"
                    type: "ui"
                    status: succeeded
                    startedAt: "2024-03-01T10:00:10Z"
                    completedAt: "2024-03-01T10:02:40Z"
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          description: Plan run not found.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: Plan run not found
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
