# AUTO-GENERATED tier-A mirror spec — do not edit.
# Source: openapi/runtime/documents.yaml (runtime 0.71.2)
#  + policy: openapi/mirror-policies/documents.json
# Regenerate: node scripts/generate-mirror-specs.mjs
openapi: 3.0.3
info:
  title: naturali.ai — Documents API
  version: 1.0.0
  description: >-
    Documents: the retrievable unit of knowledge — created from text or from a stored file, chunked
    and embedded by an ingestion pass whose status is readable, re-ingestable when a rule changes,
    and tagged for filtered search. A fully runtime-backed module — this spec is generated verbatim
    from the runtime's own, re-rooted under /v1/projects/{project_id}. The project in the path is
    authorized by naturali and enforced upstream by the project's scoped credential.


    This module mirrors the upstream runtime verbatim (tier A, #304): paths are the runtime's own
    re-rooted under /v1/projects/{project_id}, and every field, method, status code and error shape
    passes through unchanged. Errors raised by the runtime arrive in its envelope; errors raised by
    naturali itself (authentication, project resolution, an unreachable runtime) use naturali's.
  contact:
    name: naturali.ai
    url: https://naturali.ai
servers:
  - url: "{baseUrl}"
    description: Host of your naturali.ai deployment; every path carries the /v1 prefix.
    variables:
      baseUrl:
        description: Base host URL.
        default: https://api.naturali.ai
tags:
  - name: Documents
    description: Manage documents
security:
  - bearerAuth: []
  - oauth2:
      - mcp:access
paths:
  /v1/projects/{project_id}/documents:
    get:
      tags:
        - Documents
      summary: List documents
      description: Returns all documents in the project named in the path.
      operationId: listDocuments
      parameters:
        - name: path_prefix
          in: query
          required: false
          description: "Only documents filed under this directory. The prefix is a path boundary, not a
            substring: `/reports` returns `/reports/q1.txt` and never `/reports-archive/q1.txt`, and
            `/` selects the whole project. A leading slash is optional and a trailing one is
            ignored, so `reports`, `/reports` and `/reports/` are the same filter. `%` and `_` are
            literal characters, not wildcards."
          schema:
            type: string
            example: /reports/
        - name: include_withdrawn
          in: query
          required: false
          description: Include withdrawn documents. A withdrawn document leaves every default read and is not
            in the knowledge index at all — its chunks are dropped when it is withdrawn — so this
            shows it in the listing but never in a search.
          schema:
            type: boolean
            default: false
        - name: related_to
          in: query
          description: "Only documents related to this one, on either side of the edge: what it points at, and
            what points at it. An id with no relations narrows the listing to nothing."
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
        - $ref: "#/components/parameters/TagsQuery"
        - $ref: "#/components/parameters/MetadataQuery"
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: List of documents
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/DocumentRecord"
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    post:
      tags:
        - Documents
      summary: Create a document
      description: Creates a new text document and generates an embedding vector for semantic search, in
        the project named in the path.
      operationId: createDocument
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - content
              additionalProperties: false
              properties:
                content:
                  type: string
                  example: The quick brown fox jumps over the lazy dog.
                path:
                  type: string
                  description: Logical path within the project (e.g. /reports/q1.txt). Defaults to `/<filename>`, or
                    to `/<document_id>.txt` when neither is given — a document with no path is
                    reachable only by its id, since a prefix filter never matches null.
                  example: /reports/q1.txt
                filename:
                  type: string
                  example: my-doc.txt
                title:
                  type: string
                  description: Document title
                metadata:
                  description: Arbitrary metadata object. Unlike other body fields, keys are stored and returned
                    verbatim in the casing supplied — they are not converted between snake_case and
                    camelCase.
                  allOf:
                    - $ref: "#/components/schemas/MetadataBag"
                tags:
                  $ref: "#/components/schemas/TagBag"
                chunk_strategy:
                  type: string
                  enum:
                    - page
                    - whole
                    - size
                  description: How to split the content into embeddable chunks. `whole` (default) stores the content
                    as a single chunk; `size` splits into fixed-size character windows with overlap.
                    `page` is equivalent to `whole` for plain text.
                  default: whole
                chunk_size:
                  type: integer
                  description: Window size in characters when `chunk_strategy=size`. Defaults to 1000.
                  example: 1000
                chunk_overlap:
                  type: integer
                  description: Overlap in characters between consecutive windows when `chunk_strategy=size`. Defaults
                    to 200.
                  example: 200
      responses:
        "201":
          description: Document created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentRecord"
        "400":
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: "`path` is held by another file in the project (`NAME_CONFLICT`), or the project's
            `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`). Delete stored content, or
            raise the quota — no window reset clears a stored total, so no `Retry-After` is sent."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/ingest:
    post:
      tags:
        - Documents
      summary: Ingest a file into a chunked document
      description: |
        Parses an already-uploaded file and creates one Document split into one or
        more embedded chunks. The source format is detected from the file's content
        type: PDFs are parsed page-by-page; `text/plain` and `text/markdown` files
        are read as a single source. How the source is chunked is controlled by
        `chunk_strategy`.

        A file can only back one Document — a second call with the same `file_id`
        returns `409 FILE_ALREADY_INGESTED`. To re-process an already-ingested file
        (e.g. with a different `chunk_strategy`), use
        `POST /documents/{document_id}/ingest`; to ingest the same source under a
        different path, upload a new copy of the file first.
      operationId: ingestDocument
      x-iam-action: documents:IngestDocument
      parameters:
        - name: wait
          in: query
          required: false
          description: When omitted or `false` (default), processing runs in the background and `202 Accepted`
            is returned immediately with `status=pending`. Pass `true` to block until processing
            completes and receive `201 Created` with `status=ready`.
          schema:
            type: boolean
            default: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - file_id
              additionalProperties: false
              properties:
                file_id:
                  x-naturali-ref: files
                  type: string
                  description: ID of the uploaded file. Must be one of application/pdf, text/plain, text/markdown.
                  example: file_V1StGXR8Z5jdHi6B
                path_prefix:
                  type: string
                  description: Path prefix under which to store the document (e.g. /docs/). The filename is appended
                    automatically.
                  example: /docs/
                tags:
                  $ref: "#/components/schemas/TagBag"
                chunk_strategy:
                  type: string
                  enum:
                    - page
                    - whole
                    - size
                  description: How to split the source into chunks. `page` (default) creates one chunk per non-empty
                    page (PDF); for non-paged sources it yields a single chunk. `whole` joins
                    everything into one chunk. `size` splits into fixed-size character windows with
                    overlap.
                  default: page
                chunk_size:
                  type: integer
                  description: Window size in characters when `chunk_strategy=size`. Defaults to 1000.
                  example: 1000
                chunk_overlap:
                  type: integer
                  description: Overlap in characters between consecutive windows when `chunk_strategy=size`. Defaults
                    to 200.
                  example: 200
      responses:
        "201":
          description: Ingestion completed synchronously (only when `?wait=true`). The document is fully
            indexed and ready for search.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestedDocumentRecord"
        "202":
          description: Ingestion accepted. The document record has been created with `status=pending` and
            processing runs in the background. Poll `GET
            /v1/projects/{project_id}/documents/{document_id}` until `status` is `ready` or
            `failed`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestedDocumentRecord"
        "400":
          description: Invalid request, file not found, or unsupported content type
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: The file already backs a Document (a file can only be ingested once — use `POST
            /documents/{document_id}/ingest` to re-process the existing document, or upload a new
            copy of the file to ingest it separately), the path it would be filed at is held by
            another file in the project (`NAME_CONFLICT`), or the project's `storage_bytes` quota is
            exceeded (`QUOTA_STORAGE_EXCEEDED`; delete stored content or raise the quota — no
            `Retry-After` is sent, since no window reset clears a stored total).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          description: The file is too large to ingest synchronously (`?wait=true`). Retry in background mode
            and poll the document status.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/export:
    get:
      tags:
        - Documents
      summary: Export documents as NDJSON
      description: "Streams a project's documents as newline-delimited JSON — one document object per
        line, oldest first — for archiving the corpus or shipping it into another system.
        `project_id` is required: the export is per-project by design. The rows are the rows the
        listing returns for the same caller, so a policy that hides a document hides it here too,
        and withdrawn documents and the reserved `/.system/` root are left out."
      operationId: exportDocuments
      x-mcp-exclude: true
      parameters:
        - name: path_prefix
          in: query
          description: Only documents filed under this directory. The prefix is a path boundary, not a
            substring, exactly as on the listing.
          schema:
            type: string
            example: /reports
      responses:
        "200":
          description: A newline-delimited stream of documents. Each line is a JSON object with the same
            fields as `Document`.
          content:
            application/x-ndjson:
              schema:
                type: string
        "400":
          description: "`project_id` is required"
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/{document_id}:
    get:
      tags:
        - Documents
      summary: Get a document by ID
      description: Returns a document with its text content
      operationId: getDocument
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      responses:
        "200":
          description: Document found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentRecord"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    delete:
      tags:
        - Documents
      summary: Delete a document
      description: Deletes a document and its underlying file
      operationId: deleteDocument
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      responses:
        "204":
          description: Document deleted
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      tags:
        - Documents
      summary: Update a document
      description: Updates document content, title, path, metadata, or tags. Supplying `path` moves the
        document to a new logical path within the project.
      operationId: updateDocument
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
        - $ref: "#/components/parameters/IfMatchVersion"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                content:
                  type: string
                  description: New text content
                title:
                  type: string
                  description: New title
                path:
                  type: string
                  nullable: true
                  description: Logical path within the project (e.g. /reports/q1.txt). Pass null to clear.
                  example: /reports/q1.txt
                metadata:
                  description: Arbitrary metadata object, replacing the stored bag; `null` clears it. Unlike other
                    body fields, keys are stored and returned verbatim in the casing supplied — they
                    are not converted between snake_case and camelCase.
                  allOf:
                    - $ref: "#/components/schemas/NullableMetadataBag"
                tags:
                  $ref: "#/components/schemas/TagBag"
                expected_version:
                  description: Refuses the write unless the document is at this version.
                  allOf:
                    - $ref: "#/components/schemas/ExpectedVersion"
      responses:
        "200":
          description: Document updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentRecord"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: The document has moved past the version this write read (`VERSION_CONFLICT`, with
            `meta.current_version`), or `path` is held by another file in the project
            (`NAME_CONFLICT`). Nothing is written.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/{document_id}/relations:
    get:
      tags:
        - Documents
      summary: List a document's relations
      description: Returns the typed edges this document asserts about others, oldest first. An edge is
        owned by the document it leaves, so this is what the document claims — use `?related_to=` on
        the listing to find what claims something about it.
      operationId: listDocumentRelations
      parameters:
        - name: document_id
          in: path
          required: true
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      responses:
        "200":
          description: The document's relations
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/DocumentRelation"
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Document not found
    post:
      tags:
        - Documents
      summary: Assert a relation
      description: >-
        Asserts a typed edge from this document to another in the same project. Asserting the same
        edge twice is `409`: an edge is a fact, and the second assertion is the same fact.


        Writing an edge is a write of the asserting document (`documents:UpdateDocument`); the
        document it points at is unchanged, which is what lets an agent record what its own report
        derives from without being able to make another report claim anything.
      operationId: createDocumentRelation
      parameters:
        - name: document_id
          in: path
          required: true
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - type
                - to_document_id
              properties:
                type:
                  type: string
                  enum:
                    - derived_from
                    - supersedes
                    - cites
                  description: What this document claims about the other
                  example: cites
                to_document_id:
                  type: string
                  description: The document the edge points at. Must be in the same project, and visible to the
                    caller.
                  example: doc_9Kp2mQxZ7bT4rN6W
      responses:
        "201":
          description: The asserted relation
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentRelation"
        "400":
          description: An undeclared `type`, a `to_document_id` that is not a document id, a document relating
            to itself, or a target in another project
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Either document was not found
        "409":
          description: That relation is already asserted
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/{document_id}/relations/{relation_id}:
    delete:
      tags:
        - Documents
      summary: Retract a relation
      description: Removes an edge this document asserts. Both documents are left as they are — retracting
        a claim is not a change to what it was about.
      operationId: deleteDocumentRelation
      parameters:
        - name: document_id
          in: path
          required: true
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
        - name: relation_id
          in: path
          required: true
          schema:
            type: string
            example: doc_rel_V1StGXR8Z5jdHi6B
      responses:
        "204":
          description: Relation retracted
        "401":
          description: Unauthorized
        "403":
          description: Forbidden
        "404":
          description: Document or relation not found
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/{document_id}/status:
    get:
      tags:
        - Documents
      summary: Get document ingestion status
      description: |
        Returns a lightweight ingestion status payload for polling — `status`,
        `chunk_count`, `total_pages`, and (when failed) `error`. Unlike
        `GET /documents/{document_id}`, it never returns the assembled chunk
        content, so it is cheap to poll on large documents. A document whose
        ingestion has stalled (no progress past the configured timeout) is
        transitioned to `failed` with `error=INGESTION_TIMEOUT` on read.
      operationId: getDocumentStatus
      x-iam-action: documents:GetDocument
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      responses:
        "200":
          description: Document ingestion status
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentStatusRecord"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/{document_id}/withdraw:
    post:
      tags:
        - Documents
      summary: Withdraw a document
      description: |
        Takes a document out of every default read while keeping its history. The withdrawal is archived as a **tombstone version**, so one mechanism answers what a document holds now and there is no second lifecycle flag for a reader to miss.

        Its chunks are dropped from the knowledge index, so a withdrawn document costs a live search nothing. The content is not lost with them: it is in the version before the tombstone, which [`POST /v1/projects/{project_id}/documents/{document_id}/versions/{version}/restore`](/docs/api/documents/restore-document-version) re-chunks from.

        `DELETE` stays what it is — permanent, and it removes the backing file. Withdrawal is the reversible act. It does not apply under `/.system/`: a platform-written document keeps the owning module's lifecycle.
      operationId: withdrawDocument
      parameters:
        - name: document_id
          in: path
          required: true
          schema:
            type: string
        - $ref: "#/components/parameters/IfMatchVersion"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                version_label:
                  type: string
                  nullable: true
                  description: Optional tag for the tombstone version this write archives.
                  example: superseded-by-q2
                expected_version:
                  description: Refuses the withdrawal unless the document is at this version.
                  allOf:
                    - $ref: "#/components/schemas/ExpectedVersion"
      responses:
        "200":
          description: The withdrawn document
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentRecord"
        "400":
          description: Bad Request — the document is filed under the reserved root
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: The document is already withdrawn (`DOCUMENT_ALREADY_WITHDRAWN`), or it has moved past
            the version this write read (`VERSION_CONFLICT`, with `meta.current_version`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/{document_id}/versions:
    get:
      tags:
        - Documents
      summary: List a document's content versions
      description: >
        Returns the document's archived states, newest first. A version is written on create and on
        every subsequent write that changes the content or its annotations, and a withdrawal is
        archived as a version carrying no content.
      operationId: listDocumentVersions
      parameters:
        - name: document_id
          in: path
          required: true
          schema:
            type: string
        - name: limit
          in: query
          required: false
          description: Maximum number of results to return
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: offset
          in: query
          required: false
          description: Number of results to skip
          schema:
            type: integer
            default: 0
      responses:
        "200":
          description: List of document versions, newest first
          content:
            application/json:
              schema:
                type: object
                required:
                  - data
                  - total
                  - limit
                  - offset
                properties:
                  data:
                    type: array
                    items:
                      $ref: "#/components/schemas/DocumentVersion"
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/{document_id}/versions/{version}:
    get:
      tags:
        - Documents
      summary: Fetch an archived document version
      description: >
        Returns the exact content and annotations the document held at a given version, which is
        what lets a run that cited a version read what it read.
      operationId: getDocumentVersion
      parameters:
        - name: document_id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      responses:
        "200":
          description: Archived document version
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentVersion"
        "400":
          description: Bad Request — version is not a positive integer
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/{document_id}/versions/{version}/restore:
    post:
      tags:
        - Documents
      summary: Restore an archived document version
      description: >
        Writes an archived version's content and annotations back as the document's live state,
        which archives them again as a **new** version rather than rewinding the counter — so a run
        citing any version in between still resolves.


        The restore runs through the ordinary update path, so the content is re-chunked and
        re-embedded; restoring the state the document already holds is a no-op and archives nothing.
        Restoring any content version of a withdrawn document brings it back into listings and
        search.


        A tombstone version has no content, so naming one is `400 VALIDATION_FAILED`: restore the
        version before it instead.
      operationId: restoreDocumentVersion
      parameters:
        - name: document_id
          in: path
          required: true
          schema:
            type: string
        - name: version
          in: path
          required: true
          schema:
            type: integer
            minimum: 1
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                label:
                  type: string
                  nullable: true
                  description: Optional tag for the version this restore archives. Defaults to `restored from
                    v<version>`.
                  example: reinstated
      responses:
        "200":
          description: The document, at its new version
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DocumentRecord"
        "400":
          description: Bad Request — invalid version, or the version is a withdrawal
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document or version not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: Another write took the document's version first (`VERSION_CONFLICT`, with
            `meta.current_version`), or the restored `path` is held by another file in the project
            (`NAME_CONFLICT`). Nothing is written.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/{document_id}/ingest:
    post:
      tags:
        - Documents
      summary: Re-ingest an existing document
      description: |
        Re-runs ingestion for an existing document against its already-stored
        source file. Existing chunks are discarded and the document is reset to
        `status=pending` before re-processing. Use this to recover a document
        stuck in `processing`/`failed` or to re-chunk with a different strategy
        without re-uploading the file. Background by default (`202`); pass
        `?wait=true` to run synchronously (`201`).
      operationId: reingestDocument
      x-iam-action: documents:IngestDocument
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
        - name: wait
          in: query
          required: false
          description: When omitted or `false` (default), processing runs in the background and `202 Accepted`
            is returned immediately with `status=pending`. Pass `true` to block until processing
            completes and receive `201 Created` with `status=ready`.
          schema:
            type: boolean
            default: false
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              properties:
                chunk_strategy:
                  type: string
                  enum:
                    - page
                    - whole
                    - size
                  description: How to split the source into chunks. Defaults to `page`.
                  default: page
                chunk_size:
                  type: integer
                  description: Window size in characters when `chunk_strategy=size`. Defaults to 1000.
                  example: 1000
                chunk_overlap:
                  type: integer
                  description: Overlap in characters between consecutive windows when `chunk_strategy=size`. Defaults
                    to 200.
                  example: 200
      responses:
        "201":
          description: Re-ingestion completed synchronously (only when `?wait=true`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestedDocumentRecord"
        "202":
          description: Re-ingestion accepted. The document was reset to `status=pending` and processing runs
            in the background. Poll `GET /v1/projects/{project_id}/documents/{document_id}/status`.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/IngestedDocumentRecord"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "409":
          description: The project's `storage_bytes` quota is exceeded (`QUOTA_STORAGE_EXCEEDED`). Delete
            stored content, or raise the quota — no window reset clears a stored total, so no
            `Retry-After` is sent.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "413":
          description: The file is too large to re-ingest synchronously (`?wait=true`). Retry in background
            mode.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
  /v1/projects/{project_id}/documents/{document_id}/tags:
    get:
      tags:
        - Documents
      summary: Get document tags
      description: Returns all tags attached to the document
      operationId: getDocumentTags
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      responses:
        "200":
          description: Document tags
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagBag"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    put:
      tags:
        - Documents
      summary: Replace document tags
      description: Replaces all tags on the document with the provided tags (not merged)
      operationId: replaceDocumentTags
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TagBag"
      responses:
        "200":
          description: Tags replaced
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagBag"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    patch:
      tags:
        - Documents
      summary: Merge document tags
      description: Merges provided tags into the document's existing tags (existing tags are preserved
        unless overridden)
      operationId: mergeDocumentTags
      parameters:
        - name: document_id
          in: path
          required: true
          description: Document ID
          schema:
            type: string
            example: doc_V1StGXR8Z5jdHi6B
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TagBag"
      responses:
        "200":
          description: Tags merged
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TagBag"
        "401":
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "403":
          description: Forbidden
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
        "404":
          description: Document not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
    parameters:
      - $ref: "#/components/parameters/ProjectId"
components:
  schemas:
    DocumentVersion:
      type: object
      description: An immutable archive of a document's content and annotations at one version.
      properties:
        id:
          type: string
          description: Public ID of the archived version
          example: doc_ver_V1StGXR8Z5jdHi6B
        document_id:
          x-naturali-ref: documents
          type: string
          description: Public ID of the document this version belongs to
          example: doc_V1StGXR8Z5jdHi6B
        version:
          type: integer
          description: The archived version number
          example: 1
        config:
          type: object
          additionalProperties: true
          description: >-
            The document's versioned surface as it stood at this version: its `content`, `title`,
            `path`, `metadata`, `tags` and chunk configuration. A full snapshot rather than a diff,
            because what a restore has to reproduce is what a run read — which is a read by version,
            and a diff chain would have to be replayed to answer it.


            A withdrawal is archived as a version too, carrying `withdrawn: true` and no content.
            Restoring one is refused; restore the version before it.


            Deliberately open rather than a fixed schema: an archive written by an earlier release
            of the runtime reflects the document surface **of its own time**, so it may carry fields
            the current API no longer documents.
        label:
          type: string
          nullable: true
          description: Optional human tag for this version, e.g. `pre-correction`. Set from the
            `version_label` of the write that archived it, or generated for a restore or a
            withdrawal.
          example: pre-correction
        created_by:
          type: string
          nullable: true
          description: Public ID of the user whose write produced this version; null for a write with no
            request user behind it.
          example: user_V1StGXR8Z5jdHi6B
        created_at:
          type: string
          format: date-time
    DocumentRecord:
      type: object
      properties:
        id:
          type: string
          description: Document ID
          example: doc_V1StGXR8Z5jdHi6B
        file_id:
          x-naturali-ref: files
          type: string
          description: Underlying file ID
          example: file_V1StGXR8Z5jdHi6B
        project_id:
          x-naturali-ref: projects
          type: string
          description: Project ID
          example: proj_V1StGXR8Z5jdHi6B
        path:
          type: string
          nullable: true
          description: Logical path of the document within the project (e.g. /reports/q1.txt)
          example: /reports/q1.txt
        filename:
          type: string
          description: Original filename
          example: my-doc.txt
        content_type:
          type: string
          description: Media type of the source file the document was ingested from. Absent when the
            underlying file is gone.
          example: application/pdf
        size:
          type: integer
          description: File size in bytes
          example: 42
        status:
          type: string
          enum:
            - pending
            - processing
            - ready
            - failed
            - withdrawn
          description: Ingestion lifecycle state. `pending` — enqueued; `processing` — chunks being extracted
            and embedded; `ready` — fully indexed; `failed` — processing error (see the `error`
            field on `GET /documents/{id}/status`); `withdrawn` — taken out of listings and search,
            with its chunks dropped, and restorable from an earlier version.
          example: ready
        version:
          type: integer
          description: The document's content version, starting at 1. Every write that changes the content or
            its annotations archives the state it replaced and increments this.
          example: 1
        content:
          type: string
          nullable: true
          description: Text content (only present on getDocument, and only when status is ready)
          example: The quick brown fox jumps over the lazy dog.
        chunk_strategy:
          type: string
          enum:
            - page
            - whole
            - size
          description: The chunk strategy the document was last (re-)ingested with. Absent when the default
            (`whole`) was used — the mapper omits the key rather than sending `null`.
          example: size
        chunk_size:
          type: integer
          description: Window size in characters used when `chunk_strategy=size`. Absent otherwise.
          example: 800
        chunk_overlap:
          type: integer
          description: Overlap in characters between consecutive windows used when `chunk_strategy=size`.
            Absent otherwise.
          example: 120
        relations:
          type: array
          description: The typed edges this document asserts about others, on a single document read. Absent
            from a listing, which is a page of documents rather than of edges.
          items:
            $ref: "#/components/schemas/DocumentRelation"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    DocumentRelation:
      type: object
      description: A typed edge one document asserts about another, within one project.
      properties:
        id:
          type: string
          example: doc_rel_V1StGXR8Z5jdHi6B
        type:
          type: string
          enum:
            - derived_from
            - supersedes
            - cites
          description: "What the asserting document claims: it was `derived_from` the other, `supersedes` it,
            or `cites` it."
          example: cites
        from_document_id:
          type: string
          description: The document that asserts the edge
          example: doc_V1StGXR8Z5jdHi6B
        to_document_id:
          type: string
          description: The document the edge points at
          example: doc_9Kp2mQxZ7bT4rN6W
        created_at:
          type: string
          format: date-time
    IngestedDocumentRecord:
      allOf:
        - $ref: "#/components/schemas/DocumentRecord"
        - type: object
          properties:
            chunk_count:
              type: integer
              description: Number of chunks created from the file.
              example: 10
    DocumentStatusRecord:
      type: object
      properties:
        id:
          type: string
          description: Document ID
          example: doc_V1StGXR8Z5jdHi6B
        status:
          type: string
          enum:
            - pending
            - processing
            - ready
            - failed
          description: Ingestion lifecycle state.
          example: ready
        chunk_count:
          type: integer
          description: Number of chunks **currently indexed** for this document (a live count). Grows while
            `status=processing` and equals the final total once `ready`; `0` while `pending`.
          example: 10
        total_chunks:
          type: integer
          nullable: true
          description: Planned total number of chunks, known once chunking begins. `null` until then. Used as
            the denominator for `progress`.
          example: 12
        total_pages:
          type: integer
          nullable: true
          description: Number of source pages extracted. Only known after extraction, so it is `null` until
            `status` is `ready` or `failed` (not the same as zero pages).
          example: 12
        progress:
          type: integer
          nullable: true
          description: Ingestion progress as a percentage (`chunk_count / total_chunks`). `0` while `pending`,
            climbs while `processing` (capped at 99), `100` when `ready`, and `null` when `failed`
            or not yet computable.
          example: 100
        error:
          type: string
          nullable: true
          description: Failure reason when `status` is `failed` (e.g. `FILE_PARSE_FAILED`, `INGESTION_TIMEOUT`).
          example: INGESTION_TIMEOUT
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          description: Structured error. Every error response uses this shape, so `code` can be read without
            first checking the type of `error`.
          required:
            - code
            - message
          properties:
            code:
              type: string
              description: "Machine-readable error code: lower_snake for an error naturali raises
                (`access_denied`), UPPER_SNAKE for one the runtime reports (`RESOURCE_NOT_FOUND`)."
              example: access_denied
            message:
              type: string
              description: Human-readable explanation.
              example: Your role in this project does not carry this action.
            details:
              type: object
              additionalProperties: true
              description: Structured context for an error naturali raises, such as the `resource` and `limit` of
                a `plan_limit_reached`.
            meta:
              type: object
              description: Structured context for an error the runtime reports.
    MetadataBag:
      type: object
      description: >-
        Caller-owned annotations on a resource, stored as the object they were written as: the types
        a value was written with are the types a read returns, so a filter can ask an ordering
        question about a number. Unlike other body fields, keys are stored and returned verbatim in
        the casing supplied — they are not converted between snake_case and camelCase.


        No key is reserved, and that is the point: every piece of state the platform owns lives in
        its own typed column, so nothing written here reaches platform state. The platform never
        reads the bag — it is not an IAM context, not a policy input and not part of a prompt —
        which is what separates it from a tag bag.
      example:
        author: John
        revision: 2
    TagBag:
      type: object
      additionalProperties:
        type: string
      x-cli-flag-name: tags
      description: >-
        Key-value labels on a resource. A flat object of string values — an array, a nested object
        or a number is rejected with `400 VALIDATION_FAILED`, never coerced. Keys are opaque and
        stored verbatim, so `cost_center` and `costCenter` are two different tags. Matched by JSONB
        containment wherever tags are read: the `?tags=` filter and knowledge search.


        Keys beginning `system.` are reserved: the platform writes them to record which
        conversation, actor, agent and role a row came from, and a write naming one is refused with
        `400 RESERVED_TAG_KEY`. They are read and filtered like any other tag.


        The bag is bounded, because every pair reaches the IAM context of every access check on the
        resource: at most 50 keys, each key at most 128 characters and each value at most 256. A
        write past a bound — including a merge that would grow the stored bag past the key count —
        is `400 VALIDATION_FAILED` with `meta.limit` naming the bound it crossed. `system.*` keys
        are the platform's and do not count against the 50.
      example:
        team: finance
        env: prod
    NullableMetadataBag:
      type: object
      nullable: true
      description: A `MetadataBag` on a field where `null` is meaningful — a full-replacement update that
        clears the bag, or a record whose bag was never set.
      example:
        author: John
        revision: 2
    ExpectedVersion:
      type: integer
      minimum: 1
      nullable: true
      description: >-
        The version the caller believes the resource holds. When the resource is at any other
        version the write is refused with `409 VERSION_CONFLICT` and nothing is written;
        `meta.current_version` on that response names the version in force.


        Omit it to write unconditionally. Omitting it does not make the write unordered: two writes
        that reach the server together are still serialized, and the one whose version was taken
        first is refused the same way. What the field adds is refusing a write whose author read the
        resource some time ago and has not seen what happened since.


        The `If-Match` header carries the same precondition for a client that prefers the HTTP
        spelling. Sending both with different versions is `400 VALIDATION_FAILED`.
      example: 3
  parameters:
    ProjectId:
      name: project_id
      in: path
      required: true
      description: Project public ID (proj_ prefix).
      schema:
        type: string
        example: proj_V1StGXR8Z5jdHi6B
    TagsQuery:
      name: tags
      in: query
      required: false
      description: >
        Filter by tag pairs, written `key:value` (split on the first colon, so a value may contain
        colons). Repeat the parameter for several pairs; **all** must be present with exactly that
        value.
      schema:
        type: array
        items:
          type: string
      style: form
      explode: true
      example:
        - env:prod
    MetadataQuery:
      name: metadata
      in: query
      required: false
      description: A `MetadataFilter` as JSON, url-encoded. It travels as JSON rather than as `key:value`
        pairs because the match is exact and a query string cannot otherwise say whether `3` is the
        number or the string.
      schema:
        type: string
      example: '{"quarter":"Q1","revision":{"gte":3}}'
    IfMatchVersion:
      name: If-Match
      in: header
      required: false
      description: The version the caller believes the resource holds, as an entity tag (`3` or `"3"`).
        Equivalent to `expected_version` in the request body; `*` states no precondition beyond the
        resource existing. A mismatch is `409 VERSION_CONFLICT`.
      schema:
        type: string
      example: "3"
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: A naturali API key (nat_sk_…) or a session JWT.
    oauth2:
      type: oauth2
      description: "A connected app's OAuth access token, issued by this API's authorization server
        (discovery: /.well-known/oauth-authorization-server). Its one scope carries every operation,
        confined to the projects the user chose when approving the app."
      flows:
        authorizationCode:
          authorizationUrl: https://api.naturali.ai/authorize
          tokenUrl: https://api.naturali.ai/token
          refreshUrl: https://api.naturali.ai/token
          scopes:
            mcp:access: Every operation this API serves, on the projects the grant covers.
