---
title: "Create a document"
method: POST
path: "/v1/projects/{project_id}/documents"
tags: ["Documents"]
---

# Create a document

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

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/documents`
- 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`)

Creates a new text document and generates an embedding vector for semantic search, in the project named in the path.

## Request body

- object
  - `content` string, required
  - `path` string: 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.
  - `filename` string
  - `title` string: Document title
  - `metadata` MetadataBag: 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.
  - `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 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.
  - `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.

## Response `201`

Document created

- DocumentRecord
  - `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

## Other responses

- `400`: Invalid request body
- `401`: Unauthorized
- `403`: Forbidden
- `409`: `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.

---

[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)
