---
title: "Create a file"
method: POST
path: "/v1/projects/{project_id}/files"
tags: ["Files"]
---

# Create a file

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

## Request

- Base URL: `https://api.naturali.ai`
- URL: `https://api.naturali.ai/v1/projects/{project_id}/files`
- 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 file record in the system

## Request body

- object
  - `prefix` string: Directory within the project (e.g. /images). Optional; defaults to / (root). Combined with filename to form the file's key (path).
  - `filename` string: Original / download name and the key's leaf segment (e.g. logo.png).
  - `content_type` string: MIME type of the file
  - `size` integer, nullable: File size in bytes
  - `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.

## Response `201`

File created successfully

- FileRecord: Stored file metadata
  - `id` string: Unique file identifier
  - `prefix` string: Directory of the file (the `path` without its last segment). Read-only — set it via `prefix` on write.
  - `filename` string: Original / download name and the key's leaf segment.
  - `path` string, nullable: Full key of the file within the project — `prefix` + `/` + `filename` (e.g. /images/logo.png). Read-only; unique per project; the file's identity and policy-SRN target.
  - `content_type` string, nullable: MIME type of the file
  - `size` integer, nullable: File size in bytes
  - `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.
  - `created_at` string, date-time: Creation timestamp
  - `updated_at` string, date-time: Last update timestamp

## Other responses

- `409`: 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.
- `500`: Internal server error

---

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