openapi: 3.0.3
info:
  title: Clment REST API
  version: "1.0.0"
  termsOfService: https://clment.com/terms-of-service
  contact:
    name: Clment developer support
    url: https://clment.com/developers
  description: |
    The Clment REST API gives your services programmatic access to the
    same contract workspace you use in the app — contracts, AI reviews,
    rulebooks, key dates, and search.

    ## Authentication

    Every request carries a bearer token in the `Authorization` header:

    ```
    Authorization: Bearer clm_sk_live_xxxxxxxxxxxxxxxxxxxxxxxx
    ```

    Generate a key in the app under **Settings → API & Integrations →
    API keys**. The secret is shown **once** at creation — store it in a
    secret manager. A key is scoped to the organization it was created
    in and inherits that workspace's data residency region.

    ## What a key can and can't do

    An API key can read and write everything a member can in the app:
    upload contracts, run reviews, edit rulebooks, manage key dates,
    search. It **cannot** touch settings — billing, members, SSO,
    integrations, organization config, or webhook subscriptions. Those
    require a human admin signed in to the app.

    ## Regions & base URL

    Use your organization's home region's base URL. A key only works
    against the region it belongs to — calling another region returns
    401. (This is the same list as the **Servers** dropdown above; both
    are generated from our live region catalog.)

    <!--REGIONS_TABLE_START-->
    | Region         | Base URL                       |
    |----------------|--------------------------------|
    | United States  | `https://api-us.clment.com/v1` |
    | European Union | `https://api-eu.clment.com/v1` |
    | Australia      | `https://api-au.clment.com/v1` |
    | New Zealand    | `https://api-nz.clment.com/v1` |
    | United Kingdom | `https://api-uk.clment.com/v1` |
    | Canada         | `https://api-ca.clment.com/v1` |
    <!--REGIONS_TABLE_END-->

    ## Response envelope

    Successful responses wrap their payload in a `data` object:

    ```json
    { "data": { "contract": { "id": "…" } } }
    ```

    Errors return an `error` object with a stable machine-readable
    `code` and a human `message`:

    ```json
    { "error": { "code": "NOT_FOUND", "message": "Contract not found" } }
    ```

    ## Pagination

    List endpoints page with `skip` and `take` (max 100). The response
    carries the page plus a `total` count so you can compute whether
    more pages remain.

    ## Long-running operations (jobs)

    Uploads, reviews, and redlines run as background jobs. The kick-off
    endpoint returns a **job id** immediately; poll the matching job
    endpoint until it reports `complete`, then fetch the result.

    | Action | Start | Poll | Result |
    |--------|-------|------|--------|
    | Upload  | `POST /contracts/upload` | `GET /contracts/upload/jobs/{jobId}` | `GET /contracts/{id}` |
    | Review  | `POST /contracts/{id}/review` | `GET /jobs/review/{id}` | `GET /reviews/{reviewId}` |
    | Redline | `POST /contracts/{id}/generate-redline` | `GET /jobs/redline/{id}` | `GET /contracts/download/{token}` |

    An API key always gets the job id back as JSON — the live
    Server-Sent Events variant of the review and redline start
    endpoints belongs to first-party app sessions. Polling is the
    right shape for a server-to-server integration anyway: simpler
    than holding a stream open, and it survives the process that
    started the job going away.

    ## Rate limits & credits

    Two independent limits apply:

    - **Credits.** AI operations (reviews, redlines, conversions) consume
      credits from your plan's monthly allowance, then any credit packs.
      Read operations are free. A `402 INSUFFICIENT_CREDITS` indicates an
      exhausted balance.
    - **Request rate.** Requests are rate-limited per organization on a
      rolling hourly window sized to your plan. Every AUTHENTICATED
      response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`
      (the limiter is per-organization, so it runs only once the key has
      resolved to one — a 401 carries neither); exceed the window
      and you get a `429 RATE_LIMITED` with a `Retry-After` header (in
      seconds) — back off for that long, then retry.

    ## Plan & access

    The API is available on the **Business** and **Enterprise** plans. A
    call from an org without API access returns `403 API_ACCESS_NOT_IN_PLAN`;
    a call from an org whose subscription has lapsed returns
    `403 SUBSCRIPTION_SUSPENDED`. Both clear once the plan or billing
    status is resolved in the app (with a short propagation delay). An API
    key also can't reach settings surfaces — billing, members, SSO,
    integrations, org config, webhooks — which return `403 API_KEY_FORBIDDEN`.

    ## Versioning

    The API is versioned in the path; the current version is `v1` (every
    base URL ends `/v1`).

    - **Additive changes ship in place:** new endpoints, new optional
      request fields, and new response fields arrive within `v1` without
      notice. Write clients to ignore unknown response fields so these
      never break you.
    - **Breaking changes ship under a new path version** (`/v2`) — removing
      or renaming a field, changing a type, or tightening validation. `v1`
      keeps working through a published deprecation window.

    The `version` of this document tracks the spec's own revisions.

servers:
  - url: https://api-us.clment.com/v1
    description: United States
  - url: https://api-eu.clment.com/v1
    description: European Union
  - url: https://api-au.clment.com/v1
    description: Australia
  - url: https://api-nz.clment.com/v1
    description: New Zealand
  - url: https://api-uk.clment.com/v1
    description: United Kingdom
  - url: https://api-ca.clment.com/v1
    description: Canada

security:
  - bearerAuth: []

tags:
  - name: Contracts
    description: Upload, read, update, and delete contracts.
  - name: Reviews
    description: AI review artifacts — findings, verdicts, risk.
  - name: Rulebooks
    description: The rulebooks AI reviews evaluate contracts against (sometimes known as playbooks).
  - name: Key dates
    description: Renewal, expiry, and custom dates extracted or added.
  - name: Search
    description: Natural-language and filtered search across contracts.
  - name: Collaborators
    description: The people in your organization you can tag or assign work to, and your own inbox.
  - name: Insights
    description: Portfolio-level numbers behind the Insights dashboard.
  - name: Usage
    description: Credits, limits, and where the billing period stands.
  - name: Taxonomy
    description: The contract-type classification tree.
  - name: Jobs
    description: Poll the status of long-running upload / review / redline jobs.
  - name: Conversion
    description: Standalone document conversion utilities (not tied to a contract). PDF-to-Word conversion is high-fidelity — layout, tables, and styles are preserved.

paths:
  /contracts:
    get:
      tags: [Contracts]
      operationId: listContracts
      summary: List contracts
      description: |
        Returns a page of contracts for the authenticated organization,
        newest first. Combine `status`, `q`, and the `createdFrom` /
        `createdTo` window to narrow results.
      parameters:
        - name: skip
          in: query
          schema: { type: integer, default: 0, minimum: 0 }
          description: Rows to skip (offset).
        - name: take
          in: query
          schema: { type: integer, default: 20, minimum: 1, maximum: 100 }
          description: Page size (capped at 100).
        - name: status
          in: query
          schema:
            type: string
            enum: [draft, active, expiring, expired, terminated, archived]
          description: Filter by lifecycle status.
        - name: q
          in: query
          schema: { type: string }
          description: Free-text match against title, parties, and file name.
        - name: tags
          in: query
          style: form
          explode: true
          schema:
            type: array
            items: { type: string }
          description: |
            Filter to contracts carrying ALL of these tags (AND
            semantics) — the same multi-tag filter as the in-app
            Contracts screen. Repeat the param (`?tags=msa&tags=nda`)
            or comma-separate (`?tags=msa,nda`).
        - name: riskLevel
          in: query
          schema:
            $ref: "#/components/schemas/RiskLevel"
          description: |
            Filter by the AI-assessed risk level. The full set a contract
            can carry — a narrower enum here left `critical` and
            `unknown` unreachable from a typed client.
        - name: createdFrom
          in: query
          schema: { type: string, format: date-time }
          description: Only contracts created on or after this ISO timestamp.
        - name: createdTo
          in: query
          schema: { type: string, format: date-time }
          description: Only contracts created on or before this ISO timestamp.
      responses:
        "200":
          description: A page of contracts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      contracts:
                        type: array
                        items: { $ref: "#/components/schemas/Contract" }
                      total:
                        type: integer
                        description: Total matching rows across all pages.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/upload:
    post:
      tags: [Contracts]
      operationId: uploadContract
      summary: Upload a contract (file)
      description: |
        Upload a document's raw bytes as `multipart/form-data`. The file
        is stored, text-extracted, classified, and indexed
        asynchronously — the response returns immediately (`202`) with an
        upload job id. Poll `GET /contracts/upload/jobs/{jobId}` until it
        reports `complete`, then `GET /contracts/{id}` for the finished
        row.

        **Size limit: 100 MB per file.** PDF and DOCX are supported.

        ### Embedded-document matching

        A DOCX downloaded or redlined from Clment carries an embedded
        Clment document id. On upload we detect it and report any match
        in the `202` response (`matchedContractId` / `matchedContractTitle`).
        Control what happens on a match with `onEmbeddedMatch`:

        - `new` (default) — always create a new contract; the match is
          still reported so you can react.
        - `attach` — add the file as a **new version** of the matched
          contract instead of creating a new one. Responds `200` with the
          version (not a job — revisions are processed inline). Returns
          `422 NO_EMBEDDED_MATCH` if nothing matched.
        - `auto` — attach if matched, otherwise create a new contract.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: The contract document (PDF or DOCX, ≤ 100 MB).
                title:
                  type: string
                  description: Optional title override. Defaults to the inferred title.
                onEmbeddedMatch:
                  type: string
                  enum: [new, attach, auto]
                  default: new
                  description: |
                    What to do when the file's embedded Clment document id
                    matches an existing contract. See the description above.
                summariseChanges:
                  type: boolean
                  default: false
                  description: |
                    Only when attaching as a version — generate an AI
                    summary of what changed versus the prior version
                    (consumes credits).
      responses:
        "200":
          description: |
            File attached as a new version of the matched contract
            (`onEmbeddedMatch=attach` or `auto` with a match).
            `attached` is true and `contractId` names the contract.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UploadResult" }
        "202":
          description: |
            New-contract upload accepted; processing asynchronously.
            `jobId` is present — poll `GET /contracts/upload/jobs/{jobId}`.
            `matchedContractId` is non-null when the file matched an
            existing contract (and you were on the `new` path).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/UploadResult" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "413":
          description: File exceeds the 100 MB limit.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "422":
          description: '`onEmbeddedMatch=attach` but no embedded id matched a contract.'
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /contracts/from-url:
    post:
      tags: [Contracts]
      operationId: uploadContractFromUrl
      summary: Upload a contract (from URL)
      description: |
        Create a contract by fetching a document from a publicly
        reachable URL — handy when the source already lives somewhere
        addressable. Same async lifecycle as the file upload: returns a
        job id; poll `GET /contracts/upload/jobs/{jobId}`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [url]
              properties:
                url:
                  type: string
                  format: uri
                  description: Publicly reachable URL of the document (PDF or DOCX).
                title:
                  type: string
                  description: Optional title override. Defaults to the inferred title.
      responses:
        "202":
          description: Upload accepted; processing asynchronously.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    required: [jobId]
                    properties:
                      jobId: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "402": { $ref: "#/components/responses/PaymentRequired" }

  /contracts/tags:
    get:
      tags: [Contracts]
      operationId: listTags
      summary: Every tag in use, with counts
      description: |
        The tag vocabulary of this organization, ordered by how many
        contracts carry each one. What the in-app tag filter and the
        autocomplete read.
      responses:
        "200":
          description: Tags in use.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      tags:
                        type: array
                        items:
                          type: object
                          properties:
                            tag: { type: string }
                            count: { type: integer }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/tags/merge:
    post:
      tags: [Contracts]
      operationId: mergeTags
      summary: Merge tags into one
      description: |
        Rewrites every contract carrying any tag in `from` to carry
        `into` instead. The tidy-up a sync job needs when two systems
        spell the same tag differently.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [from, into]
              properties:
                from:
                  type: array
                  items: { type: string }
                  description: Tags to retire.
                into:
                  type: string
                  description: The tag they all become.
      responses:
        "200":
          description: What the merge changed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      updated:
                        type: integer
                        description: Contracts rewritten.
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/tags/delete:
    post:
      tags: [Contracts]
      operationId: deleteTag
      summary: Remove a tag from every contract
      description: |
        Strips one tag across the organization. The contracts themselves
        are untouched.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [tag]
              properties:
                tag: { type: string }
      responses:
        "200":
          description: What the removal changed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      updated: { type: integer }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/bulk-tag:
    post:
      tags: [Contracts]
      operationId: bulkTagContracts
      summary: Add and remove tags across many contracts
      description: |
        One call instead of N. `add` and `remove` both apply, in that
        order, to every contract named.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contractIds]
              properties:
                contractIds:
                  type: array
                  items: { type: string }
                add:
                  type: array
                  items: { type: string }
                remove:
                  type: array
                  items: { type: string }
      responses:
        "200":
          description: How many contracts changed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      updated: { type: integer }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/bulk-set-status:
    post:
      tags: [Contracts]
      operationId: bulkSetContractStatus
      summary: Set the lifecycle status of many contracts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contractIds, status]
              properties:
                contractIds:
                  type: array
                  items: { type: string }
                status:
                  $ref: "#/components/schemas/ContractStatus"
      responses:
        "200":
          description: The outcome, in counts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      updated: { type: integer }
                      unchanged:
                        type: integer
                        description: Already in that status.
                      missing:
                        type: integer
                        description: Ids that matched no contract.
                      failed:
                        type: array
                        description: >-
                          The ids that could not be processed, each with the reason.
                          An array rather than a count: a caller that has just sent
                          fifty ids needs to know WHICH failed to retry them.
                        items:
                          type: object
                          properties:
                            contractId: { type: string, format: uuid }
                            reason: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/bulk-delete:
    post:
      tags: [Contracts]
      operationId: bulkDeleteContracts
      summary: Delete many contracts
      description: |
        PERMANENT, and it takes everything under each contract —
        versions, reviews, redlines, key dates. There is no trash to
        recover them from.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contractIds]
              properties:
                contractIds:
                  type: array
                  items: { type: string }
      responses:
        "200":
          description: The outcome, in counts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      deleted: { type: integer }
                      missing: { type: integer }
                      failed:
                        type: array
                        description: >-
                          The ids that could not be processed, each with the reason.
                          An array rather than a count: a caller that has just sent
                          fifty ids needs to know WHICH failed to retry them.
                        items:
                          type: object
                          properties:
                            contractId: { type: string, format: uuid }
                            reason: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/{id}:
    get:
      tags: [Contracts]
      operationId: getContract
      summary: Get a contract
      parameters:
        - $ref: "#/components/parameters/ContractId"
      responses:
        "200":
          description: |
            The contract itself, at `data` — not nested under a
            `contract` key. The four next-step signals ride alongside
            the contract's own fields.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: "#/components/schemas/Contract"
                      - type: object
                        properties:
                          hasReview:
                            type: boolean
                            description: An AI review has produced findings for this contract.
                          hasRedline:
                            type: boolean
                            description: A tracked-changes version exists.
                          latestReviewId: { type: string, nullable: true }
                          assessmentPendingCount:
                            type: integer
                            description: Findings still awaiting a human verdict.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
    put:
      tags: [Contracts]
      operationId: updateContract
      summary: Update a contract
      description: |
        Update editable fields. Omitted fields are left unchanged (a
        partial merge, not a full replace). AI-extracted fields — parties,
        the key dates, duration, and the financial terms — can be
        overridden here when you have a better source of truth. `parties`
        replaces the whole list.
      parameters:
        - $ref: "#/components/parameters/ContractId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                summary: { type: string, nullable: true }
                status:
                  $ref: "#/components/schemas/ContractStatus"
                riskLevel:
                  allOf: [{ $ref: "#/components/schemas/RiskLevel" }]
                  nullable: true
                tags:
                  type: array
                  items: { type: string }
                parties:
                  type: array
                  description: Replaces the whole parties list.
                  items: { $ref: "#/components/schemas/ContractParty" }
                effectiveDate: { type: string, format: date, nullable: true }
                expirationDate: { type: string, format: date, nullable: true }
                renewalDate: { type: string, format: date, nullable: true }
                terminationDate: { type: string, format: date, nullable: true }
                durationText:
                  type: string
                  nullable: true
                  description: Free-text duration override, e.g. "2 years".
                amount: { type: number, nullable: true }
                currency: { type: string, nullable: true, example: "SGD" }
                paymentFrequency:
                  allOf: [{ $ref: "#/components/schemas/PaymentFrequency" }]
                  nullable: true
                paymentTerms: { type: string, nullable: true }
                oneTimeFees:
                  type: array
                  nullable: true
                  description: |
                    Replaces the whole list of non-recurring charges. Read
                    the contract first and send the fees you want to keep —
                    a bare new fee wipes the others.
                  items:
                    type: object
                    required: [label, amount]
                    properties:
                      label: { type: string }
                      amount: { type: number }
                totalValueOverride:
                  type: number
                  nullable: true
                  description: |
                    An entered total that replaces the calculation. Send
                    null to go back to the computed figure.
                value:
                  type: number
                  nullable: true
                  deprecated: true
                  description: |
                    DEPRECATED — a legacy alias of `amount`, not a total.
                    Do not write it; set `totalValueOverride` instead.
      responses:
        "200":
          description: |
            The updated contract itself, at `data` — not nested under a
            `contract` key.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Contract" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Contracts]
      operationId: deleteContract
      summary: Delete a contract
      description: |
        Permanently deletes a contract and its cascade — versions,
        chunks, reviews, reminders, custom dates. This is not reversible.
      parameters:
        - $ref: "#/components/parameters/ContractId"
      responses:
        "204": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/upload/jobs/{jobId}:
    get:
      tags: [Contracts]
      operationId: getUploadJob
      summary: Get upload job status
      description: Poll for the outcome of an asynchronous upload.
      parameters:
        - name: jobId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: Job status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status:
                        $ref: "#/components/schemas/JobStatus"
                      contractId:
                        type: string
                        description: Present once status is `complete`.
                      error:
                        type: string
                        description: Present when status is `error`.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/{id}/dates:
    get:
      tags: [Key dates]
      operationId: listContractDates
      summary: Custom dates on a contract
      description: |
        The dates a person added to this contract, as distinct from the
        canonical ones the AI extracted (effective, expiration, renewal,
        termination — those live on the contract record and in
        `GET /dates`).
      parameters:
        - $ref: "#/components/parameters/ContractId"
      responses:
        "200":
          description: The contract's custom dates.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      customDates:
                        type: array
                        items: { $ref: "#/components/schemas/CustomDate" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [Key dates]
      operationId: createContractDate
      summary: Add a custom date to a contract
      description: |
        For the milestones a contract implies but does not state as a
        field — a rate review, a notice window opening, a delivery
        gate. Set a reminder on it with `POST /dates/{id}/set-reminder`.
      parameters:
        - $ref: "#/components/parameters/ContractId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [label, date]
              properties:
                label: { type: string, maxLength: 200 }
                date: { type: string, format: date }
                notes: { type: string, nullable: true }
      responses:
        "201":
          description: The date as stored.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      customDate: { $ref: "#/components/schemas/CustomDate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/{id}/dates/{dateId}:
    patch:
      tags: [Key dates]
      operationId: updateContractDate
      summary: Edit a custom date
      parameters:
        - $ref: "#/components/parameters/ContractId"
        - name: dateId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                label: { type: string, maxLength: 200 }
                date: { type: string, format: date }
                notes: { type: string, nullable: true }
      responses:
        "200":
          description: The updated date.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      customDate: { $ref: "#/components/schemas/CustomDate" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/{id}/versions/{versionId}/label:
    patch:
      tags: [Contracts]
      operationId: renameVersionLabel
      summary: Rename a document version
      description: |
        The label is what people see in the version list and the
        attachment pickers — "Original", "Counterparty markup",
        "Schedule 1 — SLA".

        Renaming an ATTACHMENT renames every version in its group: the
        group is one document with a history, and a label that changed
        between revisions would read as two different documents.
      parameters:
        - $ref: "#/components/parameters/ContractId"
        - name: versionId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [label]
              properties:
                label: { type: string, maxLength: 120 }
      responses:
        "200":
          description: What was renamed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      renamed:
                        type: integer
                        description: Version rows affected — more than one for an attachment group.
                      label: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/{id}/versions:
    get:
      tags: [Contracts]
      operationId: listContractVersions
      summary: List a contract's document versions
      description: |
        Returns every `DocumentVersion` for the contract — the primary
        document, its revisions, and any **attachments** (amendments,
        schedules, exhibits, side-letters). This is where you find the
        `versionId` values for `POST /contracts/{id}/review`
        (`versionId`, `reviewAttachmentIds`, `supportingAttachmentIds`):
        the primary has `role: "primary"`; attachments have any other
        `role`. Download a version's bytes with
        `GET /contracts/{id}/versions/{versionId}/download`.
      parameters:
        - $ref: "#/components/parameters/ContractId"
      responses:
        "200":
          description: The contract's versions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      versions:
                        type: array
                        items: { $ref: "#/components/schemas/DocumentVersion" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/{id}/versions/{versionId}/text:
    get:
      tags: [Contracts]
      operationId: getContractText
      summary: Read a document's extracted text
      description: |
        The text as the AI pipeline reads it — the PDF text layer for
        born-digital files, Adobe Extract paragraphs for scans (computed
        once, then cached), mammoth for Word. When a Repair Document pass
        has run, its repaired text is returned instead, because that is
        the version reviews work from.

        `source` says which of those produced it, and `cached` whether it
        came from the sidecar rather than being extracted just now.
      parameters:
        - $ref: "#/components/parameters/ContractId"
        - name: versionId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The document's text.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      text: { type: string }
                      source:
                        type: string
                        description: e.g. pdf, docx, adobe-extract, repaired.
                      cached: { type: boolean }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/{id}/activity:
    get:
      tags: [Contracts]
      operationId: listContractActivity
      summary: What has happened to this contract
      description: |
        A merged, chronological feed: uploads and new versions, reviews
        run and signed off, redlines generated, metadata edits, comments
        and assignments. The same stream the contract's Activity tab
        shows.
      parameters:
        - $ref: "#/components/parameters/ContractId"
      responses:
        "200":
          description: The activity feed, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      events:
                        type: array
                        items:
                          type: object
                          properties:
                            type: { type: string }
                            at: { type: string, format: date-time }
                            actorUserId: { type: string, nullable: true }
                            actorName: { type: string, nullable: true }
                            summary: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /contracts/{id}/versions/{versionId}/download:
    get:
      tags: [Contracts]
      operationId: downloadContractVersion
      summary: Download a document version
      description: |
        Stream the bytes of a specific version (primary, revision, or
        attachment). Get `versionId`s from
        `GET /contracts/{id}/versions`.

        For a **redline**, `comments` trims the Word comments on the
        way out so the document can go to a counterparty quieter than
        it was generated. The transform is deterministic and free —
        no AI runs, no credits are charged, and the stored version is
        never modified, so a later download without the parameter
        still returns the full document.
      parameters:
        - $ref: "#/components/parameters/ContractId"
        - name: versionId
          in: path
          required: true
          schema: { type: string }
        - name: comments
          in: query
          required: false
          schema:
            type: string
            enum: [brief, none]
          description: |
            Redline DOCX only. `brief` cuts each Word comment to its
            ask; `none` removes the comments entirely, leaving the
            tracked changes. Omit for the document exactly as stored.
            Ignored for non-redline or non-DOCX versions. Trimming is
            one-way within the downloaded file — request again without
            the parameter to get the full version back.
      responses:
        "200":
          description: The file bytes (PDF or DOCX).
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/{id}/review:
    post:
      tags: [Reviews]
      operationId: startReview
      summary: Start an AI review
      description: |
        Run a rulebook review against a contract. Returns a job id;
        poll `GET /jobs/review/{id}` until `complete`, then fetch the
        produced review with `GET /reviews/{reviewId}`.

        Consumes credits on success (a `standard` review costs less than
        a `deep` one; both scale with page count). For live progress
        send `Accept: text/event-stream`; otherwise you get the job id
        as JSON.
      parameters:
        - $ref: "#/components/parameters/ContractId"
      requestBody:
        required: true
        description: |
          Provide **either** `rulebookId` **or** `customInstructions`
          (you may provide both — the instructions layer on top of the
          rulebook).
        content:
          application/json:
            schema:
              type: object
              properties:
                rulebookId:
                  type: string
                  description: |
                    Id of the rulebook to evaluate against — get one from
                    `GET /rulebooks`. (The in-app client may instead send
                    the full rulebook object as `rulebook`; for the API,
                    use `rulebookId`.)
                customInstructions:
                  type: string
                  maxLength: 2000
                  description: |
                    Free-text review instructions, applied on top of (or
                    instead of) the rulebook. Use this with no `rulebookId`
                    for an ad-hoc review.
                mode:
                  allOf: [{ $ref: "#/components/schemas/ReviewMode" }]
                  default: standard
                  description: '`deep` runs extended reasoning at a higher credit cost.'
                reviewStrategy:
                  type: string
                  enum: [negotiation, high_priority, strict]
                  description: |
                    How assertive the review is and how much lands in the
                    redline by default. `negotiation` ("Understand & Negotiate",
                    the default) surfaces everything AND flags what's worth
                    raising, with medium severity and up pre-included in the
                    redline; `high_priority` only flags what could genuinely
                    cause problems (high severity and up); `strict` flags every
                    deviation from the rulebook (all severities). It changes the
                    review's posture, not what is checked. Defaults to the
                    rulebook's default strategy, or `negotiation`.
                produceExecutiveSummary:
                  type: boolean
                  default: false
                  description: |
                    When true, the review also produces a short plain-English
                    executive summary for a non-lawyer audience, alongside the
                    detailed findings.
                versionId:
                  type: string
                  description: |
                    The `DocumentVersion` id of the contract document to
                    review. Defaults to the contract's current primary
                    version. List a contract's versions (and their ids)
                    with `GET /contracts/{id}/versions`.
                repairDocument:
                  type: boolean
                  description: |
                    Repair the document's text before reviewing. **Omit for
                    the recommended behaviour**: the repair runs automatically
                    when the document's text is flagged as garbled — the same
                    condition under which the in-app review setup recommends
                    "Repair document". `true` forces a repair (PDF sources
                    only); `false` never repairs. The repair is cached and
                    charged once; it permanently upgrades the document's
                    canonical text so this and every later review read the
                    repaired version.
                reviewAttachmentIds:
                  type: array
                  items: { type: string }
                  description: |
                    `DocumentVersion` ids of attachments to review as
                    subjects (findings ARE raised against them). Get
                    attachment ids from `GET /contracts/{id}/versions`
                    (rows with `role` other than `primary`).
                supportingAttachmentIds:
                  type: array
                  items: { type: string }
                  description: |
                    `DocumentVersion` ids of attachments supplied as
                    context only — visible to the model but NO findings
                    are raised against them. Same source as above.
                rerunFromReviewId:
                  type: string
                  description: Re-run an existing review — rulebook + mode are inherited from it.
                scope:
                  type: string
                  enum: [full, tracked_changes, two_version, checkin]
                  default: full
                  description: |
                    What the review looks at.

                    `full` (the default) evaluates the whole document
                    against the rulebook. `two_version` COMPARES two
                    versions and raises findings on what changed between
                    them — this is how a comparison is run; there is no
                    separate compare endpoint. `tracked_changes` scopes
                    the review to the marked-up edits in a DOCX.
                    `checkin` frames it as a compliance check against the
                    rulebook rather than a negotiation.

                    A comparison review carries `comparisonSources` on
                    the result, naming the two documents it read, so a
                    client can render them side by side.
                scopeDetail:
                  type: object
                  additionalProperties: true
                  description: |
                    Variant detail for the scope. `two_version` takes the
                    version to compare against; `tracked_changes` takes
                    the filter over the marked-up edits. Shape follows
                    the scope — see the review-scopes guide.
      responses:
        "202":
          description: |
            Review job accepted. Poll `GET /jobs/review/{jobId}` until it
            reports `complete`, then read the review it names. The job id
            comes back immediately — a review runs for minutes, far past
            any sensible request timeout.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    required: [jobId]
                    properties:
                      jobId: { type: string }
                      mode:
                        allOf: [{ $ref: "#/components/schemas/ReviewMode" }]
                        description: The mode the job actually runs in.
                      steps:
                        type: array
                        description: |
                          The stages this mode works through, in order. A
                          poller can count them off against the `steps`
                          accumulating on the job.
                        items:
                          type: object
                          properties:
                            step: { type: string }
                            label: { type: string }
                      estimateSeconds:
                        type: integer
                        description: |
                          Roughly how long the job should take, from this
                          region's own measurements of comparable reviews
                          (page count, mode, document count). Advisory,
                          never authoritative — poll for the truth. Covers
                          the review job only, not any document repair
                          that runs first.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/{id}/generate-redline:
    post:
      tags: [Reviews]
      operationId: generateRedline
      summary: Generate a redline
      description: |
        Produce a tracked-changes DOCX from a set of review findings and
        save it as a new document version. Returns a job id; poll
        `GET /jobs/redline/{id}` until `complete`, then download the DOCX
        from `GET /contracts/download/{token}` (the token is the job id).

        Typically you'd `GET /reviews/{reviewId}` first and pass the
        findings you want applied (those with `includeInRedline: true`).
      parameters:
        - $ref: "#/components/parameters/ContractId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [playbookName, findings]
              properties:
                playbookName:
                  type: string
                  description: |
                    A plain-text **label** shown on the produced redline
                    version (e.g. the rulebook the findings came from).
                    This is display text only — unlike the review start's
                    `rulebookId`, it does NOT resolve a rulebook. Any
                    string is fine; the rulebook's name is the natural
                    choice. (The field name is retained for
                    compatibility.)
                findings:
                  type: array
                  description: The findings to apply, usually taken from a review.
                  items: { $ref: "#/components/schemas/Finding" }
                sourceReviewId:
                  type: string
                  description: The review these findings came from (links the redline to it).
                targetVersionId:
                  type: string
                  description: Which version to redline. Defaults to the latest primary version (the current contract); pass an attachment version id to redline that instead.
                author:
                  type: string
                  description: Author name for the tracked changes (≤ 120 chars). Defaults to your redline-author preference, else "Clment AI".
                commentLevel:
                  type: string
                  enum: [explanatory, brief, none]
                  description: |
                    How much explanation the Word comments carry.
                    `explanatory` (the default) gives the reasoning
                    behind each change; `brief` states the ask in one
                    line and omits the reviewer's note; `none`
                    produces tracked changes with no comments at all.
                    Omit to use the user's saved preference. Comments
                    can also be trimmed at download time — see the
                    `comments` query parameter on
                    `GET /contracts/{id}/versions/{versionId}/download`
                    — which costs nothing and leaves the stored
                    document untouched.
                style:
                  type: string
                  enum: [auto, original_format, fresh_start]
                  default: auto
                  description: |
                    Which Word style to mark the changes in. **`auto` (the
                    default) follows the recommendation Clment shows
                    in-app**: Fresh Start for scanned PDFs, repaired
                    documents, and PDFs whose Word copy measured as hard to
                    edit; Original Format otherwise. `fresh_start` marks the
                    changes in a clean rebuilt Word document — reused free
                    when the rebuild already exists, otherwise built first
                    and charged at the Fresh Start rate. `original_format`
                    keeps the document's existing layout. Only primary PDF
                    documents qualify for Fresh Start; attachments and Word
                    documents always use their original format.
      responses:
        "202":
          description: |
            Redline job accepted. Poll `GET /jobs/redline/{jobId}` until
            it reports `complete`, then download the document with the
            job id as the token.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    required: [jobId]
                    properties:
                      jobId: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/download/{token}:
    get:
      tags: [Reviews]
      operationId: downloadRedline
      summary: Download a generated redline
      description: |
        Download the DOCX produced by a redline job. The `token` is the
        redline `jobId`. Valid for ~1 hour after the job completes.
      parameters:
        - name: token
          in: path
          required: true
          schema: { type: string }
          description: The redline job id.
      responses:
        "200":
          description: The redlined DOCX.
          content:
            application/vnd.openxmlformats-officedocument.wordprocessingml.document:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /reviews:
    get:
      tags: [Reviews]
      operationId: listReviews
      summary: List reviews
      description: |
        Returns AI reviews across the organization, newest first. Filter
        by `status`, `contractId`, or `assignedToUserId`, and page with
        `skip`/`take` as on every other list endpoint.
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [active, complete, all], default: all }
        - name: contractId
          in: query
          schema: { type: string }
        - name: assignedToUserId
          in: query
          schema: { type: string }
        - name: skip
          in: query
          description: Rows to skip, for paging.
          schema: { type: integer, default: 0, minimum: 0 }
        - name: take
          in: query
          description: Rows to return, capped at 100 as on every other list endpoint.
          schema: { type: integer, default: 100, minimum: 1, maximum: 100 }
        - name: limit
          in: query
          deprecated: true
          description: >-
            Deprecated alias for `take`, kept because it was once the only
            option this endpoint offered. Ignored when `take` is given, and
            it allows up to 500 where `take` caps at 100. Use `skip`/`take`.
          schema: { type: integer, maximum: 500 }
      responses:
        "200":
          description: Matching reviews.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      reviews:
                        type: array
                        items: { $ref: "#/components/schemas/ReviewSummary" }
                      total:
                        type: integer
                        description: Reviews matching the filter, before paging.
                      skip: { type: integer }
                      take: { type: integer }
                      hasMore:
                        type: boolean
                        description: Whether a further page exists.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /reviews/me/assigned:
    get:
      tags: [Reviews]
      operationId: listMyReviews
      summary: Reviews assigned to you
      description: |
        Active reviews assigned to the calling user, newest first —
        the "what's on my plate" list. Signed-off reviews drop out.

        An API key authenticates as the ORGANIZATION and has no personal
        queue, so a key caller gets an empty list; use
        `GET /reviews?assignedToUserId=…` to ask on someone's behalf.
      responses:
        "200":
          description: The caller's assigned reviews.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      reviews:
                        type: array
                        items: { $ref: "#/components/schemas/ReviewSummary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /reviews/{id}/assignee:
    patch:
      tags: [Reviews]
      operationId: assignReview
      summary: Assign a review, or clear the assignment
      description: |
        Hand a review to a colleague. Ids come from
        `GET /collaborators`; pass `null` to leave it unassigned.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [userId]
              properties:
                userId:
                  type: string
                  nullable: true
                  description: The member to assign it to, or null to clear.
      responses:
        "200":
          description: The review, reassigned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      review: { $ref: "#/components/schemas/Review" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /reviews/by-number/{reviewNumber}:
    get:
      tags: [Reviews]
      operationId: getReviewByNumber
      summary: Get a review by its REV number
      description: |
        The same review `GET /reviews/{id}` returns, addressed by the
        human REV number people actually quote to each other. Saves
        listing and filtering when all you have is "REV-12".
      parameters:
        - name: reviewNumber
          in: path
          required: true
          schema: { type: integer }
          description: The sequential REV number. Render as "REV-{n}".
      responses:
        "200":
          description: The review.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      review: { $ref: "#/components/schemas/Review" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /reviews/{id}/comments:
    post:
      tags: [Reviews]
      operationId: addReviewComment
      summary: Comment on a review
      description: |
        Append an attributed comment to the review's thread. Tag
        colleagues with `mentionedUserIds` and each newly-tagged person
        is emailed; resolve their ids from `GET /collaborators`.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string, maxLength: 5000 }
                mentionedUserIds:
                  type: array
                  items: { type: string }
                  description: |
                    Org user ids to tag. Ids that don't belong to an
                    active member of this organization are dropped, not
                    rejected.
      responses:
        "201":
          description: The comment, and the review it now belongs to.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      review: { $ref: "#/components/schemas/Review" }
                      comment: { $ref: "#/components/schemas/ReviewerComment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /reviews/{id}/findings/{findingId}/comments:
    post:
      tags: [Reviews]
      operationId: addFindingComment
      summary: Comment on a finding
      description: |
        Append an attributed comment to one finding's thread.

        `kind` is the part that does work. A plain `comment` is
        discussion. A `redline_instruction` is a directive to the
        redline pipeline: the LATEST one on a finding replaces the AI's
        `suggestedLanguage` as the source of truth for what the redline
        produces. Earlier instructions stay in the thread as history —
        only the most recent drives the document.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: findingId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [text]
              properties:
                text: { type: string, maxLength: 5000 }
                kind:
                  allOf: [{ $ref: "#/components/schemas/CommentKind" }]
                  default: comment
                  description: |
                    `redline_instruction` makes this comment the wording
                    the redline should use, overriding the AI's proposal.
                mentionedUserIds:
                  type: array
                  items: { type: string }
                  description: Org user ids to tag; resolve them from `GET /collaborators`.
      responses:
        "201":
          description: The comment, and the review it now belongs to.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      review: { $ref: "#/components/schemas/Review" }
                      comment: { $ref: "#/components/schemas/ReviewerComment" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /reviews/{id}:
    get:
      tags: [Reviews]
      operationId: getReview
      summary: Get a review
      description: |
        Returns the full review including every finding, its AI
        recommendation, the human verdict (if any), and whether it's
        flagged for inclusion in a redline.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The review.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      review: { $ref: "#/components/schemas/Review" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /reviews/{id}/report.docx:
    get:
      tags: [Reviews]
      operationId: downloadReviewReport
      summary: Download the review report (Word)
      description: |
        Download a formatted Word report of the review — summary,
        findings, recommendations, and human verdicts. Authenticated
        like every other call (send your `Authorization: Bearer` key);
        no separate download token is needed.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The review report DOCX.
          content:
            application/vnd.openxmlformats-officedocument.wordprocessingml.document:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /reviews/{id}/findings/{findingId}:
    patch:
      tags: [Reviews]
      operationId: updateFinding
      summary: Apply a decision to a finding
      description: |
        Record the human verdict on a finding — the REST equivalent of
        the MCP `set_finding_decision` skill. All four controls are
        independent; pass only the ones you're changing. Pass `null` to
        clear a field (or, for `includeInRedline`, to reset it to the
        recommendation-derived default).
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
        - name: findingId
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                humanDecision:
                  allOf: [{ $ref: "#/components/schemas/HumanDecision" }]
                  nullable: true
                  description: Your verdict on the AI's recommendation. `null` clears it.
                humanNote:
                  type: string
                  nullable: true
                  description: |
                    Free-text reviewer note (≤ 5000 chars). The single-slot
                    note kept for compatibility — for a threaded,
                    attributed discussion use
                    `POST /reviews/{id}/findings/{findingId}/comments`.
                humanNoteMentionedUserIds:
                  type: array
                  nullable: true
                  items: { type: string }
                  description: |
                    Org user ids tagged in `humanNote`; each newly-added
                    one is emailed. Resolve ids from `GET /collaborators`.
                suggestedLanguage:
                  type: string
                  nullable: true
                  description: Replacement / fallback clause language (≤ 10000 chars).
                includeInRedline:
                  type: boolean
                  nullable: true
                  description: |
                    Force this finding into (`true`) or out of (`false`)
                    the redline. `null` resets to the default derived
                    from the AI recommendation.
      responses:
        "200":
          description: The updated review.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      review: { $ref: "#/components/schemas/Review" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /reviews/{id}/complete:
    post:
      tags: [Reviews]
      operationId: completeReview
      summary: Sign off (complete) a review
      description: |
        Add the caller's sign-off, marking the review complete. The
        first sign-off flips the review to `completed` and fires the
        `review.completed` webhook. Idempotent per caller.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The completed review.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      review: { $ref: "#/components/schemas/Review" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /jobs/review/{id}:
    get:
      tags: [Jobs]
      operationId: getReviewJob
      summary: Get review job status
      description: |
        Poll a review job started by `POST /contracts/{id}/review`. When
        `status` is `complete`, `reviewId` points at the produced review
        (fetch it with `GET /reviews/{reviewId}`).
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: The review job id.
      responses:
        "200":
          description: Job status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status:
                        $ref: "#/components/schemas/JobStatus"
                      reviewId:
                        type: string
                        nullable: true
                        description: >-
                          The review this job produced. Explicitly null until
                          `status` is `complete`, not absent — the route lifts
                          it with `?? null` so a poller can read one shape
                          throughout.
                      error:
                        type: string
                        description: Present when `status` is `error`.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /jobs/redline/{id}:
    get:
      tags: [Jobs]
      operationId: getRedlineJob
      summary: Get redline job status
      description: |
        Poll a redline job started by
        `POST /contracts/{id}/generate-redline`. When `status` is
        `complete`, download the DOCX from
        `GET /contracts/download/{token}` using this job id as the token.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: The redline job id (also the download token).
      responses:
        "200":
          description: Job status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      status:
                        $ref: "#/components/schemas/JobStatus"
                      versionId:
                        type: string
                        nullable: true
                        # nullable because the route lifts it with `?? null`
                        # while the job is still queued or processing. Without
                        # this the C# generator types it as a non-nullable
                        # value type and THROWS on the first poll of a
                        # perfectly healthy job. Python survives it (Optional
                        # either way) and TypeScript never looks - which is
                        # why only the .NET runner found it.
                        description: The saved redline version, once complete.
                      appliedCount:
                        type: integer
                        nullable: true
                        description: >-
                          Number of findings applied as tracked changes. Null
                          until the job completes.
                      error:
                        type: string
                        nullable: true
                        description: Present when `status` is `error`.
                      result:
                        type: object
                        nullable: true
                        description: >-
                          Populated once the job finishes. **Check
                          `result.outcome.ok` before downloading.** A job can
                          reach `status: complete` having produced nothing to
                          download — the model may propose no change that
                          survives validation, which is a normal outcome on a
                          document that already meets the rulebook. In that
                          case `GET /contracts/download/{token}` returns 404,
                          and this object is the only place that says why.
                        properties:
                          outcome:
                            type: object
                            properties:
                              ok:
                                type: boolean
                                description: >-
                                  True when a document was produced and is
                                  downloadable. False when nothing was.
                              summary:
                                type: string
                                description: Plain-language account of what the redline did, or why it did nothing.
                              appliedCount:
                                type: integer
                                description: >-
                                  Changes actually written into the document.
                                  Can be 0 on a successful redline whose
                                  changes were all comments.
                              totalChanges:
                                type: integer
                                description: Changes the model proposed, before validation.
                              noChanges:
                                type: boolean
                                description: >-
                                  True when `ok` is false because nothing was
                                  proposed that survived validation — an
                                  informational result, not a failure.
                              error:
                                type: string
                                description: Present when `ok` is false and the cause was a fault rather than an empty result.
                              step:
                                type: string
                                enum: [convert, generate, apply]
                                description: Which stage produced the failure, when one did.
                              skipped:
                                type: array
                                description: >-
                                  Proposed CHANGES that did not make it into
                                  the document, each with the reason. Note the
                                  unit: these are edits the model proposed, not
                                  findings — a finding has no skipped state
                                  (its status is pass, fail, warning or
                                  not_applicable). One finding can produce
                                  several changes, or none.


                                  This is what lets a client say "1 of 5
                                  changes was not applied, because the clause
                                  it targeted was not found" instead of showing
                                  a bare count.
                                items:
                                  type: object
                                  properties:
                                    findingId:
                                      type: string
                                      description: >-
                                        The finding this change came from, when
                                        the proposal carried one. Use it to
                                        attribute a skipped change back to the
                                        finding a reader already knows about.
                                    section:
                                      type: string
                                      description: >-
                                        The clause or section the change was
                                        proposed against. For
                                        `no_change_generated` this is the
                                        finding's title instead.
                                    rationale:
                                      type: string
                                      description: >-
                                        Why the model thought the change
                                        mattered. For `no_change_generated`
                                        this is the finding's note instead.
                                    originalSnippet:
                                      type: string
                                      description: >-
                                        The first ~120 characters of the
                                        verbatim text the change tried to
                                        anchor on. The practical way to see
                                        why an anchor failed.
                                    reason:
                                      type: string
                                      description: >-
                                        Why it was not applied. Branch on this
                                        rather than on the prose above:
                                        `no_change_generated` means the model
                                        produced nothing for that finding,
                                        which is an outcome rather than a
                                        fault. The other five are anchoring or
                                        application failures.
                                      enum:
                                        - no_change_generated
                                        - no_original_text
                                        - original_not_found_in_document
                                        - apply_step_failed
                                        - conflicted_with_applied_change
                                        - pure_renumbering_stripped
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /rulebooks:
    get:
      tags: [Rulebooks]
      operationId: listRulebooks
      summary: List rulebooks
      parameters:
        - name: status
          in: query
          schema: { $ref: "#/components/schemas/RulebookStatus" }
      responses:
        "200":
          description: Rulebooks for the organization.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      rulebooks:
                        type: array
                        items: { $ref: "#/components/schemas/Rulebook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [Rulebooks]
      operationId: createRulebook
      summary: Create a rulebook
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, instructions]
              properties:
                name: { type: string }
                description: { type: string }
                instructions:
                  type: string
                  description: The review rulebook, in markdown.
                status:
                  allOf: [{ $ref: "#/components/schemas/RulebookStatus" }]
                  default: draft
                tags:
                  type: array
                  items: { type: string }
      responses:
        "201":
          description: The created rulebook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      rulebook: { $ref: "#/components/schemas/Rulebook" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /rulebooks/{id}:
    get:
      tags: [Rulebooks]
      operationId: getRulebook
      summary: Get a rulebook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The rulebook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      rulebook: { $ref: "#/components/schemas/Rulebook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Rulebooks]
      operationId: deleteRulebook
      summary: Delete a rulebook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "204": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "404": { $ref: "#/components/responses/NotFound" }

  /dates:
    get:
      tags: [Key dates]
      operationId: listKeyDates
      summary: List key dates
      description: |
        Returns upcoming and historical key dates across the org —
        renewal, expiry, effective, and custom dates — sorted by date.
      parameters:
        - name: from
          in: query
          schema: { type: string, format: date }
        - name: to
          in: query
          schema: { type: string, format: date }
      responses:
        "200":
          description: Key dates.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      dates:
                        type: array
                        items: { $ref: "#/components/schemas/KeyDate" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /ask:
    post:
      tags: [Search]
      operationId: askQuestion
      summary: Ask a question across your contracts
      description: |
        A natural-language answer drawn from the organization's
        contracts, with citations.

        The answer is grounded in a RELEVANCE SAMPLE, not a sweep: the
        retriever fetches the passages most likely to bear on the
        question and answers from those. `retrievalNote` and
        `answerScope` say how many contracts that was out of how many
        exist — quote them rather than presenting the answer as
        portfolio-wide. When you need every match, use `GET /search`.

        Costs 1 credit.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [question]
              properties:
                question: { type: string, maxLength: 2000 }
                scope:
                  type: object
                  description: Narrow the contracts considered.
                  properties:
                    contractIds:
                      type: array
                      items: { type: string }
                    status:
                      type: array
                      items: { type: string }
                    tags:
                      type: array
                      items: { type: string }
                verbosity:
                  type: string
                  enum: [minimal, standard, detailed]
                  default: standard
      responses:
        "200":
          description: The answer, its citations, and what it was drawn from.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      answer:
                        type: string
                        description: Markdown. Render it or strip it — it is written for a person to read.
                      citations:
                        type: array
                        items:
                          type: object
                          properties:
                            contractId: { type: string }
                            clmNumber: { type: integer, nullable: true }
                            contractName: { type: string }
                            excerpt: { type: string }
                            page: { type: integer }
                            url: { type: string }
                      consideredContracts:
                        type: array
                        description: |
                          The contracts whose text the retriever pulled
                          in. A sample, NOT the portfolio.
                        items:
                          type: object
                          properties:
                            contractId: { type: string }
                            clmNumber: { type: integer, nullable: true }
                            contractName: { type: string }
                      retrievalNote:
                        type: string
                        description: A sentence you can show the user verbatim about what the answer covers.
                      answerScope:
                        type: object
                        properties:
                          consideredContracts: { type: integer }
                          totalContractsInOrg: { type: integer }
                      creditsCharged: { type: integer }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /search:
    get:
      tags: [Search]
      operationId: searchContracts
      summary: Search contracts
      description: |
        Natural-language + hybrid search across the organization's
        contracts. Returns ranked matches with the contract id and a
        relevance score.
      parameters:
        - name: q
          in: query
          required: true
          schema: { type: string }
          description: The search query.
        - name: take
          in: query
          schema: { type: integer, default: 20, maximum: 100 }
      responses:
        "200":
          description: Ranked search hits.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      results:
                        type: array
                        description: |
                          One entry per matching CONTRACT, best match
                          first. The index is passage-level, so a
                          contract matching in several passages appears
                          once, carrying its strongest passage as the
                          snippet.
                        items:
                          type: object
                          properties:
                            contractId: { type: string }
                            title:
                              type: string
                              description: The contract's title, or its file name when untitled.
                            score: { type: number }
                            snippet: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "503":
          description: |
            The search index is not reachable for this deployment.
            Distinct from an empty result set — this one is worth
            retrying.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /collaborators:
    get:
      tags: [Collaborators]
      operationId: listCollaborators
      summary: List the people you can tag or assign to
      description: |
        The organization's active members, as the ids that mentions
        (`mentionedUserIds`) and review assignment (`assignedToUserId`)
        take. Read-only: managing members — inviting, roles, removal —
        remains a settings surface an API key cannot reach.

        Invited-but-not-joined and revoked accounts are omitted; tagging
        them would silently do nothing.
      responses:
        "200":
          description: The org's active members.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      users:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: string }
                            name: { type: string, nullable: true }
                            email: { type: string, nullable: true }
                            role: { type: string, enum: [admin, member] }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /dates/{id}/set-reminder:
    post:
      tags: [Key dates]
      operationId: updateReminder
      summary: Set or clear a reminder on a key date
      description: |
        Schedule an email reminder ahead of a date, or clear one. A date
        already in the past is refused — the reminder could never fire.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: The key-date id, from `GET /dates`.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                daysBefore:
                  type: integer
                  minimum: 1
                  description: How many days ahead to send it. Defaults to the org's setting.
                enabled:
                  type: boolean
                  description: False clears an existing reminder.
      responses:
        "200":
          description: The reminder as it now stands.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      reminder:
                        type: object
                        nullable: true
                        properties:
                          id: { type: string }
                          daysBefore: { type: integer }
                          createdByUserId: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /dashboard/insights/summary:
    get:
      tags: [Insights]
      operationId: getInsights
      summary: Portfolio KPIs
      description: |
        The headline numbers behind the Insights dashboard — contract
        counts by status, value under management, risk spread, review
        throughput. Free, and computed over the whole organization.
      responses:
        "200":
          description: The KPI summary.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/Insights" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /me/mentions:
    get:
      tags: [Collaborators]
      operationId: listMyMentions
      summary: Comments that tagged you
      description: |
        Every @mention of you on a review or a finding, newest first —
        the "what needs my attention" inbox.

        An API key authenticates as the ORGANIZATION and has no personal
        inbox, so a key caller must name someone with `userId`; ids come
        from `GET /collaborators`. Without it the call is refused rather
        than returning a confusing empty list.
      parameters:
        - name: userId
          in: query
          schema: { type: string }
          description: Whose mentions to read. Required for API-key callers.
        - name: limit
          in: query
          schema: { type: integer, default: 50, maximum: 200 }
      responses:
        "200":
          description: The mentions.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      mentions:
                        type: array
                        items: { $ref: "#/components/schemas/Mention" }
                      totalMatched:
                        type: integer
                        description: Total mentions matching the filter, before any limit.
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /me/organization:
    get:
      tags: [Collaborators]
      operationId: getOrgSummary
      summary: The organization at a glance
      description: |
        Headline counts for the workspace — contracts, reviews,
        rulebooks, members — plus the plan and credit position. What an
        integration needs to orient itself on first contact.
      responses:
        "200":
          description: The summary.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/OrgSummary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /usage/current:
    get:
      tags: [Usage]
      operationId: getUsage
      summary: Credits and limits for the current period
      description: |
        What an integration needs to avoid discovering exhaustion as a
        402 halfway through a batch: credits used and remaining, the
        plan limits they sit against, and where the period ends.
      responses:
        "200":
          description: Usage for the current billing period.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/UsageSummary" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /chat-sessions:
    get:
      tags: [Search]
      operationId: listChatSessions
      summary: Your saved conversations
      description: |
        `POST /ask` is stateless — each question stands alone. A chat
        session is where a multi-turn conversation lives, so an
        integration can keep a thread the way the app does.
      responses:
        "200":
          description: The caller's sessions, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items: { $ref: "#/components/schemas/ChatSessionSummary" }
                      total: { type: integer }
                      skip: { type: integer }
                      take: { type: integer }
                      hasMore: { type: boolean }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    post:
      tags: [Search]
      operationId: createChatSession
      summary: Start a conversation
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string, description: Defaults to "New Chat". }
      responses:
        "201":
          description: The new session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ChatSession" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /chat-sessions/{id}:
    get:
      tags: [Search]
      operationId: getChatSession
      summary: Read a conversation
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The session and its messages.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ChatSession" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    patch:
      tags: [Search]
      operationId: updateChatSession
      summary: Append messages or retitle a conversation
      description: |
        The message array is REPLACED, not appended to — send the whole
        thread, the way the app does after each turn.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                messages:
                  type: array
                  description: The full conversation, replacing what is stored.
                  items:
                    type: object
                    additionalProperties: true
      responses:
        "200":
          description: The updated session.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: "#/components/schemas/ChatSession" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
    delete:
      tags: [Search]
      operationId: deleteChatSession
      summary: Delete a conversation
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "204": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "404": { $ref: "#/components/responses/NotFound" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /taxonomy:
    get:
      tags: [Taxonomy]
      operationId: getTaxonomy
      summary: Get the contract taxonomy
      description: |
        Returns the contract-type classification tree the AI classifier
        assigns contracts to. Useful for mapping `contractCategory` codes
        from contract rows or `contract.classified` webhook events back to
        human-readable labels.
      responses:
        "200":
          description: The taxonomy tree.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      version:
                        type: string
                        example: "1.2"
                        description: >-
                          Bumped when the tree changes. Worth recording
                          alongside any code you store, so a later
                          reclassification is explainable.
                      categories:
                        type: array
                        items:
                          type: object
                          properties:
                            code: { type: string, example: "C03" }
                            label: { type: string, example: "Services Agreement" }
                            contractTypes:
                              type: array
                              description: >-
                                The types beneath this category. `code` is
                                what a contract is filed against - hardcode
                                the list instead and one taxonomy change
                                files everything as Other.
                              items:
                                type: object
                                properties:
                                  code: { type: string, example: "C01-01" }
                                  label: { type: string, example: "Sales Agreement (Goods or Services)" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }

  /convert/pdf-to-word:
    post:
      tags: [Conversion]
      operationId: convertPdfToWord
      summary: Convert a PDF (file) to Word
      description: |
        Convert an uploaded PDF to a Word (.docx) document — a one-off
        conversion that does **not** create a contract or version.
        Conversion produces a high-fidelity `.docx`, preserving
        layout, tables, and styles.
        Upload the bytes as `multipart/form-data` (field `file`,
        **≤ 100 MB** — the same cap as contract uploads).

        Returns a short-lived `downloadToken`; fetch the DOCX with
        `GET /contracts/download/{downloadToken}` (bearer-authenticated).
        Charged per 50-page block (5 credits per block); re-converting
        identical bytes is free (cached).

        The URL-based twin (`POST /convert/pdf-url-to-word`) is handy
        when the PDF already lives at a reachable address.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                  description: The PDF to convert (≤ 100 MB).
                filename:
                  type: string
                  maxLength: 200
                  description: Optional name for the resulting DOCX. Defaults to the upload's name with a .docx extension.
      responses:
        "200":
          description: Conversion complete; download with the token.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      downloadToken:
                        type: string
                        description: Use with GET /contracts/download/{downloadToken}.
                      expiresAt: { type: string, format: date-time }
                      pages: { type: integer }
                      ocrApplied: { type: boolean }
                      creditsCharged:
                        type: integer
                        description: 0 when served from the conversion cache.
        "400":
          description: No file, or the upload isn't a PDF.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "413":
          description: File exceeds the 100 MB limit.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

  /convert/pdf-url-to-word:
    post:
      tags: [Conversion]
      operationId: convertPdfUrlToWord
      summary: Convert a PDF (from URL) to Word
      description: |
        Convert a PDF at a public HTTPS URL to a Word (.docx) document —
        a one-off conversion that does **not** create a contract or any
        document version. Like its file-upload twin, it produces a high-fidelity
        `.docx` (layout, tables, and styles preserved). (To convert a PDF that's
        already a contract version and store the result on the contract,
        use `POST /contracts/{id}/versions/{versionId}/convert-to-word`.)

        The API downloads the PDF server-side (so the URL must be
        reachable from our infrastructure, ≤ 50 MB), converts it, and
        returns a short-lived `downloadToken`. Fetch the DOCX with
        `GET /contracts/download/{downloadToken}` (bearer-authenticated,
        same as redline downloads); the token is valid for 30 minutes.

        Charged per 50-page block: 5 credits up to 50 pages, +5 per
        additional 50. Same SHA-256 cache as the rest of the platform —
        re-converting identical bytes is free (`creditsCharged: 0`).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [pdfUrl]
              properties:
                pdfUrl:
                  type: string
                  format: uri
                  description: Public HTTPS URL of the PDF (≤ 50 MB, reachable from our servers).
                filename:
                  type: string
                  maxLength: 200
                  description: Optional name for the resulting DOCX (path components stripped). Defaults to "converted.docx".
      responses:
        "200":
          description: Conversion complete; download with the token.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      downloadToken:
                        type: string
                        description: Use with GET /contracts/download/{downloadToken}.
                      expiresAt:
                        type: string
                        format: date-time
                        description: After this the token stops working (~30 min).
                      pages: { type: integer }
                      ocrApplied:
                        type: boolean
                        description: Whether OCR was applied (image-only PDF).
                      creditsCharged:
                        type: integer
                        description: 0 when served from the conversion cache.
        "400":
          description: Bad input, fetch failed, or the URL didn't return a PDF.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "403": { $ref: "#/components/responses/Forbidden" }
        "429": { $ref: "#/components/responses/TooManyRequests" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "413":
          description: PDF exceeds the 50 MB limit.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/Error" }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: clm_sk_live_*
      description: |
        A Clment API key. Format `clm_sk_live_` followed by a
        URL-safe-base64 random string. Created in **Settings → API &
        Integrations**.

  parameters:
    ContractId:
      name: id
      in: path
      required: true
      schema: { type: string }
      description: The contract id (UUID).

  responses:
    Unauthorized:
      description: Missing, malformed, revoked, or wrong-region key.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            unauthorized:
              value: { error: { code: "UNAUTHORIZED", message: "Invalid API key" } }
    BadRequest:
      description: The request body or query failed validation.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            badRequest:
              value: { error: { code: "INVALID_INPUT", message: "url is required" } }
    NotFound:
      description: The resource doesn't exist in this organization.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            notFound:
              value: { error: { code: "NOT_FOUND", message: "Contract not found" } }
    PaymentRequired:
      description: |
        Payment-gated failure, told apart by the error `code`:
          - `INSUFFICIENT_CREDITS` — the org's credit balance can't cover
            this AI operation. Top up or upgrade, then retry.
          - `USAGE_LIMIT_EXCEEDED` — a plan stock cap was hit (e.g. the
            plan's contract limit on upload). Upgrade the plan.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            insufficientCredits:
              value: { error: { code: "INSUFFICIENT_CREDITS", message: "Not enough credits" } }
            usageLimitExceeded:
              value: { error: { code: "USAGE_LIMIT_EXCEEDED", message: "Contract limit reached (25/25)" } }
    Forbidden:
      description: |
        The request isn't allowed — one of three cases, told apart by the
        error `code`:
          - `API_ACCESS_NOT_IN_PLAN` — the API is a Business / Enterprise
            feature and this organization isn't on a qualifying plan.
          - `SUBSCRIPTION_SUSPENDED` — the org's subscription has lapsed;
            API access is paused until billing is resolved.
          - `API_KEY_FORBIDDEN` — an API key tried to reach a settings
            surface (billing, members, SSO, integrations, org config,
            webhooks); those require a human admin signed in to the app.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            notInPlan:
              value: { error: { code: "API_ACCESS_NOT_IN_PLAN", message: "The Clment API is available on the Business and Enterprise plans." } }
            suspended:
              value: { error: { code: "SUBSCRIPTION_SUSPENDED", message: "API access requires an active subscription." } }
            settingsBlocked:
              value: { error: { code: "API_KEY_FORBIDDEN", message: "API keys cannot access settings" } }
    TooManyRequests:
      description: |
        The per-organization request rate limit was exceeded. Wait the
        number of seconds in the `Retry-After` header, then retry. Every
        authenticated response (not just this one) carries
        `X-RateLimit-Limit` and `X-RateLimit-Remaining` so you can pace
        yourself before hitting it.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: { type: integer }
        X-RateLimit-Limit:
          description: Requests allowed per window.
          schema: { type: integer }
        X-RateLimit-Remaining:
          description: Requests remaining in the current window.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            rateLimited:
              value: { error: { code: "RATE_LIMITED", message: "Too many requests. Please try again in a few minutes." } }

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable machine-readable error code.
            message:
              type: string
              description: Human-readable explanation.

    ContractParty:
      type: object
      required: [name, role, address, email, phoneNumber, taxId, dba, location, registrationNumber]
      properties:
        name: { type: string }
        role:
          type: string
          description: >-
            Free-text role label, e.g. "vendor", "customer", "lessor",
            "landlord", "licensor". Defaults to "other".
        address: { type: string, nullable: true }
        email: { type: string, nullable: true }
        phoneNumber: { type: string, nullable: true }
        taxId: { type: string, nullable: true }
        dba: { type: string, nullable: true }
        location: { type: string, nullable: true }
        registrationNumber: { type: string, nullable: true }

    RiskLevel:
      type: string
      description: >-
        The AI-assessed risk of a contract. `unknown` means no assessment
        has run yet, not that the contract is risk-free.
      enum: [low, medium, high, critical, unknown]

    ContractStatus:
      type: string
      description: >-
        The lifecycle state a contract is IN. Note this is not the same
        set the list filter accepts: `expiring` and `archived` are
        derived views you can filter by but never store, and `superseded`
        is a stored state you cannot filter on.
      enum: [draft, active, expired, terminated, superseded]

    JobStatus:
      type: string
      description: >-
        Lifecycle of a background job. `complete` means the job finished,
        NOT that it produced a downloadable artefact — a redline can
        complete having applied no changes.
      enum: [queued, processing, complete, failed]

    RulebookStatus:
      type: string
      description: Whether a rulebook is a working draft or available to run reviews against.
      enum: [draft, published]

    PaymentFrequency:
      type: string
      description: How often the contract's amount is payable.
      enum: [one-time, monthly, quarterly, annual, other]

    ReviewMode:
      type: string
      description: >-
        How much reasoning a review applies. `deep` costs more credits and
        takes longer.
      enum: [standard, deep]

    CommentKind:
      type: string
      description: >-
        What a reviewer comment is FOR. `redline_instruction` is not a
        note: the latest one on a finding replaces the AI's proposed
        wording in the generated document.
      enum: [comment, redline_instruction]

    HumanDecision:
      type: string
      description: A reviewer's verdict on an AI finding.
      enum: [agree, partial, disagree]

    Contract:
      type: object
      # pageCount is deliberately NOT required: GET /contracts/{id} returns
      # it, the GET /contracts list projection does not, and both use this
      # schema. Declaring it required made the generated C# client throw
      # "Required property 'pageCount' not found" on every list call.
      # `required` here has to mean "present in EVERY response using this
      # schema", not "present in the richest one".
      required: [id, contractNumber, title, summary, fileName, fileFormat, status, riskLevel, parties, effectiveDate, expirationDate, renewalDate, terminationDate, amount, currency, paymentTerms, tags, createdAt, createdBy]
      properties:
        id: { type: string }
        organizationId:
          type: string
          description: >-
            The organisation the contract belongs to. Returned on every
            contract; an integration handling more than one tenant needs
            it, and it was undocumented until the SDK harness noticed no
            generated client could see it.
        contractNumber:
          type: integer
          nullable: true
          description: The human CLM-N number. Render as "CLM-{n}".
        title: { type: string, nullable: true }
        summary: { type: string, nullable: true }
        fileName: { type: string }
        fileFormat: { type: string, example: "pdf" }
        status:
          $ref: "#/components/schemas/ContractStatus"
        riskLevel:
          allOf: [{ $ref: "#/components/schemas/RiskLevel" }]
          nullable: true
        contractCategory:
          type: string
          nullable: true
          description: Taxonomy code, e.g. "C03". Map via GET /taxonomy.
        parties:
          type: array
          items: { $ref: "#/components/schemas/ContractParty" }
      # date-time, not date: the API returns a full ISO timestamp here
        # (e.g. 2018-10-13T00:00:00.000Z) even though the value is
        # semantically a day. Declaring `date` made the generated C#
        # client type it DateOnly and throw on every contract it read,
        # which took listContracts and getContract out of .NET entirely.
        # The request bodies DO accept YYYY-MM-DD and keep `format: date`.
        effectiveDate: { type: string, format: date-time, nullable: true }
        expirationDate: { type: string, format: date-time, nullable: true }
        renewalDate: { type: string, format: date-time, nullable: true }
        terminationDate: { type: string, format: date-time, nullable: true }
        durationText:
          type: string
          nullable: true
          description: Free-text duration override; falls back to the span derived from the dates.
        amount:
          type: number
          nullable: true
          description: The per-period (or one-time) figure.
        suspectedInjection:
          type: object
          nullable: true
          description: |
            Present when the document's text contains content that
            appears aimed at the AI reading it — "ignore previous
            instructions", a fake system turn, an attempt to dictate
            review outcomes.

            This describes the DOCUMENT'S AUTHOR, not whoever uploaded
            it: the usual case is a counterparty draft, and your user is
            the target. Surface it to them; it is not grounds for
            treating the upload as hostile. Absent when nothing was
            found, and on contracts ingested before the scan existed.
          properties:
            suspicion:
              type: string
              enum: [possible, likely]
              description: |
                Capped at `likely` on purpose — an AI-services contract
                can carry a system prompt as an exhibit and read exactly
                like an attack, so a human decides what this means.
            matches:
              type: array
              description: Worst first, capped at a handful for display.
              items:
                type: object
                properties:
                  rule: { type: string }
                  description: { type: string }
                  excerpt: { type: string }
            totalMatches: { type: integer }
            scannedAt: { type: string, format: date-time }
        totalValue:
          type: number
          nullable: true
          description: |
            The contract's total worth, resolved for you: the
            `totalValueOverride` when one was entered, otherwise the
            per-period `amount` multiplied out over the term plus any
            `oneTimeFees`. Null when it genuinely cannot be derived — a
            recurring amount with no usable term is indeterminate, and
            reporting the fees alone would understate it.
        totalValueOverride:
          type: number
          nullable: true
          description: |
            An entered total that replaces the calculation — for what a
            single rate cannot express, such as a mid-term rate change or
            a negotiated blended figure. Read `totalValue`; this is the
            input, not the answer.
        oneTimeFees:
          type: array
          description: Non-recurring charges (implementation, setup) added on top of the recurring base.
          items:
            type: object
            properties:
              label: { type: string }
              amount: { type: number }
        value:
          type: number
          nullable: true
          deprecated: true
          description: |
            DEPRECATED — a legacy upload-time alias of `amount`, not a
            total, and no longer written. Use `totalValue`. Reading this
            as a contract's worth reports one period's payment as the
            whole agreement.
        currency: { type: string, nullable: true, example: "USD" }
        paymentFrequency:
          allOf: [{ $ref: "#/components/schemas/PaymentFrequency" }]
          nullable: true
        paymentTerms: { type: string, nullable: true }
        tags:
          type: array
          items: { type: string }
        pageCount: { type: integer, nullable: true }
        createdAt: { type: string, format: date-time }
        createdBy: { type: string }

    ReviewSummary:
      type: object
      required: [id, contractId, playbookId, playbookName, overallRisk, reviewedAt]
      properties:
        id: { type: string }
        reviewNumber:
          type: integer
          nullable: true
          description: Human REV-N. Render as "REV-{n}".
        contractId: { type: string }
        playbookId:
          type: string
          description: Id of the rulebook the review ran against (field name retained for compatibility).
        playbookName:
          type: string
          description: Name of the rulebook the review ran against (field name retained for compatibility).
        overallRisk:
          type: string
          enum: [low, medium, high, critical]
        assignedToUserId: { type: string, nullable: true }
        reviewedAt: { type: string, format: date-time }
        findingsTotal: { type: integer }

    Review:
      allOf:
        - $ref: "#/components/schemas/ReviewSummary"
        - type: object
          required: [summary, findings]
          properties:
            summary: { type: string }
            findings:
              type: array
              items: { $ref: "#/components/schemas/Finding" }
            completedAt: { type: string, format: date-time, nullable: true }

    ReviewerComment:
      type: object
      required: [id, authorUserId, text, createdAt, updatedAt]
      description: |
        One attributed comment on a review or a finding. Independent of
        the verdict pills — a reviewer can comment without taking a
        position, or alongside one. Only the author may edit or delete
        their own.
      properties:
        id: { type: string }
        authorUserId: { type: string }
        authorUserName: { type: string, nullable: true }
        text: { type: string }
        mentionedUserIds:
          type: array
          items: { type: string }
          description: Org user ids tagged in this comment; each newly-added one is emailed.
        kind:
          allOf: [{ $ref: "#/components/schemas/CommentKind" }]
          description: |
            `redline_instruction` marks the comment as the reviewer's
            directive for the redline. The latest such comment on a
            finding overrides the AI's `suggestedLanguage`.
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    Insights:
      type: object
      description: Dashboard KPI summary for a window.
      properties:
        range: { type: string, description: 'The window requested, e.g. "30d".' }
        scope: { type: string, description: Whose contracts the numbers cover. }
        windowStart: { type: string, format: date-time }
        windowEnd: { type: string, format: date-time }
        kpis:
          type: array
          items:
            type: object
            properties:
              key:
                type: string
                description: >-
                  Stable identifier for the metric. New keys appear as the
                  dashboard grows, so treat an unrecognised one as data
                  rather than an error.
              value: { type: number }
              priorValue:
                type: number
                description: The same metric over the preceding window, for comparison.
              percentChange: { type: number, nullable: true }
              sparkline:
                type: array
                description: Per-day values across the window, oldest first.
                items:
                  type: object
                  properties:
                    date: { type: string, format: date-time }
                    value: { type: number }

    OrgSummary:
      type: object
      description: The organisation at a glance — who is in it and how much is in it.
      properties:
        organization:
          type: object
          properties:
            name: { type: string }
            legalName: { type: string, nullable: true }
            tier: { type: string, description: 'Plan tier, e.g. "business".' }
            region:
              type: string
              description: The organisation's data-residency region code.
        users:
          type: object
          properties:
            active: { type: integer }
            invitedPending: { type: integer }
            list:
              type: array
              items:
                type: object
                properties:
                  id: { type: string }
                  name: { type: string }
                  email: { type: string }
                  role: { type: string }
                  lastSeenAt: { type: string, format: date-time, nullable: true }
        counts:
          type: object
          properties:
            contracts: { type: integer }
            reviews: { type: integer }
            rulebooks: { type: integer }

    Mention:
      type: object
      description: One @-mention of a user, on a review note or a finding note.
      properties:
        reviewId: { type: string }
        contractId: { type: string }
        contractClmNumber: { type: integer, nullable: true }
        contractTitle: { type: string }
        rulebookName: { type: string }
        surface:
          type: string
          enum: [review_note, finding_note]
          description: Which surface carried the mention.
        findingId:
          type: string
          description: Present when surface is finding_note.
        findingTitle: { type: string }
        mentionedByUserId: { type: string }
        mentionedByUserName: { type: string, nullable: true }
        mentionedAt:
          type: string
          format: date-time
          description: >-
            When the note was last touched. The best available signal of
            when the mention was set — there is no per-mention audit row.
        noteText:
          type: string
          description: The note text containing the mention, truncated to 500 characters.
        reviewSessionStatus:
          type: string
          description: >-
            Lifecycle of the parent review — active, complete, signed_off.
            Useful for telling a live action item from a stale one.
        url: { type: string }

    UsageSummary:
      # Named UsageSummary rather than Usage on purpose: a component called
      # Usage that carries a `usage` property makes the C# generator escape
      # the collision to `VarUsage`, and the Python one to `var_usage`.
      # A name that does not collide is free; the escape hatch is not.
      type: object
      description: Plan, allowances and what has been consumed this period.
      properties:
        plan:
          type: object
          properties:
            tier: { type: string }
            name: { type: string }
            price: { type: string }
        usage:
          type: object
          properties:
            credits:
              type: object
              properties:
                used: { type: integer }
                limit: { type: integer }
                planLimit:
                  type: integer
                  description: The plan's own monthly allowance, before any packs.
                packBalance:
                  type: integer
                  description: Credits remaining in purchased packs, drawn on after the plan allowance.
            contracts: { $ref: "#/components/schemas/UsageCounter" }
            users: { $ref: "#/components/schemas/UsageCounter" }
            playbooks: { $ref: "#/components/schemas/UsageCounter" }
        period: { type: string, description: 'Billing period, e.g. "2026-08".' }
        breakdown:
          type: object
          description: What consumed credits this period.
          properties:
            contractsUploaded: { type: integer }
            pagesProcessed: { type: integer }
            aiQueries: { type: integer }
            playbookReviews: { type: integer }
            redlinesGenerated: { type: integer }
            playbookGenerations: { type: integer }

    UsageCounter:
      type: object
      properties:
        used: { type: integer }
        limit: { type: integer }

    ChatSessionSummary:
      type: object
      description: >-
        A saved conversation as it appears in a list. The list projection
        omits the messages and reports a count instead — read one session
        to get its messages.
      properties:
        id: { type: string }
        organizationId: { type: string }
        userId: { type: string }
        title: { type: string }
        messageCount: { type: integer }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    ChatSession:
      type: object
      description: A saved conversation, with its messages.
      properties:
        id: { type: string }
        organizationId: { type: string }
        userId: { type: string }
        title: { type: string }
        messages:
          type: array
          items: { $ref: "#/components/schemas/ChatSessionMessage" }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    ChatSessionMessage:
      type: object
      description: >-
        One turn in a saved conversation. First-party app clients attach
        further fields for in-app rendering; per the versioning policy,
        ignore any field you do not recognise.
      properties:
        id: { type: string }
        role: { type: string, enum: [user, assistant] }
        content: { type: string }
        timestamp: { type: string, format: date-time }
        mode:
          type: string
          enum: [full-context, rag]
          description: How the answer was produced.
        citations:
          type: array
          description: The contracts surfaced alongside the answer.
          items:
            type: object
            properties:
              contractId: { type: string }
              contractNumber: { type: integer, nullable: true }
              contractName: { type: string }
              excerpt: { type: string }
              page: { type: integer }
        discussedContracts:
          type: array
          description: >-
            Every contract the reply considered — usually wider than
            `citations`, which is curated for display.
          items:
            type: object
            properties:
              contractId: { type: string }
              contractNumber: { type: integer, nullable: true }
              contractName: { type: string }

    UploadResult:
      # ONE schema for both success responses of uploadContract, and the
      # reason is not tidiness. openapi-generator emits a single return
      # type per operation for C# and Python; given two differently
      # shaped success responses it models the first and drops the other.
      # Two separate schemas here meant a .NET or Python caller uploading
      # a NEW contract got a 202 carrying the jobId, deserialised it into
      # a model with no jobId field, and received an object with every
      # field null and no error at all — the id needed to poll was
      # silently discarded.
      #
      # So: every field optional, and WHICH ones are populated tells you
      # which of the two outcomes you got. Check jobId first.
      type: object
      description: >-
        The result of an upload. Two outcomes share this shape: a NEW
        contract returns 202 with `jobId` to poll, while a file matched to
        an existing contract returns 200 with `attached: true` and the
        contract it was filed under. Test `jobId` to tell them apart.
      properties:
        data:
          type: object
          properties:
            jobId:
              type: string
              description: >-
                Present on 202. Poll `GET /contracts/upload/jobs/{jobId}` until
                it reports complete, then read the contract it names.
            matchedContractId:
              type: string
              nullable: true
              description: Non-null when the file matched an existing contract on the `new` path.
            matchedContractTitle:
              type: string
              nullable: true
            attached:
              type: boolean
              description: Present and true on 200 — the file became a new version of an existing contract.
              example: true
            contractId:
              type: string
              description: On 200, the contract the file was filed under.
            contractTitle: { type: string }
            version:
              type: object
              description: On 200, the version the file became.
              properties:
                id: { type: string }
                versionNumber: { type: integer }
                label: { type: string }
                fileName: { type: string }
                fileSize: { type: integer }
            revisionSummary:
              type: string
              nullable: true
              description: Present when summariseChanges was set.

    CustomDate:
      type: object
      description: |
        A date a person added to a contract, as opposed to the canonical
        dates the AI extracted onto the contract record.
      required: [id, contractId, label, date]
      properties:
        id: { type: string }
        contractId: { type: string }
        label: { type: string }
      # date-time, not date: the API returns a full ISO timestamp here
        # (e.g. 2018-10-13T00:00:00.000Z) even though the value is
        # semantically a day. Declaring `date` made the generated C#
        # client type it DateOnly and throw on every contract it read,
        # which took listContracts and getContract out of .NET entirely.
        # The request bodies DO accept YYYY-MM-DD and keep `format: date`.
        date: { type: string, format: date-time }
        notes: { type: string, nullable: true }
        createdBy: { type: string }
        createdAt: { type: string, format: date-time }

    Finding:
      type: object
      required: [id, title, status, severity, note]
      properties:
        id: { type: string }
        title: { type: string }
        status:
          type: string
          enum: [pass, fail, warning, not_applicable]
          description: |
            The verdict the AI reached against the rulebook item. `pass`
            findings are reported too — a review is an account of what was
            checked, not only of what failed.
        severity:
          type: string
          enum: [critical, high, medium, low]
        note:
          type: string
          description: The AI's explanation of the finding.
        clause:
          type: string
          description: The contract clause the finding refers to, when identified.
        quotedText:
          type: string
          description: The passage from the contract the finding is about.
        suggestedLanguage:
          type: string
          description: Replacement wording the AI proposes, when it proposes any.
        actionSummary:
          type: string
          description: One-line statement of the ask, used as the redline comment.
        confidence:
          type: string
          enum: [high, medium, low]
        sourceStatus:
          type: string
          enum: [contract_text, playbook_rule, inferred, needs_verification]
          description: Where the finding came from — quoted text, a rule, or inference.
        humanDecision:
          allOf: [{ $ref: "#/components/schemas/HumanDecision" }]
          nullable: true
          description: The reviewer's verdict, or null if unreviewed.
        includeInRedline:
          type: boolean
          description: Whether this finding is flagged for the redline.

    Rulebook:
      type: object
      required: [id, name, description, instructions, status, version, tags, createdAt, updatedAt]
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        instructions: { type: string }
        status:
          $ref: "#/components/schemas/RulebookStatus"
        version: { type: integer }
        tags:
          type: array
          items: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    DocumentVersion:
      type: object
      required: [id, contractId, versionNumber, label, versionType, fileName, fileFormat, fileSize, createdAt, createdBy]
      description: |
        A file attached to a contract — the primary document, one of
        its revisions, or a supplementary attachment.
      properties:
        id:
          type: string
          description: Use this as `versionId` / in the attachment-id arrays on a review.
        contractId: { type: string }
        role:
          type: string
          description: '`primary` for the main document; any other value (e.g. amendment, schedule, exhibit, side-letter) marks an attachment.'
          example: primary
        attachmentGroupId:
          type: string
          description: Groups a document and its revisions together.
        versionNumber: { type: integer }
        label:
          type: string
          description: Human label, e.g. "Original" or "Redline v3".
        versionType:
          type: string
          description: e.g. original, revision, redline, converted.
        fileName: { type: string }
        fileFormat: { type: string, example: pdf }
        fileSize: { type: integer, nullable: true }
        createdAt: { type: string, format: date-time }
        createdBy: { type: string }

    KeyDate:
      type: object
      required: [id, contractId, contractName, dateType, date, isCustom]
      properties:
        id:
          type: string
          description: >-
            Identifies the date, and is the `{id}` that
            `POST /dates/{id}/set-reminder` takes. For an AI-extracted
            date it is composed from the contract and the date type
            (`{contractId}_effectiveDate`); for a user-added one it is
            the custom date's own id. Undocumented until an SDK harness
            test tried to set a reminder and found no id to send.
        contractId: { type: string }
        contractName: { type: string }
        contractNumber:
          type: integer
          nullable: true
          description: The CLM number a person would recognise the contract by.
        contractTypeCode: { type: string, nullable: true }
        contractTypeLabel: { type: string, nullable: true }
        contractCategoryCode: { type: string, nullable: true }
        contractCategoryLabel: { type: string, nullable: true }
        displayDescription:
          type: string
          nullable: true
          description: A ready-made label for the date, contract name included.
        dateType:
          type: string
          example: "renewal"
        # date-time, not date: the API returns a full ISO timestamp here
        # (e.g. 2018-10-13T00:00:00.000Z) even though the value is
        # semantically a day. Declaring `date` made the generated C#
        # client type it DateOnly and throw on every contract it read,
        # which took listContracts and getContract out of .NET entirely.
        # The request bodies DO accept YYYY-MM-DD and keep `format: date`.
        date: { type: string, format: date-time }
        dateString:
          type: string
          nullable: true
          description: The date as it reads in the document, e.g. "September 1, 2015".
        isApproximate:
          type: boolean
          description: True when the date was inferred rather than stated outright.
        isCustom:
          type: boolean
          description: True for user-added dates, false for AI-extracted canonical dates.
        customDateId:
          type: string
          nullable: true
          description: Present when isCustom — the underlying custom date row.
        label:
          type: string
          nullable: true
          description: The user's own label, on a custom date.
        notes: { type: string, nullable: true }
        context:
          type: string
          nullable: true
          description: The surrounding text the date was extracted from.
        pageNumber: { type: integer, nullable: true }
        reminderSet:
          type: boolean
          description: Whether a reminder is currently enabled for this date.
        reminderDaysBefore:
          type: integer
          nullable: true
          description: How many days ahead the reminder fires. Read it before changing it, so you can put it back.
        reminderSentAt:
          type: string
          format: date-time
          nullable: true
