openapi: 3.0.3
info:
  title: Clment REST API
  version: "1.0.0"
  description: |
    The Clment REST API gives your services programmatic access to the
    same contract workspace you use in the app — contracts, AI reviews,
    playbooks, 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 playbooks, 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://us.clment.com/v1`     |
    | Europe        | `https://eu.clment.com/v1`     |
    | Australia     | `https://au.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}` |

    The review and redline start endpoints also support live progress
    over Server-Sent Events if you send `Accept: text/event-stream`.
    For server-to-server integrations, omit that header and poll the
    job endpoint instead — simpler and more robust than holding a
    stream open.

    ## Rate limits & credits

    AI operations (reviews, redlines, conversions) consume credits from
    your plan's monthly allowance, then any credit packs. Read
    operations are free. A `402` indicates an exhausted balance.

servers:
  - url: https://us.clment.com/v1
    description: United States
  - url: https://eu.clment.com/v1
    description: Europe
  - url: https://au.clment.com/v1
    description: Australia

security:
  - bearerAuth: []

tags:
  - name: Contracts
    description: Upload, read, update, and delete contracts.
  - name: Reviews
    description: AI review artifacts — findings, verdicts, risk.
  - name: Playbooks
    description: The rulebooks AI reviews evaluate contracts against.
  - name: Key dates
    description: Renewal, expiry, and custom dates extracted or added.
  - name: Search
    description: Natural-language and filtered search across contracts.
  - 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 powered by Adobe PDF Services for high-fidelity output — layout, tables, and styles are preserved.

paths:
  /contracts:
    get:
      tags: [Contracts]
      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:
            type: string
            enum: [low, medium, high]
          description: Filter by the AI-assessed risk level.
        - 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" }

  /contracts/upload:
    post:
      tags: [Contracts]
      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).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      attached: { type: boolean, example: true }
                      contractId: { type: string }
                      contractTitle: { type: string }
                      version:
                        type: object
                        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.
        "202":
          description: |
            New-contract upload accepted; processing asynchronously.
            `matchedContractId` is non-null when the file matched an
            existing contract (and you were on the `new` path).
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      jobId: { type: string }
                      matchedContractId:
                        type: string
                        nullable: true
                      matchedContractTitle:
                        type: string
                        nullable: true
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "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]
      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
                    properties:
                      jobId: { type: string }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }

  /contracts/{id}:
    get:
      tags: [Contracts]
      summary: Get a contract
      parameters:
        - $ref: "#/components/parameters/ContractId"
      responses:
        "200":
          description: The contract.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      contract: { $ref: "#/components/schemas/Contract" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    patch:
      tags: [Contracts]
      summary: Update a contract
      description: |
        Patch editable fields. Omitted fields are left unchanged. AI-
        extracted fields (dates, parties, value) can be overridden here
        when you have a better source of truth.
      parameters:
        - $ref: "#/components/parameters/ContractId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                title: { type: string }
                summary: { type: string }
                status:
                  type: string
                  enum: [draft, active, expiring, expired, terminated, archived]
                tags:
                  type: array
                  items: { type: string }
                riskLevel:
                  type: string
                  enum: [low, medium, high]
      responses:
        "200":
          description: The updated contract.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      contract: { $ref: "#/components/schemas/Contract" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Contracts]
      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" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/upload/jobs/{jobId}:
    get:
      tags: [Contracts]
      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:
                        type: string
                        enum: [queued, processing, complete, error]
                      contractId:
                        type: string
                        description: Present once status is `complete`.
                      error:
                        type: string
                        description: Present when status is `error`.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/{id}/versions:
    get:
      tags: [Contracts]
      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" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/{id}/versions/{versionId}/download:
    get:
      tags: [Contracts]
      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`.
      parameters:
        - $ref: "#/components/parameters/ContractId"
        - name: versionId
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The file bytes (PDF or DOCX).
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/{id}/review:
    post:
      tags: [Reviews]
      summary: Start an AI review
      description: |
        Run a playbook 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** `playbookId` **or** `customInstructions`
          (you may provide both — the instructions layer on top of the
          playbook).
        content:
          application/json:
            schema:
              type: object
              properties:
                playbookId:
                  type: string
                  description: |
                    Id of the playbook to evaluate against — get one from
                    `GET /playbooks`. (The in-app client may instead send
                    the full playbook object as `playbook`; for the API,
                    use `playbookId`.)
                customInstructions:
                  type: string
                  maxLength: 2000
                  description: |
                    Free-text review instructions, applied on top of (or
                    instead of) the playbook. Use this with no `playbookId`
                    for an ad-hoc review.
                mode:
                  type: string
                  enum: [standard, deep]
                  default: standard
                  description: '`deep` runs extended reasoning at a higher credit cost.'
                reviewStrategy:
                  type: string
                  enum: [explore, negotiation, high_priority, strict]
                  description: |
                    How assertive the review is and how much lands in the
                    redline by default. `explore` surfaces everything and
                    redlines nothing; `negotiation` flags what's worth raising
                    (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 playbook (all severities). It changes
                    the review's posture, not what is checked. Defaults to the
                    playbook's default strategy, or `explore`.
                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`.
                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 — playbook + mode are inherited from it.
      responses:
        "200":
          description: Review job accepted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      jobId: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/{id}/generate-redline:
    post:
      tags: [Reviews]
      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 playbook the findings came from).
                    This is display text only — unlike `start_review`'s
                    `playbookId`, it does NOT resolve a playbook. Any
                    string is fine; the playbook's name is the natural
                    choice.
                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 primary; 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".
      responses:
        "200":
          description: Redline job accepted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      jobId: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "402": { $ref: "#/components/responses/PaymentRequired" }
        "404": { $ref: "#/components/responses/NotFound" }

  /contracts/download/{token}:
    get:
      tags: [Reviews]
      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" }
        "404": { $ref: "#/components/responses/NotFound" }

  /reviews:
    get:
      tags: [Reviews]
      summary: List reviews
      description: |
        Returns AI reviews across the organization, newest first. Filter
        by `status`, `contractId`, or `assignedToUserId`.
      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: limit
          in: query
          schema: { type: integer, default: 100, 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" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /reviews/{id}:
    get:
      tags: [Reviews]
      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" }
        "404": { $ref: "#/components/responses/NotFound" }

  /reviews/{id}/report.docx:
    get:
      tags: [Reviews]
      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" }
        "404": { $ref: "#/components/responses/NotFound" }

  /reviews/{id}/findings/{findingId}:
    patch:
      tags: [Reviews]
      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:
                  type: string
                  nullable: true
                  enum: [agree, partial, disagree]
                  description: Your verdict on the AI's recommendation. `null` clears it.
                humanNote:
                  type: string
                  nullable: true
                  description: Free-text reviewer note (≤ 5000 chars). May contain @mentions.
                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" }
        "404": { $ref: "#/components/responses/NotFound" }

  /reviews/{id}/complete:
    post:
      tags: [Reviews]
      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" }
        "404": { $ref: "#/components/responses/NotFound" }

  /jobs/review/{id}:
    get:
      tags: [Jobs]
      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:
                        type: string
                        enum: [queued, processing, complete, error]
                      reviewId:
                        type: string
                        description: Present once `status` is `complete`.
                      error:
                        type: string
                        description: Present when `status` is `error`.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /jobs/redline/{id}:
    get:
      tags: [Jobs]
      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:
                        type: string
                        enum: [queued, processing, complete, error]
                      versionId:
                        type: string
                        description: The saved redline version, once complete.
                      appliedCount:
                        type: integer
                        description: Number of findings applied as tracked changes.
                      error:
                        type: string
                        description: Present when `status` is `error`.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /playbooks:
    get:
      tags: [Playbooks]
      summary: List playbooks
      parameters:
        - name: status
          in: query
          schema: { type: string, enum: [draft, published] }
      responses:
        "200":
          description: Playbooks for the organization.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      playbooks:
                        type: array
                        items: { $ref: "#/components/schemas/Playbook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
    post:
      tags: [Playbooks]
      summary: Create a playbook
      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:
                  type: string
                  enum: [draft, published]
                  default: draft
                tags:
                  type: array
                  items: { type: string }
      responses:
        "201":
          description: The created playbook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      playbook: { $ref: "#/components/schemas/Playbook" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /playbooks/{id}:
    get:
      tags: [Playbooks]
      summary: Get a playbook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "200":
          description: The playbook.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      playbook: { $ref: "#/components/schemas/Playbook" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }
    delete:
      tags: [Playbooks]
      summary: Delete a playbook
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
      responses:
        "204": { description: Deleted. }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "404": { $ref: "#/components/responses/NotFound" }

  /dates:
    get:
      tags: [Key dates]
      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" }

  /search:
    get:
      tags: [Search]
      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
                        items:
                          type: object
                          properties:
                            contractId: { type: string }
                            title: { type: string }
                            score: { type: number }
                            snippet: { type: string }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /taxonomy:
    get:
      tags: [Taxonomy]
      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:
                      categories:
                        type: array
                        items:
                          type: object
                          properties:
                            code: { type: string, example: "C03" }
                            label: { type: string, example: "Services Agreement" }
        "401": { $ref: "#/components/responses/Unauthorized" }

  /convert/pdf-to-word:
    post:
      tags: [Conversion]
      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 is powered by **Adobe PDF Services**, preserving
        layout, tables, and styles for a high-fidelity `.docx`.
        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" }
        "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]
      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, conversion is
        powered by **Adobe PDF Services** for 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" }
        "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: The org's credit balance is exhausted for this AI operation.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            paymentRequired:
              value: { error: { code: "INSUFFICIENT_CREDITS", message: "Not enough credits" } }
    Forbidden:
      description: |
        The key tried to reach a settings endpoint. API keys can't
        touch billing, members, integrations, org config, or webhooks.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
          examples:
            forbidden:
              value: { error: { code: "API_KEY_FORBIDDEN", message: "API keys cannot access settings" } }

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

    Contract:
      type: object
      properties:
        id: { type: string }
        contractNumber:
          type: integer
          nullable: true
          description: The human CLM-N number. Render as "CLM-{n}".
        title: { type: string }
        summary: { type: string, nullable: true }
        fileName: { type: string }
        fileFormat: { type: string, example: "pdf" }
        status:
          type: string
          enum: [draft, active, expiring, expired, terminated, archived]
        riskLevel:
          type: string
          enum: [low, medium, high]
          nullable: true
        contractCategory:
          type: string
          nullable: true
          description: Taxonomy code, e.g. "C03". Map via GET /taxonomy.
        parties:
          type: array
          items: { type: string }
        effectiveDate: { type: string, format: date, nullable: true }
        expirationDate: { type: string, format: date, nullable: true }
        renewalDate: { type: string, format: date, nullable: true }
        value: { type: number, nullable: true }
        currency: { type: string, nullable: true, example: "USD" }
        tags:
          type: array
          items: { type: string }
        pageCount: { type: integer, nullable: true }
        createdAt: { type: string, format: date-time }
        createdBy: { type: string }

    ReviewSummary:
      type: object
      properties:
        id: { type: string }
        reviewNumber:
          type: integer
          nullable: true
          description: Human REV-N. Render as "REV-{n}".
        contractId: { type: string }
        playbookId: { type: string }
        playbookName: { type: string }
        overallRisk:
          type: string
          enum: [low, medium, high]
        assignedToUserId: { type: string, nullable: true }
        reviewedAt: { type: string, format: date-time }
        findingsTotal: { type: integer }

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

    Finding:
      type: object
      properties:
        id: { type: string }
        title: { type: string }
        severity:
          type: string
          enum: [critical, high, medium, low]
        recommendation:
          type: string
          description: The AI's recommended action.
        humanDecision:
          type: string
          nullable: true
          enum: [agree, partial, disagree]
          description: The reviewer's verdict, or null if unreviewed.
        includeInRedline:
          type: boolean
          description: Whether this finding is flagged for the redline.

    Playbook:
      type: object
      properties:
        id: { type: string }
        name: { type: string }
        description: { type: string, nullable: true }
        instructions: { type: string }
        status:
          type: string
          enum: [draft, published]
        version: { type: integer }
        tags:
          type: array
          items: { type: string }
        createdAt: { type: string, format: date-time }
        updatedAt: { type: string, format: date-time }

    DocumentVersion:
      type: object
      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
      properties:
        contractId: { type: string }
        contractTitle: { type: string }
        dateType:
          type: string
          example: "renewal"
        date: { type: string, format: date }
        isCustom:
          type: boolean
          description: True for user-added dates, false for AI-extracted canonical dates.
