Skip to main content

Files

The bytes a project works from — uploaded, listed, tagged, downloaded, deleted.

Overview​

A file is stored content plus the metadata needed to find it again: a path within the project, a MIME type, a size, and tags. It is the raw layer under the retrieval stack — a document is created from a file, and it is where a trace parks its serialized steps.

Uploads come in two shapes. multipart/form-data is the one to use for real files: the bytes stream through as they are. Base64 is the one to use from a client that cannot send multipart — an MCP tool call, a shell one-liner — at the cost of roughly a third more bytes on the wire. Downloads mirror that split.

This module is a verbatim mirror of the runtime: every field, method, status code and error shape is the runtime's own, re-rooted under the project in the path.

A plan caps how much your account stores

Free stores 1 GB, Pro 30 GB, Business 100 GB; an Enterprise ceiling is set by contract. Uploading past that answers 403 plan_limit_reached with resource: "storage", and the plan, the limit and what your account is holding as storage_gb in details.

The ceiling is your account's, not each project's: every project the account pays for draws on the same figure, so one project may hold all of it. Read where you stand from storage_gb and storage_limit_gb on GET /v1/users/me/billing; the usage route below reports one project at a time.

The figure counts indexed storage — uploaded files plus the chunks and embedding vectors of anything ingested from them — so a file that backs a document counts for more than its own size. See Documents for how that works, and read your current figure from the gb_day component of GET /v1/projects/{project_id}/usage under meter_type=storage.

Downloading, updating metadata and deleting stay open at the ceiling; only uploads are refused.

See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Files.

Data Model​

FileRecord​

FieldTypeDescription
idstringPublic file ID (file_ prefix).
prefixstringThe directory part of path. Read-only in the response; set it with prefix on upload.
filenamestringOriginal / download name, and the last segment of path.
pathstring, nullableprefix + / + filename (e.g. /images/logo.png). Read-only, and unique per project — the file's identity.
content_typestring, nullableMIME type.
sizeinteger, nullableSize in bytes.
metadatastring, nullableA JSON string, not an object — see Metadata is a string here.
tagsobjectKey-value tags, values are strings.
created_atstring (date-time)
updated_atstring (date-time)

Key Concepts​

Two ways to upload, two ways to download​

POST /v1/projects/{project_id}/files/upload takes multipart/form-data with the bytes in a file part, plus optional prefix, filename and metadata parts. Nothing is re-encoded on the way through, so this is the form to use for anything large or binary.

POST /v1/projects/{project_id}/files/upload/base64 takes JSON with the content in content. Every client can produce it, which is why it exists — the CLI and the MCP tool surface use it, as does Answer from your documents — but base64 inflates the payload by about 33%, so prefer multipart when the client can.

Downloading is the same choice: GET /v1/projects/{project_id}/files/{file_id}/download answers the raw bytes under the file's own content type, with content-disposition carrying the filename; GET /v1/projects/{project_id}/files/{file_id}/download/base64 answers JSON.

The raw download is not an MCP tool

A tool result is a JSON value, and this route's is a stream of bytes, so the raw download is deliberately absent from the MCP tool surface. The base64 download is the callable form and returns the same content.

Creating a record without bytes​

POST /v1/projects/{project_id}/files registers a file record — prefix, filename, content_type, size, metadata — with no content attached. It is for the case where the bytes arrive by some other route and only the entry is needed. If you have the bytes, upload them: one call instead of two, and the record is consistent with what was actually stored.

Metadata is a string here​

metadata on a file is a JSON string, unlike the metadata object on most other resources. Send it serialized ('{"author":"Ada"}') and parse it on the way out. The value is stored verbatim, so keys keep the casing you wrote them in.

Tags are a separate, first-class surface: read them with GET /v1/projects/{project_id}/files/{file_id}/tags, replace the whole set with PUT /v1/projects/{project_id}/files/{file_id}/tags, or merge into it with PATCH /v1/projects/{project_id}/files/{file_id}/tags. PATCH only ever adds or overwrites the keys you send; PUT drops the ones you leave out.

Who may do what​

Every route needs any project member.

Examples​

Upload a file​

naturali upload-file-base64 \
--project-id proj_V1StGXR8Z5jdHi6B \
--content "$(base64 -w0 handbook.md)" \
--filename handbook.md \
--prefix /docs \
--content-type text/markdown

List the project's files​

naturali list-files \
--project-id proj_V1StGXR8Z5jdHi6B \
--limit 20

Download the content​

naturali download-file-base64 \
--project-id proj_V1StGXR8Z5jdHi6B \
--file-id file_V1StGXR8Z5jdHi6B

Delete a file​

naturali delete-file \
--project-id proj_V1StGXR8Z5jdHi6B \
--file-id file_V1StGXR8Z5jdHi6B