openapi: 3.1.0

info:
  title: LexSelect External API
  description: |
    External API for the LexSelect platform. All endpoints are mounted under `/api`.

    **[Examples & Quick Start (GitHub)](https://github.com/LexSelect/lexselect-api-demo)** — Go CLI, Python notebook, and TypeScript examples.

    ## Authentication

    All documented endpoints require a Bearer API key in the `Authorization` header:

    ```
    Authorization: Bearer lxs_your_api_key
    ```

    API keys require `ProcessingAPI` scope to access document and processing endpoints.

    ## Client Environment

    This is a server-to-server API **by design**. API keys must not be embedded
    in browser applications, and cross-origin browser requests are not supported.
    The API does not emit CORS preflight response headers, and a preflight
    `OPTIONS` request is answered with `401` — this is intentional, not a bug.
    Browser-based integrations are out of scope for this release.

    ## Versioning

    Pass the `X-API-Version` header to pin to a specific API version. Omitting the header
    defaults to the latest version (`2026-06-07`). Unsupported versions return `400`.

    The latest version (`2026-06-07`) reports processing progress as **facts**:
    per-scope `progress_details` plus `pages_done` / `pages_total` / `total_known` /
    `stage` on `GET /documents/{id}/processing/latest` — and drops the legacy
    `processing_progress` fraction (everywhere) and the legacy progress
    `page_count` (on `/processing/latest`). The `/parse` response's `page_count` /
    `total_pages` are response-batch metadata, present on all versions, and are
    unaffected. Integrators that still need the legacy shape pin
    `X-API-Version: 2026-03-06`, the only version that carries it.

    ## Rate Limiting

    Authenticated requests are subject to per-key, per-user aggregate, and optional
    per-key concurrency limits. Authenticated responses include the active
    `X-RateLimit-*` budget headers. When a per-minute rate limit is exceeded,
    a `429` is returned with a `Retry-After` header indicating seconds to wait.
    When an optional concurrency cap is exceeded, a `429` is returned with
    detail text such as `Too many concurrent requests. Maximum: 10.`

    | Header | Description |
    |--------|-------------|
    | `X-RateLimit-Limit` | Requests allowed per minute for this key tier |
    | `X-RateLimit-Remaining` | Requests remaining in the current window |
    | `X-RateLimit-Reset` | Unix timestamp (seconds) when the window resets |
    | `Retry-After` | Seconds to wait before retrying. Present on per-minute rate-limit `429` responses; omitted for concurrency-limit `429` responses. |

    ## Idempotency

    Mutating requests (POST, PUT, PATCH, DELETE) may include an `Idempotency-Key` header.
    Duplicate requests with the same key return the cached response for 24 hours.

    ## Errors

    All error responses use [RFC 9457 Problem Details](https://www.rfc-editor.org/rfc/rfc9457)
    with `Content-Type: application/problem+json`.

    ```json
    {
      "type": "about:blank",
      "title": "Unauthorized",
      "status": 401,
      "detail": "Missing or invalid Authorization header."
    }
    ```

    Every response includes an `X-Request-Id` header (UUID) for tracing.

    ## Upload Flow

    Upload a file with a single request to `POST /documents/upload`. Send a
    `multipart/form-data` body containing the file `name`, its `size` in bytes,
    and the `file` part. In one HTTP round-trip the server stores the bytes,
    verifies their integrity, and queues the document for processing; the
    response is the created document object.

    **Requirements:**
    - Maximum file size: 250 MB.
    - Allowed extensions: `doc`, `docx`, `eml`, `html`, `msg`, `odt`, `pdf`, `rtf`.
    - The request `Content-Type` must be `multipart/form-data`.
    - If `parent_id` is omitted, the file is placed in the user's Default Project.

    See `POST /documents/upload` below for the full field reference.

    ## Webhooks

    Configure webhook endpoints via the UI to receive real-time notifications
    when document processing state changes. Payloads are signed with HMAC-SHA256.

    Subscribable event types:

    | Event | Description |
    |-------|-------------|
    | `document.processing.queued` | Document added to processing queue |
    | `document.processing.started` | Processing has begun |
    | `document.processing.page_completed` | A page finished processing |
    | `document.processing.completed` | Processing finished successfully |
    | `document.processing.failed` | Processing failed |
    | `*` | All events (includes future event types) |
  version: "2026-06-07"
  contact:
    name: LexSelect API Support

servers:
  - url: /api
    description: API server (relative path, environment-agnostic)

security:
  - ApiKeyAuth: []

tags:
  - name: Files
    description: File storage — upload, list, retrieve, and delete files and folders.
  - name: Processing
    description: Processing pipeline — track status and progress of document processing.
  - name: Results
    description: Processing results — page metadata, extracted data, and evaluations.

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API-Key
      description: >
        Bearer API key issued by LexSelect. Keys are prefixed `lxs_`.
        Pass as `Authorization: Bearer lxs_your_key`.
        Requires `ProcessingAPI` scope for document and processing endpoints.

  schemas:
    ProblemDetails:
      type: object
      description: RFC 9457 Problem Details error response.
      required:
        - type
        - title
        - status
        - detail
      properties:
        type:
          type: string
          description: URI reference identifying the problem type.
          example: about:blank
        title:
          type: string
          description: Short, human-readable summary of the problem type.
          example: Unauthorized
        status:
          type: integer
          description: HTTP status code.
          example: 401
        detail:
          type: string
          description: Human-readable explanation specific to this occurrence.
          example: Missing or invalid Authorization header.

    Document:
      type: object
      properties:
        id:
          type: string
          description: Document ObjectId.
          example: 665f1a2b3c4d5e6f7a8b9c0d
        name:
          type: string
          example: report.pdf
        type:
          type: string
          enum: [file, folder, project]
          example: file
        parent_id:
          type:
            - string
            - "null"
          example: 665f1a2b3c4d5e6f7a8b9c0d
        file_extension:
          type:
            - string
            - "null"
          example: pdf
        size:
          type:
            - integer
            - "null"
          example: 204800
        created_at:
          type: string
          format: date-time
        modified_at:
          type: string
          format: date-time
        deleted:
          type: boolean
          example: false
        processing_status:
          type:
            - string
            - "null"
          description: Current processing status. Present on files in list responses when enrichment is available.
          example: SUCCESS
        processing_progress:
          type:
            - number
            - "null"
          description: >
            Processing progress (0.0 to 1.0). **Pinned version `2026-03-06`
            only** — omitted on `2026-06-07` and later, where progress is
            served by `GET /documents/{id}/processing/latest` (the document
            object is metadata, not a progress surface). Present on files
            when enrichment is available.
          example: 1.0
        is_preview_ready:
          type:
            - boolean
            - "null"
          description: Whether a preview is available. Present on files in list responses when enrichment is available.
        document_type:
          type:
            - string
            - "null"
          description: Detected document type (e.g., "Transcript"). Present on files in list responses when enrichment is available.
          example: Transcript

    DocumentListResponse:
      type: object
      required:
        - items
        - total
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Document"
        next_cursor:
          type:
            - string
            - "null"
          description: Cursor for the next page. Null if no more results.
        total:
          type: integer
          description: Number of items in this page.
          example: 25

    ProgressDetail:
      type: object
      description: >
        One per-scope progress fact (latest API version). `done` of `total`
        units complete for this scope. While `total_known` is `false`, the
        total may still grow as fan-out is discovered — render counts, never
        derive a percentage from an unresolved total.
      required:
        - scope
        - kind
        - done
        - total
        - total_known
      properties:
        scope:
          type: string
          description: Scope identifier (currently `page`).
          example: page
        kind:
          type: string
          description: Unit of work counted (currently `page`).
          example: page
        done:
          type: integer
          description: Completed units in this scope.
          example: 7
        total:
          type: integer
          description: Discovered units in this scope so far.
          example: 12
        total_known:
          type: boolean
          description: Whether `total` is final.
          example: true

    ProcessingLatestResponse:
      type: object
      description: >
        The response shape is selected by `X-API-Version`.
        **Latest (`2026-06-07`, default):** progress facts —
        `progress_details[]` + `pages_done` / `pages_total` / `total_known` /
        `stage`; the legacy `processing_progress` and `page_count` are NOT
        returned. **Pinned `2026-03-06`:** the legacy pair
        `processing_progress` + `page_count`; none of the facts fields.
      properties:
        document_id:
          type: string
          example: 665f1a2b3c4d5e6f7a8b9c0d
        document_name:
          type: string
          example: report.pdf
        processing_id:
          type: string
          example: 665f1a2b3c4d5e6f7a8b9c0e
        status:
          type: string
          enum: [in_progress, completed, failed]
          example: completed
        progress_details:
          type: array
          description: >
            Per-scope progress facts. **Version `2026-06-07` and later only.**
          items:
            $ref: "#/components/schemas/ProgressDetail"
        pages_done:
          type: integer
          description: >
            Successfully processed pages. **Version `2026-06-07` and later only.**
          example: 7
        pages_total:
          type: integer
          description: >
            Pages discovered so far (in progress + done). **Version
            `2026-06-07` and later only.**
          example: 12
        total_known:
          type: boolean
          description: >
            Whether `pages_total` is final. While `false`, show counts —
            never a percentage. **Version `2026-06-07` and later only.**
          example: true
        stage:
          type: string
          description: >
            Coarse work stage label (e.g. `preparing`, `page-processing`,
            `done`). Label only — never a substitute for `status`.
            **Version `2026-06-07` and later only.**
          example: done
        processing_progress:
          type: number
          format: float
          description: >
            Legacy progress fraction (0.0 to 1.0). **Pinned version
            `2026-03-06` only.**
          example: 1.0
        engine_version:
          type: integer
          example: 3
        page_count:
          type: integer
          description: >
            Legacy count of all page rows. **Pinned version `2026-03-06`
            only.** Unrelated to the `/parse` response's `page_count`
            (response-batch metadata).
          example: 12
        preview_available:
          type: boolean
          example: true
        created_at:
          type:
            - string
            - "null"
          format: date-time
        started_at:
          type:
            - string
            - "null"
          format: date-time
        finished_at:
          type:
            - string
            - "null"
          format: date-time
        error_message:
          type: string
          description: Present only for failed processings.

    PageMetadata:
      type: object
      properties:
        page_index:
          type: integer
          example: 0
        width:
          type: number
          format: float
          example: 612.0
        height:
          type: number
          format: float
          example: 792.0
        page_type:
          type:
            - string
            - "null"
          example: text

    PageListResponse:
      type: object
      required:
        - pages
        - total
      properties:
        pages:
          type: array
          items:
            $ref: "#/components/schemas/PageMetadata"
        next_cursor:
          type:
            - string
            - "null"
          description: Cursor for the next page. Null if no more results.
        total:
          type: integer
          description: Total number of pages for this processing.
          example: 12

    DocumentDeleteResponse:
      type: object
      required:
        - id
        - deleted
      properties:
        id:
          type: string
          example: 665f1a2b3c4d5e6f7a8b9c0d
        deleted:
          type: boolean
          example: true

    ParseResponse:
      type: object
      required:
        - document_id
        - processing_id
        - status
        - document_type
        - sub_documents
        - total_pages
        - page_count
        - pages
      properties:
        document_id:
          type: string
          example: 665f1a2b3c4d5e6f7a8b9c0d
        processing_id:
          type:
            - string
            - "null"
          example: 665f1a2b3c4d5e6f7a8b9c0e
        status:
          type: string
          enum: [completed, in_progress, failed, unknown]
          description: >
            Processing status. For `in_progress` responses, only pages that
            have already reached `SUCCESS` are included in `pages`.
        document_type:
          type:
            - string
            - "null"
          description: >
            High-level document classification, one of 18 legal-domain
            categories produced by the engine's document type detection
            stage. `null` when the classifier has not run for this
            document (older processings, or pipelines that skipped the
            detection stage). Values are stable strings; treat unknown
            values as a forward-compatible extension rather than an error.
          enum:
            - Transcript
            - Expert Report
            - Affidavit
            - Orders
            - Email
            - Case Initiating Filing
            - Case Management Statement
            - Contracts/Agreements
            - Demand for Arbitration
            - Proof of Service
            - Service List
            - Motion
            - Discovery
            - Declaration
            - Brief/Memorandum
            - Stipulation
            - Deposition Notice
            - Other
            - null
          example: Affidavit
        sub_documents:
          type: array
          description: >
            Detected sub-document outline. Each entry represents a
            contiguous page range within the parent document that the
            engine identified as a semantically distinct unit (an
            exhibit, an attached affidavit, a motion within a brief,
            etc.). Nested sub-documents are supported via `children`.
            Empty list when no sub-documents were detected.
          items:
            $ref: "#/components/schemas/SubDocument"
        total_pages:
          type: integer
          description: >
            Total number of successfully processed pages in the
            document, independent of any `pages=` filter on the
            request. Partners can use this to detect when their
            requested range is smaller or larger than the doc.
          example: 6
        page_count:
          type: integer
          description: >
            Number of pages **returned in this response** after applying
            the `pages=` filter (if any), clamping against the document's
            real size, and the in-progress success filter. Always less
            than or equal to `total_pages`. Response-batch metadata on all
            API versions — NOT the legacy `page_count` progress field that
            the pinned-version `/processing/latest` response carries.
          example: 5
        pages:
          type: array
          items:
            $ref: "#/components/schemas/ParsedPage"

    SubDocument:
      type: object
      description: >
        A detected sub-document — a contiguous slice of the parent
        document that the engine classified as a standalone unit.
      required:
        - id
        - label
        - page_from
        - page_to
        - children
      properties:
        id:
          type: string
          description: Stable identifier for this sub-document.
          example: "2d07ae8a-cd0b-43ba-ae68-7cb21cecc2b0"
        label:
          type: string
          description: >
            Human-readable category label. Matches the
            `starter_category` field for single-start sub-documents;
            derived from the dominant category for aggregates.
          example: "Affidavit First Page"
        page_from:
          type: integer
          description: >
            Zero-based index of the first page in this sub-document
            (inclusive). Matches `ParsedPage.page_index` in the same
            response.
          example: 0
        page_to:
          type: integer
          description: >
            Zero-based index of the last page in this sub-document
            (inclusive).
          example: 2
        children:
          type: array
          description: Nested sub-documents contained within this one.
          items:
            $ref: "#/components/schemas/SubDocument"
        starter_category:
          type: string
          description: >
            The page category that triggered the sub-document boundary
            (e.g. `Exhibit Cover`, `Affidavit First Page`). Internal
            hint — prefer `label` for partner display.
          example: "Affidavit First Page"

    ParsedPage:
      type: object
      description: >
        A single page of parsed output. Which fields are present depends on
        the `include` query param. Fields for slices that were not requested
        are omitted entirely. `width` and `height` are omitted when
        `strip_geometry=true`.
      required:
        - page_index
      properties:
        page_index:
          type: integer
          description: Zero-based page index within the processing.
          example: 0
        width:
          type: number
          format: float
          description: Page width in points. Omitted when `strip_geometry=true`.
          example: 612.0
        height:
          type: number
          format: float
          description: Page height in points. Omitted when `strip_geometry=true`.
          example: 792.0
        tree:
          oneOf:
            - $ref: "#/components/schemas/ParsedTree"
            - type: "null"
          description: >
            Hierarchical layout tree. Present only when `include` contains
            `tree` (default). `null` if the tree-builder task has not run
            for this page. Respects `exclude_node_types` and
            `strip_geometry`.
        kvps:
          oneOf:
            - type: object
              additionalProperties:
                type: string
              example:
                Plaintiff Name: "Jane Doe"
                Claim Number: "CLM-12345"
            - type: "null"
          description: >
            Flat `{key: string, value: string}` map of key-value pairs.
            Present only when `include` contains `kvps` (default). `null`
            when the extraction task has not run for this document — which
            is the common case unless a partner-specific workflow is
            enabled.
        text:
          type: string
          description: >
            Plain text in reading order, one line per LINE node joined with
            `\n`. Present only when `include` contains `text`. Respects
            `exclude_node_types`.
          example: "Hello world\nSecond line of text"
        tables:
          type: array
          description: >
            All tables on the page, each projected to `{headers, rows}`.
            Present only when `include` contains `tables`.
          items:
            $ref: "#/components/schemas/ParsedTable"
        blocks:
          type: array
          description: >
            Flat pre-order list of content blocks for the page. Present
            only when `include` contains `blocks`.
          items:
            $ref: "#/components/schemas/ParsedBlock"

    ParsedTable:
      type: object
      description: A table extracted from the tree.
      required:
        - headers
        - rows
      properties:
        id:
          type: string
          description: Stable node id of the TABLE subtree this came from.
          example: "tbl_0"
        headers:
          type: array
          description: Column headers, in order. Empty if the table has no header row.
          items:
            type: string
          example: ["Name", "Age"]
        rows:
          type: array
          description: Row values, each an ordered array of cell text.
          items:
            type: array
            items:
              type: string
          example:
            - ["Alice", "30"]
            - ["Bob", "42"]

    ParsedBlock:
      type: object
      description: >
        A flat content block — one entry in the pre-order traversal of the
        tree for block-level classes (HEADER, FOOTER, SECTION, LINE, TABLE,
        SUB_DOCUMENT).
      required:
        - id
        - class
        - text
      properties:
        id:
          type: string
          description: Stable node id from the tree.
          example: "section_0"
        class:
          type: string
          description: Lowercase `NodeClass` name.
          enum:
            - header
            - footer
            - section
            - line
            - table
            - sub_document
          example: "section"
        text:
          type: string
          description: Concatenated text content of the block.
          example: "Hello world"

    ParsedTree:
      type: object
      required:
        - root
      properties:
        root:
          type: array
          items:
            $ref: "#/components/schemas/ParsedTreeNode"

    ParsedTreeNode:
      type: object
      description: >
        A node in the hierarchical layout tree. Each node carries a
        `class_id` from the `NodeClass` enum, a 4-point polygon `coordinates`
        array, and a recursive `content` array of child nodes. `value` and
        `engine_meta` carry class-specific data such as extracted text,
        ordering hints, and table continuation metadata.
      properties:
        id:
          type: string
          example: line_0
        class_id:
          type: integer
          description: >
            Node class identifier. Partners should treat `class_id` as a stable
            contract and map it to the `NodeClass` vocabulary below. New classes
            may be added; existing ids will not be renumbered.
          enum:
            - 0 # ROOT
            - 1 # MAIN_PAGE
            - 2 # SUBPAGE
            - 3 # HEADER
            - 4 # FOOTER
            - 5 # CONTENT
            - 6 # COLUMN
            - 7 # SECTION
            - 8 # TRANSCRIPT_LINE
            - 9 # QUESTION
            - 10 # ANSWER
            - 11 # QUESTION_MARK_TOKEN
            - 12 # ANSWER_MARK_TOKEN
            - 13 # CONVERSATION
            - 14 # VISION_OBJECT
            - 15 # TOKEN
            - 16 # LINE
            - 17 # SUB_DOCUMENT
            - 18 # HORIZONTAL_BAND
            - 19 # TABLE
            - 20 # TABLE_ROW
            - 21 # TABLE_COLUMN_HEADER
            - 22 # TABLE_CELL
        coordinates:
          type: array
          description: 4-point polygon `[[x,y], [x,y], [x,y], [x,y]]`.
          items:
            type: array
            items:
              type: number
        content:
          type: array
          description: Child nodes (recursive).
          items:
            $ref: "#/components/schemas/ParsedTreeNode"
        value:
          type: object
          description: >
            Class-specific value bag. May contain `text`, `paragraph_number`,
            `ocr_line_number`, `transcript_line_number`, `question_number`,
            `answer_number`, etc. Treat as a free-form object.
          additionalProperties: true
        engine_meta:
          type: object
          description: >
            Engine scoring and layout metadata. May contain ordering hints,
            table continuation (`continues_from_previous`, `continuation_table_id`,
            `is_table_start`), and cross-page range markers (`page_from`, `page_to`).
          additionalProperties: true
        tokens:
          type: array
          items: {}
        order_id:
          type: integer

  headers:
    X-Request-Id:
      description: Unique identifier for this request, assigned by the gateway. Present on all responses.
      schema:
        type: string
        format: uuid
        example: 550e8400-e29b-41d4-a716-446655440000

    X-RateLimit-Limit:
      description: Maximum number of requests allowed per minute for this API key tier.
      schema:
        type: integer
        example: 60

    X-RateLimit-Remaining:
      description: Number of requests remaining in the current rate-limit window.
      schema:
        type: integer
        example: 59

    X-RateLimit-Reset:
      description: Unix timestamp (seconds) at which the current rate-limit window resets.
      schema:
        type: integer
        example: 1741802460

    Retry-After:
      description: Number of seconds to wait before retrying. Present on per-minute rate-limit 429 responses.
      schema:
        type: integer
        example: 30

  parameters:
    XApiVersion:
      name: X-API-Version
      in: header
      required: false
      description: >
        API version to use. Defaults to the latest version (`2026-06-07`) if omitted.
        Unsupported versions return `400 Bad Request`. Pin `2026-03-06` to keep the
        legacy progress shape (`processing_progress` + `page_count`).
      schema:
        type: string
        example: "2026-06-07"

    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >
        Client-generated key for idempotent requests (POST, PUT, PATCH, DELETE).
        Duplicate requests with the same key return the original cached response for 24 hours.
      schema:
        type: string
        example: order-abc-attempt-1

  responses:
    Unauthorized:
      description: Missing or invalid API key.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          example:
            type: about:blank
            title: Unauthorized
            status: 401
            detail: Missing or invalid Authorization header.

    Forbidden:
      description: API key is valid but does not have permission for this operation.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          example:
            type: about:blank
            title: Forbidden
            status: 403
            detail: Insufficient permissions for this operation.

    TooManyRequests:
      description: A per-key, per-user, IP, or concurrency limit was exceeded.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
        Retry-After:
          $ref: "#/components/headers/Retry-After"
        X-RateLimit-Limit:
          $ref: "#/components/headers/X-RateLimit-Limit"
        X-RateLimit-Remaining:
          $ref: "#/components/headers/X-RateLimit-Remaining"
        X-RateLimit-Reset:
          $ref: "#/components/headers/X-RateLimit-Reset"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          example:
            type: about:blank
            title: Too Many Requests
            status: 429
            detail: Rate limit exceeded. Retry after 30 seconds.

    InternalServerError:
      description: An unexpected server error occurred.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          example:
            type: about:blank
            title: Internal Server Error
            status: 500
            detail: An unexpected error occurred.

    GatewayTimeout:
      description: The orchestrator did not respond within the timeout window.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          example:
            type: about:blank
            title: Gateway Timeout
            status: 504
            detail: The server did not receive a timely response.

    PayloadTooLarge:
      description: >
        The request body exceeds the maximum allowed size. For multipart file
        uploads this also covers a file part larger than the 250 MB limit.
      headers:
        X-Request-Id:
          $ref: "#/components/headers/X-Request-Id"
      content:
        application/problem+json:
          schema:
            $ref: "#/components/schemas/ProblemDetails"
          example:
            type: about:blank
            title: Payload Too Large
            status: 413
            detail: Request body exceeds the maximum allowed size.

paths:
  /documents:
    get:
      operationId: listDocuments
      summary: List files
      description: >
        Lists documents and folders within a parent. Without `parent_id`,
        returns root-level items (projects). Use the project ID as `parent_id`
        to list files within it.
      tags:
        - Files
      parameters:
        - $ref: "#/components/parameters/XApiVersion"
        - name: parent_id
          in: query
          required: false
          description: Parent folder or project ObjectId. Omit for root-level listing (projects).
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: >-
            Maximum items per page — an integer from 1 to 100 (default 25).
            Out-of-range or non-integer values are rejected with `400`
            (not clamped).
          schema:
            type: integer
            default: 25
            minimum: 1
            maximum: 100
        - name: cursor
          in: query
          required: false
          description: Cursor from a previous response's `next_cursor` field.
          schema:
            type: string
        - name: sort_by
          in: query
          required: false
          description: Field to sort by.
          schema:
            type: string
            enum: [name, size, created_at, modified_at]
            default: name
        - name: sort_direction
          in: query
          required: false
          schema:
            type: string
            enum: [asc, desc]
            default: asc
        - name: type
          in: query
          required: false
          description: Filter by item type.
          schema:
            type: string
            enum: [file, folder, project]
        - name: include_deleted
          in: query
          required: false
          description: Include soft-deleted items.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: List of documents.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentListResponse"
        "400":
          description: Invalid query parameters.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Parent folder not found.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /documents/upload:
    post:
      operationId: uploadDocument
      summary: Upload file
      description: >
        Upload a file in a single `multipart/form-data` request. The server
        handles storage upload and the processing trigger internally, and
        returns the created document object.


        Maximum file size: 250 MB. Allowed extensions: doc, docx, eml, html,
        msg, odt, pdf, rtf.


        If `parent_id` is omitted the file is placed in the user's Default
        Project.
      tags:
        - Files
      parameters:
        - $ref: "#/components/parameters/XApiVersion"
        - $ref: "#/components/parameters/IdempotencyKey"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required:
                - name
                - size
                - file
              properties:
                name:
                  type: string
                  description: File name including extension.
                  example: report.pdf
                size:
                  type: integer
                  example: 0
                  description: >
                    File size in bytes. Must equal the file's exact byte count,
                    or the upload is rejected. Maximum 250 MB.
                file:
                  type: string
                  format: binary
                  description: The file to upload (max 250 MB).
                content_type:
                  type: string
                  description: MIME type override. Inferred from file name if omitted.
                parent_id:
                  type: string
                  description: Parent folder or project ObjectId. Defaults to the user's Default Project.
      responses:
        "200":
          description: File uploaded and processing queued.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Document"
        "400":
          description: >
            Validation error (missing name/file, unsupported extension,
            invalid parent), a non-`multipart/form-data` Content-Type, or a
            malformed `multipart/form-data` body (missing or invalid boundary).
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "502":
          description: Storage upload failed.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /documents/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Document ObjectId.
        schema:
          type: string

    get:
      operationId: getDocument
      summary: Get file
      description: Retrieve a single document by ID.
      tags:
        - Files
      parameters:
        - $ref: "#/components/parameters/XApiVersion"
      responses:
        "200":
          description: Document details.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Document"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Document not found or not owned by caller.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

    delete:
      operationId: deleteDocument
      summary: Delete file
      description: >
        Soft-deletes a document or folder. Recursive for folders.
        Idempotent — deleting an already-deleted item returns success.
      tags:
        - Files
      parameters:
        - $ref: "#/components/parameters/XApiVersion"
        - $ref: "#/components/parameters/IdempotencyKey"
      responses:
        "200":
          description: Document deleted.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentDeleteResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Document not found or not owned by caller.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /documents/{id}/parse:
    parameters:
      - name: id
        in: path
        required: true
        description: Document ObjectId.
        schema:
          type: string

    get:
      operationId: getDocumentParse
      summary: Get parsed document structure
      description: >
        Returns one or more projections of the document's latest processing
        output for every page. The default response includes the full
        hierarchical `tree` and the flat `kvps` map per page. Use the
        `include` query parameter to request alternative projections (plain
        `text`, extracted `tables`, or a flat list of `blocks`) instead of —
        or in addition to — the raw tree.


        **Slices:**


        - `tree` — hierarchical layout tree produced by the engine's
          tree-builder stage. See `ParsedTreeNode.class_id` for the node
          vocabulary.
        - `kvps` — flat `{key: string, value: string}` map of extracted
          key-value pairs. Only populated when a partner-specific extraction
          workflow has run; otherwise `null` on every page.
        - `text` — plain text in reading order, one line per LINE node,
          joined with `\n`.
        - `tables` — every TABLE subtree projected to
          `{headers: [...], rows: [[...]]}`.
        - `blocks` — flat pre-order list of content blocks (HEADER, FOOTER,
          SECTION, LINE, TABLE, SUB_DOCUMENT), each with `{id, class, text}`.


        **Filtering:**

        - `pages` limits which pages are returned (1-indexed).
        - `exclude_node_types` strips subtrees whose class matches before
          any projection runs — so it applies consistently across `tree`,
          `text`, `tables`, and `blocks`.
        - `strip_geometry=true` removes `coordinates`, `engine_meta`, and
          `tokens` from every node in the `tree` slice (no effect on other
          slices).


        For in-progress processings, only pages with `status=SUCCESS` are
        included.
      tags:
        - Results
      parameters:
        - $ref: "#/components/parameters/XApiVersion"
        - name: pages
          in: query
          required: false
          description: >
            Page subset, 1-indexed, inclusive ranges. Examples: `1-10`,
            `1,3,5`, `1,3-5,10`. Empty or omitted returns all pages.


            Out-of-range values are silently clamped to the document's
            actual page count. A request like `pages=1-1000` against a
            6-page document returns all 6 pages, not an empty list. Use
            the response's `total_pages` field to see the document's
            real size.


            Ranges that would expand to more than **100,000 distinct
            pages** are rejected with `400`. Cumulative lists are
            checked against the same cap.


            Invalid syntax (non-numeric, zero, negative, reversed
            range) → `400`.
          schema:
            type: string
            example: "1-10"
        - name: include
          in: query
          required: false
          description: >
            Comma-separated list of slices to include per page. Valid
            values: `tree`, `kvps`, `text`, `tables`, `blocks`. Unknown
            values → 400. Defaults to `tree,kvps`.
          schema:
            type: string
            example: "text,tables"
        - name: exclude_node_types
          in: query
          required: false
          description: >
            Comma-separated list of `NodeClass` names (lowercase) whose
            subtrees should be stripped before projection. Common use:
            `header,footer` to drop boilerplate. Unknown names → 400.
            See `ParsedTreeNode.class_id` for the full vocabulary.
          schema:
            type: string
            example: "header,footer"
        - name: strip_geometry
          in: query
          required: false
          description: >
            When `true`, removes all spatial metadata from the response:
            per-node `coordinates`, `engine_meta`, and `tokens` inside the
            `tree` slice, plus per-page `width` and `height`. Roughly 50-60%
            smaller response for tree-heavy payloads. Has no effect on the
            content of `text`, `tables`, `blocks`, or `kvps` slices. Default
            `false`.
          schema:
            type: boolean
            default: false
      responses:
        "200":
          description: Parsed document structure.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ParseResponse"
        "400":
          description: Invalid document id format.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Document not found, not owned by caller, or has no processing records.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /documents/{id}/render:
    parameters:
      - name: id
        in: path
        required: true
        description: Document ObjectId.
        schema:
          type: string

    get:
      operationId: getDocumentRender
      summary: Render document to Markdown or HTML (experimental)
      description: >
        **EXPERIMENTAL.** Renders the document's parsed tree to a single
        Markdown or HTML string and returns it as the raw response body
        (not JSON). Use this to quickly preview what the parser produced,
        paste into a Markdown viewer, or open in a browser.


        Rendering rules (first pass, best effort):


        - `LINE` → one line of text (Markdown) or `<p>` (HTML).
        - `SECTION` → blank-line separated block (Markdown) or `<section>`
          (HTML).
        - `TABLE` → CommonMark table or `<table>/<thead>/<tbody>`.
        - `HEADER`/`FOOTER` → emitted by default. Use
          `exclude_node_types=header,footer` to strip them.


        Multi-page responses join pages with `\n\n---\n\n` (Markdown) or
        `<hr class="lexselect-page-break"/>` (HTML). Response size is not
        bounded — use `pages=` to cap large documents.


        **Stability:** output format and rules may change without notice.
        Not recommended for programmatic consumption — use `/parse` for
        stable structured output.


        **Security note — Markdown output:** CommonMark-style Markdown
        explicitly allows inline HTML, so hostile text content in the
        source document (e.g. `<script>...</script>` in a scanned PDF)
        passes through the Markdown response as-is. Consumers that render
        our Markdown into HTML MUST use a sanitising renderer (for
        example `marked` with default sanitisation, or `rehype-sanitize`
        in the unified pipeline). The **HTML output** is already escaped
        by this service — only raw text content is ever emitted and every
        `<`, `>`, `"`, `'`, `&` is entity-encoded before it reaches the
        response body.
      tags:
        - Results
      parameters:
        - $ref: "#/components/parameters/XApiVersion"
        - name: format
          in: query
          required: true
          description: Output format.
          schema:
            type: string
            enum:
              - markdown
              - html
            example: "markdown"
        - name: pages
          in: query
          required: false
          description: >
            Same syntax as `/parse`: 1-indexed inclusive ranges, e.g.
            `1-10`, `1,3,5`. Empty returns all pages.
          schema:
            type: string
            example: "1-5"
        - name: exclude_node_types
          in: query
          required: false
          description: >
            Comma-separated list of `NodeClass` names (lowercase) whose
            subtrees should be stripped before rendering. Common use:
            `header,footer` to drop boilerplate.
          schema:
            type: string
            example: "header,footer"
      responses:
        "200":
          description: >
            Rendered document. Content-Type is `text/markdown;
            charset=utf-8` or `text/html; charset=utf-8` depending on
            format.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            text/markdown:
              schema:
                type: string
                example: |
                  # Section heading

                  Body text line one.
                  Body text line two.

                  | Name | Age |
                  | --- | --- |
                  | Alice | 30 |
            text/html:
              schema:
                type: string
                example: |
                  <section><p>Hello world</p></section>
                  <table><thead><tr><th>Name</th></tr></thead></table>
        "400":
          description: Missing or unknown format, or invalid filter param.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Document not found, not owned by caller, or has no processing records.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /documents/{id}/processing/latest:
    parameters:
      - name: id
        in: path
        required: true
        description: Document ObjectId.
        schema:
          type: string

    get:
      operationId: getProcessingLatest
      summary: Get processing status
      description: >
        Returns the most recent processing record for the document,
        including status, progress, and preview availability. The progress
        shape is versioned: the latest version (`2026-06-07`) returns
        per-scope facts (`progress_details[]`, `pages_done`, `pages_total`,
        `total_known`, `stage`); the pinned `2026-03-06` version returns the
        legacy `processing_progress` fraction + `page_count`. See
        `ProcessingLatestResponse`.
      tags:
        - Processing
      parameters:
        - $ref: "#/components/parameters/XApiVersion"
      responses:
        "200":
          description: Latest processing status.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProcessingLatestResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Document not found, not owned by caller, or has no processing records.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"

  /documents/{id}/processing/latest/pages:
    parameters:
      - name: id
        in: path
        required: true
        description: Document ObjectId.
        schema:
          type: string

    get:
      operationId: getProcessingPages
      summary: List pages
      description: >
        Returns page metadata (index, dimensions, type) for the latest
        processing. No extracted content is included.
        For in-progress processings, only completed pages are returned.
      tags:
        - Results
      parameters:
        - $ref: "#/components/parameters/XApiVersion"
        - name: limit
          in: query
          required: false
          description: Maximum pages per response (1-100, default 50).
          schema:
            type: integer
            default: 50
            minimum: 1
            maximum: 100
        - name: cursor
          in: query
          required: false
          description: Cursor from a previous response's `next_cursor` field.
          schema:
            type: string
      responses:
        "200":
          description: Page metadata list.
          headers:
            X-Request-Id:
              $ref: "#/components/headers/X-Request-Id"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PageListResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: Document not found, not owned by caller, or has no processing records.
          content:
            application/problem+json:
              schema:
                $ref: "#/components/schemas/ProblemDetails"
        "429":
          $ref: "#/components/responses/TooManyRequests"
        "500":
          $ref: "#/components/responses/InternalServerError"
        "504":
          $ref: "#/components/responses/GatewayTimeout"
