---
title: "Ingest a file into a chunked document"
method: POST
path: "/v1/projects/{project_id}/documents/ingest"
tags: ["Documents"]
---

# Ingest a file into a chunked document

`POST /v1/projects/{project_id}/documents/ingest`

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/documents/ingest`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://api.naturali.ai/authorize`, token URL `https://api.naturali.ai/token`, scopes: `mcp:access`)

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.

## Query parameters

- `wait` boolean

## Request body

- object
  - `file_id` string, required: ID of the uploaded file. Must be one of application/pdf, text/plain, text/markdown.
  - `path_prefix` string: Path prefix under which to store the document (e.g. /docs/). The filename is appended automatically.
  - `tags` TagBag: 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.
  - `chunk_strategy` 'page' | 'whole' | 'size': 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.
  - `chunk_size` integer: Window size in characters when `chunk_strategy=size`. Defaults to 1000.
  - `chunk_overlap` integer: Overlap in characters between consecutive windows when `chunk_strategy=size`. Defaults to 200.
  - `session_id` string: Session the ingest runs for. The converter's generation and the chunk embeddings are metered against the session and its actor.
  - `actor_id` string: Actor the ingest runs for when there is no session; metered like `session_id`. Beside `session_id` it must be that session's actor, else `400 VALIDATION_FAILED`.

## Response `201`

Ingestion completed synchronously (only when `?wait=true`). The document is fully indexed and ready for search.

- IngestedDocumentRecord
  - `id` string: Document ID
  - `file_id` string: Underlying file ID
  - `project_id` string: Project ID
  - `path` string, nullable: Logical path of the document within the project (e.g. /reports/q1.txt)
  - `filename` string: Original filename
  - `content_type` string: Media type of the source file the document was ingested from. Absent when the underlying file is gone.
  - `size` integer: File size in bytes
  - `status` 'pending' | 'processing' | 'ready' | 'failed' | 'withdrawn': 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.
  - `version` integer: 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.
  - `content` string, nullable: Text content (only present on getDocument, and only when status is ready)
  - `chunk_strategy` 'page' | 'whole' | 'size': 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`.
  - `chunk_size` integer: Window size in characters used when `chunk_strategy=size`. Absent otherwise.
  - `chunk_overlap` integer: Overlap in characters between consecutive windows used when `chunk_strategy=size`. Absent otherwise.
  - `created_at` string, date-time
  - `updated_at` string, date-time
  - `chunk_count` integer: Number of chunks created from the file.

## Other responses

- `202`: 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`.
- `400`: Invalid request, file not found, unsupported content type, or an unknown `session_id` (`SESSION_NOT_FOUND`) or `actor_id` (`ACTOR_NOT_FOUND`) in the project
- `401`: Unauthorized
- `403`: Forbidden
- `409`: 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).
- `413`: The file is too large to ingest synchronously (`?wait=true`). Retry in background mode and poll the document status.

---

[API](https://skmtc.dev/naturali/apis/naturali-ai-api.md) · [All operations](https://skmtc.dev/naturali/apis/naturali-ai-api/llms.txt) · [OpenAPI document](https://skmtc.dev/naturali/apis/naturali-ai-api/revisions/e4905b78f422?raw)
