openapi: "3.1.0"

info:
  title: "Document Operations API"
  version: "0.1.0"
  description: |
    The Document Operations API is the APEX Suite document toolbox: conversion, manipulation,
    extraction, and compliance operations over PDF and Office documents, exposed as plain REST
    endpoints. Upload a document, get a processed document (or a structured report) back.

    Operation families: conversion (Office/HTML/Markdown to PDF, OCR), page organisation
    (merge, split, extract), optimisation (compress), stamping (watermarks, page numbers,
    QR codes), metadata read/write, security (encryption, permissions, sanitisation,
    irreversible redaction), cryptographic provenance, enrichment (links, annotations),
    archival compliance (PDF/A, accessibility tagging), analysis (version diff, semantic data
    extraction), text editing, and a natural-language document agent.

    ## How requests work

    Every operation is a `POST` accepting a `multipart/form-data` upload — the document file
    plus operation parameters as ordinary form fields. There are two execution modes:

    **Synchronous operations** (the majority) return the result directly in the response body:
    the output document (`application/pdf`, or `application/zip` for split) or a JSON report
    (metadata read, provenance verify, diff, text blocks, redaction suggestions).

    **Asynchronous operations** — `convert`, `ocr`, `extract-data`, `tag`, and `agent` — are
    heavier workloads. They return immediately with a job reference:

    ```json
    { "ok": true, "data": { "job_id": "…", "status": "pending" }, "error": null, "meta": { … } }
    ```

    Poll `GET /v1/jobs/{job_id}` until the job's `status` is `completed` (or `failed`), then
    retrieve the output via `GET /v1/jobs/{job_id}/download`. Processing failures surface as
    `status: "failed"` with a human-readable `error` on the job, not as a 5xx on the submission.

    ## Authentication

    Every request (except `GET /health`) carries a Bearer token in the `Authorization` header:
    either a machine-to-machine access token issued by the platform identity provider, or a Forge-issued tenant API key.

    ```
    Authorization: Bearer <token>
    ```

    ## Tenancy

    Requests are tenant-scoped via the required `X-Tenant-ID` header. A tenant that has not
    been provisioned for Document Operations is refused with `403 TENANT_NOT_CONFIGURED`.
    Per-tenant limits apply — most notably a maximum upload size; oversized requests are
    refused with `413 FILE_TOO_LARGE` before the body is processed.

    ## Response envelope

    JSON control responses use the standard APEX envelope:

    ```json
    { "ok": true, "data": { … }, "error": null, "meta": { "request_id": "…", "timestamp": "…", "version": "…" } }
    ```

    On failure `ok` is `false` and `error` carries `{ code, message, detail }`. Analysis
    reports (diff, text blocks, redaction suggestions) return the report object directly.
    Guard-rail errors (tenant gate `403`, upload cap `413`, validation `422`, unexpected `500`)
    use the envelope; operation-level errors (`400`, `502`, and job `404`/`409`/`410`) return a
    compact `{ "detail": "…" }` body. Quote `meta.request_id` when raising a support query —
    supplying an `X-Correlation-ID` request header threads your own identifier through the
    platform's usage records and is echoed back on the response.

servers:
  - url: http://localhost:9190
    description: >-
      Local development. Cloud base URLs are issued per tenant by your platform operator —
      request yours through your platform contact.

security:
  - bearerAuth: []

tags:
  - name: Conversion
    description: Convert Office, HTML, and Markdown documents to PDF, and add searchable text layers via OCR. Asynchronous.
  - name: Organise
    description: Merge, split, and extract pages.
  - name: Optimise
    description: Reduce file size.
  - name: Stamping
    description: Watermarks, page numbers, and QR codes.
  - name: Metadata
    description: Read and write document metadata.
  - name: Security
    description: Encryption, permission restrictions, sanitisation, and irreversible redaction.
  - name: Provenance
    description: Cryptographic provenance chains — stamp and verify document custody.
  - name: Enrichment
    description: Hyperlinks and annotations.
  - name: Archival
    description: PDF/A conversion and accessibility tagging.
  - name: Analysis
    description: Version comparison and semantic data extraction.
  - name: Editing
    description: Extract editable text blocks and apply style-preserving text edits.
  - name: Agent
    description: Natural-language document agent — describe the outcome, the agent plans and runs the operations.
  - name: Jobs
    description: Poll asynchronous jobs and download their results.
  - name: Service
    description: Service liveness and the machine-readable operation catalog.

paths:

  # ---------------------------------------------------------------- Conversion

  /v1/convert:
    post:
      operationId: convertToPdf
      tags: [Conversion]
      summary: Convert a document or web page to PDF
      x-apex-availability: available
      description: |
        Convert an uploaded Office document (Word, Excel, PowerPoint, OpenDocument), HTML file,
        or Markdown file to PDF. This is an asynchronous operation: the response carries a
        `job_id` — poll `GET /v1/jobs/{job_id}` until completion, then download the PDF via
        `GET /v1/jobs/{job_id}/download`.

        **URL mode.** Instead of uploading a file, pass a `url` form field to render a web page
        as PDF. URL mode is disabled by default and must be enabled for your deployment; URLs
        resolving to private or internal network addresses are always refused.

        Example (PowerShell):

        ```powershell
        curl.exe -X POST "http://localhost:9190/v1/convert" `
          -H "Authorization: Bearer $token" `
          -H "X-Tenant-ID: acme" `
          -F "file=@quarterly-report.docx"
        ```
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/ConvertRequest"
            examples:
              fileConversion:
                summary: Convert a Word document to PDF
                value:
                  file: "(binary — quarterly-report.docx)"
                  zoom: 1.0
      responses:
        "202":
          description: File conversion job accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobSubmittedEnvelope"
        "200":
          description: URL-mode conversion job accepted (same body as `202`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobSubmittedEnvelope"
        "400":
          description: >-
            Blocked or invalid parameters — neither `file` nor `url` supplied, or the URL
            resolves to a blocked (private/loopback/link-local) address.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: >-
            URL-mode conversion is disabled for this tenant or deployment, or the tenant is
            not configured for Document Operations.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/BasicError"
                  - $ref: "#/components/schemas/ErrorEnvelope"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          description: >-
            Upstream conversion engine failure. Failures during background processing surface
            as `status: "failed"` on the job rather than on this response.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"

  /v1/ocr:
    post:
      operationId: ocrPdf
      tags: [Conversion]
      summary: Add a searchable text layer to a PDF (OCR)
      x-apex-availability: available
      description: |
        Run optical character recognition over a scanned or image-only PDF and embed a
        searchable text layer. Asynchronous: poll `GET /v1/jobs/{job_id}` and download the
        result when completed. OCR failures surface as `status: "failed"` on the job.

        Modes: `auto` skips pages that already contain text (default), `force` re-recognises
        every page, `strict` aborts if any page already contains text.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/OcrRequest"
      responses:
        "202":
          description: OCR job accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobSubmittedEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"

  # ------------------------------------------------------------------ Organise

  /v1/merge:
    post:
      operationId: mergePdfs
      tags: [Organise]
      summary: Merge PDFs into a single document
      x-apex-availability: available
      description: |
        Merge two or more PDF files into one, preserving upload order. Optionally strip
        digital signatures and generate a table of contents. The output filename is
        `merged_<first input name>.pdf`.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/MergeRequest"
      responses:
        "200":
          description: The merged PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          description: Fewer than two files supplied.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/split:
    post:
      operationId: splitPdf
      tags: [Organise]
      summary: Split a PDF into multiple documents
      x-apex-availability: available
      description: |
        Split a PDF either at explicit page boundaries (`method=page_numbers` with `pages`
        interpreted as split-after positions, e.g. `"2,4"`) or into fixed-size chunks
        (`method=every_n`). Returns a ZIP archive containing `<name>_1.pdf`, `<name>_2.pdf`, and
        so on.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/SplitRequest"
      responses:
        "200":
          description: ZIP archive of the split parts.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/zip:
              schema:
                type: string
                contentMediaType: application/zip
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/extract:
    post:
      operationId: extractPages
      tags: [Organise]
      summary: Extract pages into a new PDF
      x-apex-availability: available
      description: |
        Extract the selected pages into a new PDF. The `pages` selection uses the page
        expression syntax, e.g. `"1,3,5-8"` or `"odd & 1-10"`. The output filename is
        `<name>_extracted.pdf`.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/ExtractPagesRequest"
      responses:
        "200":
          description: PDF containing the extracted pages.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          description: Invalid page expression or out-of-range pages.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  # ------------------------------------------------------------------ Optimise

  /v1/compress:
    post:
      operationId: compressPdf
      tags: [Optimise]
      summary: Compress a PDF
      x-apex-availability: available
      description: |
        Reduce a PDF's file size. `method=quality` applies a fixed quality level from 1
        (maximum compression) to 9 (minimum compression). `method=target_size` searches for the
        highest quality that fits within `target_size_mb`. Optionally linearise the output for
        fast web viewing. The output keeps the original filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/CompressRequest"
      responses:
        "200":
          description: The compressed PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  # ------------------------------------------------------------------ Stamping

  /v1/watermark:
    post:
      operationId: addWatermark
      tags: [Stamping]
      summary: Add a text or image watermark
      x-apex-availability: available
      description: |
        Stamp a text or image watermark onto every page, or onto a page selection. Colour,
        font size, rotation, and opacity are configurable. The output keeps the original
        filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/WatermarkRequest"
      responses:
        "200":
          description: The watermarked PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/page-numbers:
    post:
      operationId: addPageNumbers
      tags: [Stamping]
      summary: Stamp page numbers
      x-apex-availability: available
      description: |
        Stamp page numbers onto a PDF. `position` is a 3x3 grid code (`tl`, `tc`, `tr`, `ml`,
        `mc`, `mr`, `bl`, `bc`, `br`). `format_string` templates the stamped text: `{n}` is the
        page number, `{total}` the page count, `{filename}` the source filename. The output
        keeps the original filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/PageNumbersRequest"
      responses:
        "200":
          description: The numbered PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/qr:
    post:
      operationId: stampQr
      tags: [Stamping]
      summary: Stamp a QR code onto a PDF
      x-apex-availability: available
      description: |
        Generate a QR code from `payload` (a URL, verification link, or tracking identifier)
        and stamp it at the requested grid position, sized as a fraction of the page width.
        The output keeps the original filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/QrRequest"
      responses:
        "200":
          description: The stamped PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  # ------------------------------------------------------------------ Metadata

  /v1/metadata/read:
    post:
      operationId: readMetadata
      tags: [Metadata]
      summary: Read PDF metadata
      x-apex-availability: available
      description: |
        Read a PDF's metadata (title, author, subject, keywords, creator, producer, dates,
        trapped flag, and custom fields) and return it as JSON. Uses `POST` with a file upload.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/MetadataReadRequest"
      responses:
        "200":
          description: Metadata envelope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MetadataReadEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/metadata:
    post:
      operationId: writeMetadata
      tags: [Metadata]
      summary: Write PDF metadata
      x-apex-availability: available
      description: |
        Set standard metadata fields and custom key-value pairs on a PDF, optionally wiping
        existing metadata first (`delete_all=true`). Dates are ISO 8601 strings. The output
        keeps the original filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/MetadataWriteRequest"
      responses:
        "200":
          description: The updated PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  # ------------------------------------------------------------------ Security

  /v1/encrypt:
    post:
      operationId: encryptPdf
      tags: [Security]
      summary: Encrypt a PDF
      x-apex-availability: available
      description: |
        Encrypt a PDF with AES-256 (default) or AES-128. `owner_pw` (required) controls
        permission changes; `user_pw` (optional) is prompted for on open. Legacy weak ciphers
        are never offered. The output keeps the original filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/EncryptRequest"
      responses:
        "200":
          description: The encrypted PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/permissions:
    post:
      operationId: setPermissions
      tags: [Security]
      summary: Set PDF permission restrictions
      x-apex-availability: available
      description: |
        Apply the eight standard PDF permission restrictions (printing, modification, content
        extraction, form filling, and so on) using AES-256 re-encryption. Flags are phrased as
        "prevent X" — a flag left `false` leaves that action allowed. An `owner_pw` is required
        because restrictions without an owner password are advisory only. The output keeps the
        original filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/PermissionsRequest"
      responses:
        "200":
          description: The restricted PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/sanitize:
    post:
      operationId: sanitizePdf
      tags: [Security]
      summary: Sanitise a PDF
      x-apex-availability: available
      description: |
        Remove active content and optional metadata from a PDF: embedded JavaScript, embedded
        files, XMP metadata, document metadata, and link annotations. At least one flag must be
        `true`. `remove_fonts` is reserved and currently has no effect. The output keeps the
        original filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/SanitizeRequest"
      responses:
        "200":
          description: The sanitised PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          description: No sanitisation flags set, or invalid parameters.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/redact:
    post:
      operationId: redactPdf
      tags: [Security]
      summary: Irreversibly redact content
      x-apex-availability: available
      description: |
        True redaction: paint black boxes over explicit regions (`boxes`) and/or text matching
        regular expressions (`patterns`), then rasterise the document so redacted content is
        unrecoverable. At least one of `boxes` or `patterns` must be provided.

        The output has no text layer — run `/v1/ocr` afterwards if you need searchable text.
        Response headers `X-Redact-Regions`, `X-Redact-Matches`, and `X-Redact-Rasterized`
        report what was applied.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/RedactRequest"
      responses:
        "200":
          description: The redacted, rasterised PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
            X-Redact-Regions:
              description: Number of explicit regions redacted.
              schema: { type: string }
            X-Redact-Matches:
              description: Number of pattern matches redacted.
              schema: { type: string }
            X-Redact-Rasterized:
              description: Whether the output was rasterised.
              schema: { type: string }
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          description: Neither boxes nor patterns provided, or invalid parameters.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/redact/suggest:
    post:
      operationId: suggestRedactions
      tags: [Security]
      summary: Suggest redaction targets
      x-apex-availability: available
      description: |
        Scan a PDF for likely redaction targets. The deterministic catalog detects email
        addresses, South African identity numbers, and phone numbers; set `ai=true` to add
        AI-suggested entities. When the AI assist layer is disabled or unavailable, the
        deterministic suggestions still return with `ai_assist.status` set accordingly.
        Returns the report directly (not wrapped in the envelope).
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/RedactSuggestRequest"
      responses:
        "200":
          description: Suggested redaction targets.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RedactSuggestions"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  # ---------------------------------------------------------------- Provenance

  /v1/provenance/stamp:
    post:
      operationId: stampProvenance
      tags: [Provenance]
      summary: Append a signed provenance entry
      x-apex-availability: available
      description: |
        Append a cryptographically signed entry to the document's provenance chain — recording
        the operation label, actor, and an optional note — and return the stamped PDF.
        `actor` defaults to the authenticated identity. Response headers `X-Provenance-Seq`
        and `X-Provenance-Entries` report the new entry's sequence number and the chain length.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/ProvenanceStampRequest"
      responses:
        "200":
          description: The stamped PDF with the chain entry appended.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
            X-Provenance-Seq:
              description: Sequence number of the appended entry.
              schema: { type: string }
            X-Provenance-Entries:
              description: Total entries in the chain.
              schema: { type: string }
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/provenance/verify:
    post:
      operationId: verifyProvenance
      tags: [Provenance]
      summary: Verify a provenance chain
      x-apex-availability: available
      description: |
        Replay a document's provenance chain and report validity: whether a chain is present,
        its entries, whether the signatures verify, and whether the document content is intact.
        Optionally pin verification to a specific Ed25519 public key (base64).
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/ProvenanceVerifyRequest"
      responses:
        "200":
          description: Verification report envelope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProvenanceVerifyEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  # ---------------------------------------------------------------- Enrichment

  /v1/links:
    post:
      operationId: addLinks
      tags: [Enrichment]
      summary: Add hyperlinks to a PDF
      x-apex-availability: available
      description: |
        Add clickable link annotations: explicit links (a JSON array of page/rectangle/target
        entries, where the target is a URL, `mailto:` address, or `page:N` internal jump)
        and/or automatic detection of URLs and email addresses in the text layer
        (`autolink=true`). At least one of the two must be provided. Response headers
        `X-Links-Added` and `X-Links-Autolinked` carry the counts. The output keeps the
        original filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/LinksRequest"
      responses:
        "200":
          description: The PDF with link annotations added.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
            X-Links-Added:
              description: Number of explicit links added.
              schema: { type: string }
            X-Links-Autolinked:
              description: Number of auto-detected links added.
              schema: { type: string }
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          description: Neither explicit links nor autolink requested, or invalid parameters.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/annotate:
    post:
      operationId: annotatePdf
      tags: [Enrichment]
      summary: Add annotations to a PDF
      x-apex-availability: available
      description: |
        Add sticky notes, highlights, and free-text annotations from a JSON array of
        specifications (page, rectangle, type, text, author, colour). Rectangles are PDF
        user-space points with the origin at the bottom-left; colour components are floats
        from 0 to 1. Set `flatten=true` to burn annotations into the page content —
        note that flattening rewrites the page streams and therefore breaks any existing
        provenance content hash. Response headers `X-Annotations-Added` and
        `X-Annotations-Flattened` report the outcome.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/AnnotateRequest"
      responses:
        "200":
          description: The annotated PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
            X-Annotations-Added:
              description: Number of annotations added.
              schema: { type: string }
            X-Annotations-Flattened:
              description: Whether annotations were flattened into page content.
              schema: { type: string }
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  # ------------------------------------------------------------------ Archival

  /v1/pdfa:
    post:
      operationId: convertToPdfa
      tags: [Archival]
      summary: Convert a PDF to PDF/A
      x-apex-availability: available
      description: |
        Convert a PDF to the PDF/A archival format at conformance level `1b`, `2b`, or `3b`
        (default `2b`). The response header `X-Pdfa-Claims-Conformance` reports a
        self-declaration check (XMP identification plus output intent), not a full external
        validation. The output keeps the original filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/PdfaRequest"
      responses:
        "200":
          description: The PDF/A document.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
            X-Pdfa-Level:
              description: Requested conformance level.
              schema: { type: string }
            X-Pdfa-Claims-Conformance:
              description: Whether the output self-declares conformance.
              schema: { type: string }
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/tag:
    post:
      operationId: tagPdf
      tags: [Archival]
      summary: Add accessibility tags (PDF/UA)
      x-apex-availability: available
      description: |
        Add an accessibility structure tree to a PDF for PDF/UA compliance. Asynchronous:
        poll `GET /v1/jobs/{job_id}` — the tagging report lands in the job's `meta.report`
        and the tagged PDF (`<name>_tagged.pdf`) is served by the job download endpoint.

        Set `ai=true` for an AI-derived document title and figure alt-texts; when the AI
        assist layer is disabled or unavailable, deterministic tagging still completes with
        `ai_assist.status` set accordingly.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/TagRequest"
      responses:
        "202":
          description: Tagging job accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobSubmittedEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"

  # ------------------------------------------------------------------ Analysis

  /v1/diff:
    post:
      operationId: diffPdfs
      tags: [Analysis]
      summary: Compare two PDF versions
      x-apex-availability: available
      description: |
        Text-layer comparison of two PDF versions. With `output=json` (default) the response
        is a structured change report, returned directly (not wrapped in the envelope); set
        `ai_summary=true` to attach an AI-written summary — if the AI assist layer is disabled
        or unavailable the diff still succeeds with `ai_assist.status` set accordingly.
        With `output=pdf` the response is an annotated copy of the new version with changed
        lines highlighted, plus `X-Diff-Pages-Changed` and `X-Diff-Annotations` headers.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/DiffRequest"
      responses:
        "200":
          description: Change report (JSON) or annotated PDF, depending on `output`.
          headers:
            X-Diff-Pages-Changed:
              description: Number of pages with changes (PDF output).
              schema: { type: string }
            X-Diff-Annotations:
              description: Number of change annotations added (PDF output).
              schema: { type: string }
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DiffReport"
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/extract-data:
    post:
      operationId: extractData
      tags: [Analysis]
      summary: Extract structured data from a document
      x-apex-availability: available
      description: |
        Semantic extraction: page text, detected tables, and — when `fields` names specific
        values — AI field extraction. Asynchronous: poll `GET /v1/jobs/{job_id}`; the
        completed result is a JSON report (`<name>_extracted.json`) served by the job download
        endpoint, shaped as:

        ```json
        { "pages_total": 12, "pages": [{ "page": 1, "text": "…" }],
          "tables": [{ "page": 3, "table_index": 0, "rows": [] }],
          "ai_extraction": { "field": "value" }, "ai_assist": { } }
        ```

        Without the AI assist layer, deterministic text and tables still return with
        `ai_assist.status` set accordingly. Extraction failures surface as `status: "failed"`
        on the job.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/ExtractDataRequest"
      responses:
        "202":
          description: Extraction job accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobSubmittedEnvelope"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"

  # ------------------------------------------------------------------- Editing

  /v1/text-blocks:
    post:
      operationId: extractTextBlocks
      tags: [Editing]
      summary: Extract editable text blocks
      x-apex-availability: available
      description: |
        Extract line-level text blocks with geometry and style — rectangle, font name and
        size, colour, baseline, and horizontal start — as the groundwork for `/v1/edit-text`.
        Coordinates are PDF user-space points with the origin at the bottom-left. Set
        `ocr=true` to first give scanned pages a coordinate-bearing text layer (adds seconds
        per page). Returns the report directly (not wrapped in the envelope).
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/TextBlocksRequest"
      responses:
        "200":
          description: Text-block report.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TextBlocksResult"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  /v1/edit-text:
    post:
      operationId: editText
      tags: [Editing]
      summary: Apply in-place text edits
      x-apex-availability: available
      description: |
        Apply font- and style-preserving text edits to a PDF. `edits` is a JSON array of
        specifications — page, rectangle, and `new_text`, with optional style hints
        (`original_text`, `fontname`, `font_size`, `color`, `baseline`, `x_start`,
        `background`) passed through from `/v1/text-blocks`. Rectangles are PDF user-space
        points, origin bottom-left. Response headers `X-Edit-Applied`, `X-Edit-InStream`, and
        `X-Edit-CoverReplace` carry counts per strategy, and `X-Edit-Report` a JSON per-edit
        report. The output keeps the original filename.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/EditTextRequest"
      responses:
        "200":
          description: The edited PDF.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
            X-Edit-Applied:
              description: Number of edits applied.
              schema: { type: string }
            X-Edit-InStream:
              description: Number of edits applied in-stream.
              schema: { type: string }
            X-Edit-CoverReplace:
              description: Number of edits applied by cover-and-replace.
              schema: { type: string }
            X-Edit-Report:
              description: JSON per-edit report (index, page, strategy, overflow, warnings).
              schema: { type: string }
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"
        "502":
          $ref: "#/components/responses/EngineError"

  # --------------------------------------------------------------------- Agent

  /v1/agent:
    post:
      operationId: runAgent
      tags: [Agent]
      summary: Run the natural-language document agent
      x-apex-availability: available
      description: |
        Describe the outcome you want in plain language — for example "merge these three
        reports, stamp page numbers, and encrypt the result" — upload the input documents,
        and the agent plans and executes the operations. Asynchronous: poll
        `GET /v1/jobs/{job_id}`; the job's `meta.report` holds the run report
        (`status`, `summary`, `transcript`, `outputs` with filenames and sizes, `steps_used`,
        `ai_assist`).

        The job download endpoint serves the single output file when there is one, an
        `agent_outputs.zip` archive when there are several, or `agent_report.json` when the
        run produced no files. The agent requires the AI assist layer; if it is disabled or
        unreachable the job fails with a clear message rather than a server error.
      parameters:
        - $ref: "#/components/parameters/TenantId"
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: "#/components/schemas/AgentRequest"
      responses:
        "202":
          description: Agent job accepted.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobSubmittedEnvelope"
        "400":
          description: Empty instruction or invalid step budget.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "413":
          $ref: "#/components/responses/PayloadTooLarge"
        "422":
          $ref: "#/components/responses/ValidationError"

  # ---------------------------------------------------------------------- Jobs

  /v1/jobs/{job_id}:
    get:
      operationId: getJob
      tags: [Jobs]
      summary: Poll an asynchronous job
      x-apex-availability: available
      description: |
        Return the current status of an asynchronous job. Jobs move through
        `pending` → `running` → `completed` or `failed`. A completed job carries a
        `result_key` and its output can be fetched via the download endpoint; a failed job
        carries a human-readable `error`.
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: job_id
          in: path
          required: true
          description: Job identifier returned by an asynchronous operation.
          schema:
            type: string
      responses:
        "200":
          description: Job status envelope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/JobEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "404":
          description: Job not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "422":
          $ref: "#/components/responses/ValidationError"

  /v1/jobs/{job_id}/download:
    get:
      operationId: downloadJobResult
      tags: [Jobs]
      summary: Download a completed job's result
      x-apex-availability: preview
      x-apex-note: "Result download delivery is being finalised; verify availability with your platform contact."
      description: |
        Serve the result file of a completed job — the converted PDF, OCR output, extraction
        report, tagged document, or agent output. The response carries a
        `Content-Disposition: attachment` header with the natural output filename. The media
        type follows the result: `application/pdf` for documents, `application/json` for
        reports, `text/plain` for text sidecars, and `application/octet-stream` otherwise
        (including ZIP archives of multiple agent outputs).
      parameters:
        - $ref: "#/components/parameters/TenantId"
        - name: job_id
          in: path
          required: true
          description: Job identifier returned by an asynchronous operation.
          schema:
            type: string
      responses:
        "200":
          description: The result file.
          headers:
            Content-Disposition:
              $ref: "#/components/headers/ContentDisposition"
          content:
            application/pdf:
              schema:
                type: string
                contentMediaType: application/pdf
            application/json:
              schema:
                type: string
                contentMediaType: application/json
            text/plain:
              schema:
                type: string
            application/octet-stream:
              schema:
                type: string
                contentMediaType: application/octet-stream
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/TenantNotConfigured"
        "404":
          description: Job not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "409":
          description: Job is not yet completed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "410":
          description: The result is no longer available.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BasicError"
        "422":
          $ref: "#/components/responses/ValidationError"

  # ------------------------------------------------------------------- Service

  /v1/capabilities:
    get:
      operationId: getCapabilities
      tags: [Service]
      summary: List available operations
      x-apex-availability: available
      description: |
        Return the machine-readable operation catalog: every operation with its identifier,
        title, category, description, method and path, whether it runs as an asynchronous job,
        whether it has an AI-assisted option, its output type, and its file and parameter
        specifications. Use this to discover capabilities programmatically or to drive a
        dynamic user interface.
      responses:
        "200":
          description: Capabilities envelope.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CapabilitiesEnvelope"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/ValidationError"

  /health:
    get:
      operationId: getHealth
      tags: [Service]
      summary: Service liveness
      x-apex-availability: available
      description: >-
        Return service liveness and per-dependency readiness. `status` is `ok` when all
        processing dependencies are available, otherwise `degraded`. No authentication
        required.
      security: []
      responses:
        "200":
          description: Health report.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/HealthStatus"

components:

  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >-
        Bearer token — either an access token issued by the platform identity provider (user or machine-to-machine) or a
        Forge-issued tenant API key. Keys are managed through the platform portal; rotation
        and revocation take effect without redeployment.

  parameters:
    TenantId:
      name: X-Tenant-ID
      in: header
      required: true
      description: >-
        Tenant identifier issued by the platform operator. Requests for tenants not
        provisioned for Document Operations are refused with `403 TENANT_NOT_CONFIGURED`.
      schema:
        type: string
      example: acme

  headers:
    ContentDisposition:
      description: >-
        `attachment; filename="<output name>"` — the natural filename of the returned
        document.
      schema:
        type: string

  responses:
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/BasicError"
    TenantNotConfigured:
      description: The tenant is not configured for Document Operations.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    PayloadTooLarge:
      description: The upload exceeds the tenant's maximum file size.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    ValidationError:
      description: Request validation failed (`error.code` is `VALIDATION_ERROR`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorEnvelope"
    BadRequest:
      description: Invalid parameters.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/BasicError"
    EngineError:
      description: The document processing engine failed to complete the operation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/BasicError"

  schemas:

    # ------------------------------------------------------------- Envelope

    ResponseMeta:
      type: object
      description: Response metadata — quote `request_id` in support queries.
      properties:
        request_id:
          type: string
          description: Unique identifier for this request.
        timestamp:
          type: string
          format: date-time
          description: Server timestamp (UTC, ISO 8601).
        version:
          type: string
          description: API version.
      required: [request_id, timestamp, version]

    ErrorDetail:
      type: object
      properties:
        code:
          type: string
          description: Stable machine-readable error code, e.g. `TENANT_NOT_CONFIGURED`, `FILE_TOO_LARGE`, `VALIDATION_ERROR`, `INTERNAL_ERROR`.
        message:
          type: string
          description: Human-readable summary.
        detail:
          description: Optional structured detail (for validation errors, the field-level issues).
      required: [code, message]

    ErrorEnvelope:
      type: object
      description: Standard APEX error envelope.
      properties:
        ok:
          type: boolean
          const: false
        data:
          type: "null"
        error:
          $ref: "#/components/schemas/ErrorDetail"
        meta:
          $ref: "#/components/schemas/ResponseMeta"
      required: [ok, error, meta]

    BasicError:
      type: object
      description: Compact error body used by operation-level errors.
      properties:
        detail:
          type: string
          description: Human-readable error message.
      required: [detail]

    # ----------------------------------------------------------------- Jobs

    JobSubmitted:
      type: object
      properties:
        job_id:
          type: string
          description: Identifier to poll via `GET /v1/jobs/{job_id}`.
        status:
          type: string
          const: pending
      required: [job_id, status]

    JobSubmittedEnvelope:
      type: object
      description: Envelope returned when an asynchronous job is accepted.
      properties:
        ok:
          type: boolean
          const: true
        data:
          $ref: "#/components/schemas/JobSubmitted"
        error:
          type: "null"
        meta:
          $ref: "#/components/schemas/ResponseMeta"
      required: [ok, data, meta]

    Job:
      type: object
      description: Asynchronous job state.
      properties:
        id:
          type: string
        operation:
          type: string
          description: Operation that created the job, e.g. `convert`, `ocr`, `extract_data`, `tag`, `agent`.
        status:
          type: string
          enum: [pending, running, completed, failed]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        result_key:
          type: [string, "null"]
          description: Set when the job completes; the output is served by `GET /v1/jobs/{job_id}/download`.
        error:
          type: [string, "null"]
          description: Human-readable failure reason when `status` is `failed`.
        meta:
          type: object
          additionalProperties: true
          description: >-
            Operation-specific metadata. For agent jobs, `meta.report` carries the run report
            (summary, transcript, outputs, steps used); for tagging jobs it carries the
            tagging report.
      required: [id, operation, status, created_at, updated_at]

    JobEnvelope:
      type: object
      description: Envelope wrapping the current job state.
      properties:
        ok:
          type: boolean
          const: true
        data:
          $ref: "#/components/schemas/Job"
        error:
          type: "null"
        meta:
          $ref: "#/components/schemas/ResponseMeta"
      required: [ok, data, meta]

    # ------------------------------------------------------ Report payloads

    MetadataDocument:
      type: object
      description: PDF metadata fields.
      properties:
        title: { type: string }
        author: { type: string }
        subject: { type: string }
        keywords: { type: string }
        creator: { type: string }
        producer: { type: string }
        creation_date: { type: string }
        mod_date: { type: string }
        trapped: { type: string }
        custom:
          type: object
          additionalProperties: { type: string }
          description: Custom metadata key-value pairs.

    MetadataReadEnvelope:
      type: object
      properties:
        ok:
          type: boolean
          const: true
        data:
          $ref: "#/components/schemas/MetadataDocument"
        error:
          type: "null"
        meta:
          $ref: "#/components/schemas/ResponseMeta"
      required: [ok, data, meta]

    ProvenanceVerifyResult:
      type: object
      description: Provenance chain verification report.
      properties:
        has_chain:
          type: boolean
          description: Whether the document carries a provenance chain.
        entries:
          type: array
          items:
            type: object
            additionalProperties: true
          description: The chain entries in order.
        chain_valid:
          type: boolean
          description: Whether every signature in the chain verifies.
        content_intact:
          type: boolean
          description: Whether the document content matches the last stamped state.
        dev_key:
          type: boolean
          description: True when the chain was signed with a non-production development key.

    ProvenanceVerifyEnvelope:
      type: object
      properties:
        ok:
          type: boolean
          const: true
        data:
          $ref: "#/components/schemas/ProvenanceVerifyResult"
        error:
          type: "null"
        meta:
          $ref: "#/components/schemas/ResponseMeta"
      required: [ok, data, meta]

    TextBlock:
      type: object
      description: One editable text block. Geometry is PDF user-space points, origin bottom-left.
      properties:
        block_id: { type: string }
        page: { type: integer }
        rect:
          type: array
          items: { type: number }
          description: "[x0, y0, x1, y1]"
        text: { type: string }
        fontname: { type: string }
        font_size: { type: number }
        color:
          type: array
          items: { type: number }
          description: RGB components, 0 to 1.
        baseline: { type: number }
        x_start: { type: number }
        upright: { type: boolean }

    TextBlocksResult:
      type: object
      description: Text-block extraction report (returned directly, not enveloped).
      properties:
        pages:
          type: array
          items:
            type: object
            properties:
              page: { type: integer }
              width: { type: number }
              height: { type: number }
              blocks:
                type: array
                items:
                  $ref: "#/components/schemas/TextBlock"
        block_count:
          type: integer
        ocr_applied:
          type: boolean

    DiffReport:
      type: object
      additionalProperties: true
      description: >-
        Structured change report (returned directly, not enveloped): per-page changes with
        added/removed lines, and — when requested and available — an `ai_summary` plus an
        `ai_assist` status block.

    RedactSuggestions:
      type: object
      description: Suggested redaction targets (returned directly, not enveloped).
      properties:
        suggestions:
          type: array
          items:
            type: object
            additionalProperties: true
          description: Detected targets with their locations and categories.
        ai_assist:
          type: object
          additionalProperties: true
          description: AI assist status block (`status` is `disabled` or `unavailable` when the AI layer did not contribute).

    CapabilitiesEnvelope:
      type: object
      description: Envelope wrapping the operation catalog.
      properties:
        ok:
          type: boolean
          const: true
        data:
          type: object
          properties:
            service: { type: string }
            manifest_version: { type: string }
            operations:
              type: array
              items:
                type: object
                additionalProperties: true
                description: >-
                  Operation descriptor — id, title, category, description, method, path,
                  async_job, ai, output, files, params, panel.
            jobs:
              type: object
              additionalProperties: true
              description: How to poll and download asynchronous jobs.
        error:
          type: "null"
        meta:
          $ref: "#/components/schemas/ResponseMeta"
      required: [ok, data, meta]

    HealthStatus:
      type: object
      description: Service health report (returned directly, not enveloped).
      properties:
        status:
          type: string
          enum: [ok, degraded]
        service:
          type: string
        version:
          type: string
        timestamp:
          type: string
          format: date-time
        dependencies:
          type: object
          additionalProperties:
            type: object
            properties:
              status:
                type: string
                enum: [ok, missing]
              path:
                type: string
          description: Per-dependency readiness map.
        ai_assist:
          type: string
          enum: [enabled, disabled]
          description: Whether the optional AI assist layer is active. Informational only.

    # ----------------------------------------------------- Request payloads

    ConvertRequest:
      type: object
      description: Provide either `file` or `url` (URL mode must be enabled for your deployment).
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: Source document — Office (Word, Excel, PowerPoint, OpenDocument), HTML, or Markdown.
        url:
          type: string
          default: ""
          description: Web page to render as PDF (URL mode only).
        zoom:
          type: number
          default: 1.0
          description: Zoom factor for HTML and URL rendering.

    OcrRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        mode:
          type: string
          default: auto
          enum: [auto, force, strict]
          description: "`auto` skips pages that already have text; `force` re-recognises everything; `strict` aborts if text is found."
        languages:
          type: string
          default: eng
          description: Comma-separated OCR language codes, e.g. `eng`, `afr`, `deu`.
        deskew:
          type: boolean
          default: false
          description: Straighten skewed scans.
        clean:
          type: boolean
          default: false
          description: Clean the input before recognition.
        clean_final:
          type: boolean
          default: false
          description: Clean the final output.
        sidecar:
          type: boolean
          default: false
          description: Also produce a plain-text sidecar file.
      required: [file]

    MergeRequest:
      type: object
      properties:
        files:
          type: array
          items:
            type: string
            contentMediaType: application/octet-stream
          description: Two or more PDF files to merge, in order.
        remove_signatures:
          type: boolean
          default: false
          description: Strip digital signatures from the inputs.
        generate_toc:
          type: boolean
          default: false
          description: Generate a table of contents from the input filenames.
      required: [files]

    SplitRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        method:
          type: string
          default: page_numbers
          enum: [page_numbers, every_n]
        pages:
          type: string
          default: ""
          description: Split-after page boundaries, e.g. `"2,4"` (required when `method=page_numbers`).
        every_n:
          type: integer
          default: 1
          minimum: 1
          description: Chunk size in pages (required when `method=every_n`).
      required: [file]

    ExtractPagesRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        pages:
          type: string
          description: Page expression, e.g. `"1,3,5-8"` or `"odd & 1-10"`.
      required: [file, pages]

    CompressRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        method:
          type: string
          default: quality
          enum: [quality, target_size]
        quality:
          type: integer
          default: 5
          minimum: 1
          maximum: 9
          description: 1 = maximum compression, 9 = minimum compression (`method=quality`).
        target_size_mb:
          type: number
          default: 0.0
          description: Target output size in megabytes (required when `method=target_size`).
        linearize:
          type: boolean
          default: false
          description: Optimise for fast web viewing.
      required: [file]

    WatermarkRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        type:
          type: string
          default: text
          enum: [text, image]
        text:
          type: string
          default: ""
          description: Watermark text (required when `type=text`).
        image:
          type: string
          contentMediaType: application/octet-stream
          description: Watermark image (required when `type=image`).
        color:
          type: string
          default: "#d3d3d3"
          description: Hex colour for text watermarks.
        font_size:
          type: integer
          default: 12
        rotation:
          type: number
          default: 0.0
          description: Rotation in degrees.
        opacity:
          type: number
          default: 0.5
          description: 0.0 (invisible) to 1.0 (opaque).
        pages:
          type: string
          default: ""
          description: Page expression; empty applies to all pages.
      required: [file]

    PageNumbersRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        position:
          type: string
          default: bc
          enum: [tl, tc, tr, ml, mc, mr, bl, bc, br]
          description: 3x3 grid position.
        pages:
          type: string
          default: ""
          description: Page expression; empty applies to all pages.
        start_number:
          type: integer
          default: 1
          minimum: 1
        font_size:
          type: integer
          default: 12
        format_string:
          type: string
          default: "Page {n}"
          description: "Template for the stamped text — `{n}` page number, `{total}` page count, `{filename}` source filename."
      required: [file]

    QrRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        payload:
          type: string
          description: QR content — a URL, verification link, or tracking identifier.
        position:
          type: string
          default: br
          enum: [tl, tc, tr, ml, mc, mr, bl, bc, br]
          description: 3x3 grid position.
        scale:
          type: number
          default: 0.15
          description: QR size as a fraction of page width (0.05 to 0.5).
        pages:
          type: string
          default: ""
          description: Page expression; empty applies to all pages.
      required: [file, payload]

    MetadataReadRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
      required: [file]

    MetadataWriteRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        delete_all:
          type: boolean
          default: false
          description: Wipe all existing metadata before writing.
        title: { type: string, default: "" }
        author: { type: string, default: "" }
        subject: { type: string, default: "" }
        keywords: { type: string, default: "" }
        creator: { type: string, default: "" }
        producer: { type: string, default: "" }
        creation_date:
          type: string
          default: ""
          description: ISO 8601 date-time.
        mod_date:
          type: string
          default: ""
          description: ISO 8601 date-time.
        trapped:
          type: string
          default: ""
          description: "`Unknown`, `True`, or `False`."
        custom_keys:
          type: string
          default: "{}"
          description: JSON object (string-to-string) of custom metadata fields.
      required: [file]

    EncryptRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        user_pw:
          type: string
          default: ""
          description: Password required to open the document (may be empty).
        owner_pw:
          type: string
          description: Owner password (required) — controls permission changes.
        aes128:
          type: boolean
          default: false
          description: Use AES-128 instead of the default AES-256.
      required: [file, owner_pw]

    PermissionsRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        owner_pw:
          type: string
          description: Owner password (required for the restrictions to be meaningful).
        user_pw:
          type: string
          default: ""
          description: Optional password required to open the document.
        prevent_assembly:
          type: boolean
          default: false
          description: Prevent page insertion, deletion, and rotation.
        prevent_extract:
          type: boolean
          default: false
          description: Prevent content copying.
        prevent_extract_accessibility:
          type: boolean
          default: false
          description: Prevent extraction by accessibility tools.
        prevent_fill_forms:
          type: boolean
          default: false
        prevent_modify:
          type: boolean
          default: false
        prevent_modify_annotations:
          type: boolean
          default: false
        prevent_print:
          type: boolean
          default: false
        prevent_print_high_quality:
          type: boolean
          default: false
      required: [file, owner_pw]

    SanitizeRequest:
      type: object
      description: At least one flag must be `true`.
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        remove_javascript:
          type: boolean
          default: true
        remove_embedded_files:
          type: boolean
          default: true
        remove_xmp_metadata:
          type: boolean
          default: false
        remove_document_metadata:
          type: boolean
          default: false
        remove_links:
          type: boolean
          default: false
          description: Strip link annotations and document open actions.
        remove_fonts:
          type: boolean
          default: false
          description: Reserved; currently has no effect.
      required: [file]

    RedactRequest:
      type: object
      description: At least one of `boxes` or `patterns` must be provided.
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        boxes:
          type: string
          default: "[]"
          description: >-
            JSON array of explicit regions:
            `[{"page": 1, "rect": [x0, y0, x1, y1]}]` —
            PDF user-space points, origin bottom-left.
        patterns:
          type: string
          default: "[]"
          description: JSON array of regular expressions matched against the text layer.
        case_insensitive:
          type: boolean
          default: true
          description: Case-insensitive pattern matching.
        dpi:
          type: integer
          default: 150
          minimum: 72
          maximum: 600
          description: Rasterisation resolution.
      required: [file]

    RedactSuggestRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        ai:
          type: boolean
          default: false
          description: Add AI-suggested entities to the deterministic catalog.
      required: [file]

    ProvenanceStampRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        op:
          type: string
          default: stamp
          description: Operation label recorded in the chain entry.
        actor:
          type: string
          default: ""
          description: Author identity; defaults to the authenticated identity.
        note:
          type: string
          default: ""
          description: Free-text note.
      required: [file]

    ProvenanceVerifyRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        public_key:
          type: string
          default: ""
          description: Base64 Ed25519 public key to pin verification to (optional).
      required: [file]

    LinksRequest:
      type: object
      description: Provide explicit `links`, set `autolink=true`, or both.
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        links:
          type: string
          default: "[]"
          description: >-
            JSON array of explicit links:
            `[{"page": 1, "rect": [x0, y0, x1, y1], "target": "https://…"}]` —
            target is a URL, `mailto:` address, or `page:N` internal jump; rect is PDF
            user-space points, origin bottom-left.
        autolink:
          type: boolean
          default: false
          description: Detect URLs and email addresses in the text layer and link them.
      required: [file]

    PdfaRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        level:
          type: string
          default: "2b"
          enum: ["1b", "2b", "3b"]
          description: PDF/A conformance level.
      required: [file]

    AnnotateRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        annotations:
          type: string
          description: >-
            JSON array (non-empty):
            `[{"page": 1, "rect": [x0, y0, x1, y1], "type": "note" | "highlight" | "freetext",
            "text": "…", "author": "…", "color": [r, g, b]}]` —
            rect is PDF user-space points, origin bottom-left; colour components are floats
            0 to 1 (optional).
        author:
          type: string
          default: ""
          description: Default author applied to annotations without one.
        flatten:
          type: boolean
          default: false
          description: >-
            Burn annotations into the page content. Flattening rewrites page streams and
            breaks any existing provenance content hash.
      required: [file, annotations]

    DiffRequest:
      type: object
      properties:
        file_old:
          type: string
          contentMediaType: application/octet-stream
          description: Baseline PDF.
        file_new:
          type: string
          contentMediaType: application/octet-stream
          description: Revised PDF.
        output:
          type: string
          default: json
          enum: [json, pdf]
          description: "`json` for a structured change report; `pdf` for an annotated copy of the revised file."
        ai_summary:
          type: boolean
          default: false
          description: Attach an AI-written summary to the JSON report.
      required: [file_old, file_new]

    TextBlocksRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        ocr:
          type: boolean
          default: false
          description: Run a synchronous OCR pass first so scanned pages gain a coordinate-bearing text layer (adds seconds per page).
        languages:
          type: string
          default: eng
          description: Comma-separated OCR language codes.
        pages:
          type: string
          default: ""
          description: Page expression subset, e.g. `"1,3,5-8"`; empty selects all pages.
      required: [file]

    EditTextRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        edits:
          type: string
          description: >-
            JSON array (non-empty) of edit specifications:
            `[{"page": 1, "rect": [x0, y0, x1, y1], "new_text": "…", "original_text": "…",
            "fontname": "…", "font_size": 11, "color": [r, g, b], "baseline": 700.1,
            "x_start": 72, "background": [1, 1, 1]}]` —
            optional style hints pass through from `/v1/text-blocks`; rect is PDF user-space
            points, origin bottom-left.
      required: [file, edits]

    ExtractDataRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        tables:
          type: boolean
          default: true
          description: Detect and extract tables.
        fields:
          type: string
          default: "[]"
          description: JSON array of field names for AI extraction, e.g. `["invoice_number", "total"]`.
      required: [file]

    TagRequest:
      type: object
      properties:
        file:
          type: string
          contentMediaType: application/octet-stream
          description: PDF file.
        language:
          type: string
          default: en
          description: BCP 47 language tag for the document.
        title:
          type: string
          default: ""
          description: Document title override; defaults to a name derived from the filename, or from AI when `ai=true`.
        ai:
          type: boolean
          default: false
          description: Use AI for a human-meaningful title and figure alt-texts.
      required: [file]

    AgentRequest:
      type: object
      properties:
        instruction:
          type: string
          description: What to do, in natural language.
        files:
          type: array
          items:
            type: string
            contentMediaType: application/octet-stream
          default: []
          description: Zero or more input documents.
        max_steps:
          type: integer
          default: 6
          description: Step budget for the agent (capped at 12).
      required: [instruction]
