> ## Documentation Index
> Fetch the complete documentation index at: https://vetta.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Files

> Naive Managed Agents object storage for uploads, imports and published artifacts — the asset library.

**Files** is Naive Managed Agents' object storage and its media asset library. Use it to upload inputs an [agent](https://vetta.sh/docs/api/agents.md) can read, to hold artifacts an agent publishes, and to find them again by `kind`, `tag` or a word from the title. Files are either **session-scoped** (temporary, tied to a [session](https://vetta.sh/docs/api/sessions.md)) or **published/persistent** (retained at the organization level).

## The file object

**id** `string`

Unique id (e.g. `fil_01H...`).

**object** `string`

Always `file`.

**name** `string`

File name.

**content\_type** `string`

MIME type.

**size\_bytes** `integer`

Size in bytes.

**scope** `string`

`session` (temporary) or `published` (persistent).

**session\_id** `string | null`

Owning session, for session-scoped files.

**sha256** `string`

Content hash.

**created\_at** `string`

Upload timestamp.

**kind** `string`

Derived from `content_type`: `image`, `video`, `audio`, `document`, `model` (3D) or `other`.

**title** `string | null`

A human title, editable.

**description** `string | null`

Free text, editable and searchable.

**tags** `string[]`

Labels to filter by; up to 32, each up to 64 characters.

**folder** `string | null`

The [folder](#folders) the file is in, or `null`.

**source** `object | null`

Where the bytes came from: `{ type, job_id, url }`. `type` is `upload`, `url_import`, `image_generation`, `video_generation`, `video_clipping`, `screenshot`, `speech_generation`, `mesh_generation` (a generated 3D model) or `model_build` (a file `build-3d` published); `job_id` names the `med_`/`clj_` job that produced it, `url` the address an import fetched. A generated picture or clip also carries `model` and `prompt`: what it was made with. An edited clip also carries `from_file_id`: the library clip it was made from, when it was made from one. `null` for files written before provenance was recorded.

**agent\_id** `string | null`

The agent whose session made the file. `null` for an upload, an import, or a render asked for over the API.

**width** `integer | null`

Pixel width, read from the bytes of a PNG, JPEG, GIF, WebP or MP4 at write time.

**height** `integer | null`

Pixel height, likewise.

**duration\_seconds** `number | null`

Length of an audio or video file: as its producer reported it (for [generated speech](https://vetta.sh/docs/api/audio.md)), or read from an MP4's own header at write time. `null` otherwise.

**thumbnail\_file\_id** `string | null`

Reserved for a preview image; always `null` today.

## Session-scoped vs published

| Scope | Lifetime | Created by |
| - | - | - |
| `session` | Lives with the session; cleaned up when the session is deleted. | Uploads to a session, scratch work. |
| `published` | Persistent, org-level. | An upload with `scope: published`, a [URL import](#import-a-file-from-a-url), [publishing](#publish-a-file) a session file, or an agent's `publish_file` tool — which writes a **published** file directly, with the `session_id` kept as provenance, so it outlives the session. |

> [!NOTE]
>
> Anything an agent should keep beyond a run is a **published** file. Scratch work stays session-scoped. Promotion emits no [event](https://vetta.sh/docs/api/events.md); an import emits `file.imported`.

## Getting bytes out of a session

Files travel **outward** by default: an agent writes an artifact with the built-in `publish_file` tool and you read it back through this API. There is no create-time mount — a session takes no `files[]`. Inward, the agent asks: the built-in [`fetch_file`](https://vetta.sh/docs/capabilities/tools#reading-a-file-back-into-the-sandbox) tool takes up to eight `fil_` ids the organization holds and writes those files into the session's sandbox, which is how an agent post-processes an asset the platform rendered for it. The sandbox file tools themselves still address the sandbox filesystem and not file ids — there is no `read_file` — and `find_files` still returns ids and metadata, never bytes. To put an input in front of an agent, name its `fil_` id in the session's first `message` and let it fetch the bytes, or put the content in the message itself.

Each `publish_file` call mints a **new** file id, including for a name that already exists: nothing is superseded and both remain listed. Pick the one you want by `created_at`, or give it a `title` and `tags` and find it by those.

## Upload a file

`POST /v1/files` → `201 Created`. Accepts a multipart upload. The `file` part carries the bytes; other parts set metadata.

**file** `file` *required*

The file contents (multipart).

**scope** `string`

`session` or `published`. Defaults to `published`.

**session\_id** `string`

Required when `scope` is `session`.

**name** `string`

Override the file name.

**title** `string`

A human title for the library.

**description** `string`

A longer description, searchable.

**tags** `string`

Comma-separated labels, e.g. `launch,web`.

**folder** `string`

Put the file in this [folder](#folders).

The body is capped at **25 MiB**. The stored `kind` is derived from the content type, and an image's `width`/`height` are read from its bytes. `source` is `{ "type": "upload" }`.

```bash cURL theme={"system"}
curl -fsSL https://api.vetta.sh/v1/files \
  -H "authorization: Bearer sk_live_..." \
  -H "idempotency-key: $(uuidgen)" \
  -F "file=@invoice.pdf" \
  -F "scope=published"
```

```json Response theme={"system"}
{
  "id": "fil_01H9CD...",
  "object": "file",
  "name": "invoice.pdf",
  "content_type": "application/pdf",
  "size_bytes": 20481,
  "scope": "published",
  "session_id": null,
  "sha256": "e3b0c442...",
  "created_at": "2026-08-20T17:20:00Z",
  "kind": "document",
  "title": null,
  "description": null,
  "tags": [],
  "source": { "type": "upload", "job_id": null, "url": null },
  "width": null,
  "height": null,
  "duration_seconds": null,
  "thumbnail_file_id": null
}
```

## Upload up to 60 MiB with a link

`POST /v1/files/uploads` → `201 Created`. Scope `files:write`. Mints a one-shot link; the bytes go to it in a second request that needs no key, so a browser can send them straight from the page.

**name** `string` *required*

The file name.

**content\_type** `string` *required*

A MIME type, e.g. `video/mp4`. The PUT must send the same.

**size\_bytes** `integer` *required*

The exact size, at most **60 MiB**.

**folder** `string`

Put the file in this [folder](#folders) when it lands.

```json Response theme={"system"}
{
  "object": "file_upload",
  "file_id": "fil_01H9CF...",
  "upload_url": "https://api.vetta.sh/v1/files/uploads/eyJv...",
  "expires_at": "2026-10-13T12:15:00Z"
}
```

`PUT /v1/files/uploads/{token}` → `201 Created`, with the [file object](#the-file-object). No key: the token is the proof. Send the bytes as the body within 15 minutes, with the declared `Content-Type` and a `Content-Length` of exactly `size_bytes`; anything else is `validation_failed`. The link works once. A used, expired or altered link is `forbidden`. The file is `published`, with `source.type: "upload"`. The route answers CORS, so any page may call it.

```bash theme={"system"}
curl -fsSL -X PUT "$UPLOAD_URL" -H "content-type: video/mp4" --data-binary @clip.mp4
```

The mint is audit-logged as `file.created`.

## Import a file from a URL

`POST /v1/files/imports` → `201 Created`. Scope `files:write`. Requires an `Idempotency-Key` header: the same key replays the same file instead of fetching twice.

The API fetches the URL **server-side** and stores the bytes as a `published` file with `source.type: "url_import"` and `source.url` set to the address you gave. The stored `content_type` is what the bytes say (PNG, JPEG, GIF, WebP, MP4, WebM, MP3, WAV, Ogg, PDF are recognised by signature), falling back to what the server declared.

**url** `string` *required*

A public `http(s)` URL.

**title** `string`

A human title for the library.

**description** `string`

A longer description, searchable.

**tags** `string[]`

Labels to filter by.

**folder** `string`

Put the file in this [folder](#folders).

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/files/imports \
  -H "authorization: Bearer sk_live_..." \
  -H "idempotency-key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d '{ "url": "https://cdn.example.com/hero.png", "title": "Launch hero", "tags": ["launch"] }'
```

```json Response theme={"system"}
{
  "id": "fil_01H9CE...",
  "object": "file",
  "name": "hero.png",
  "content_type": "image/png",
  "kind": "image",
  "width": 1600,
  "height": 900,
  "title": "Launch hero",
  "tags": ["launch"],
  "scope": "published",
  "source": { "type": "url_import", "job_id": null, "url": "https://cdn.example.com/hero.png" }
}
```

**Limits.** Only `http` and `https`; no credentials in the URL; the host must be public — loopback, private-network, link-local and cloud-metadata addresses are refused, and every redirect is checked the same way before it is followed (three at most). The body is capped at **25 MiB**, the same as an upload; a larger declared or actual body fails the import. All of these, and an upstream error status, are `validation_failed` (400) on `url`.

The import is audit-logged and emits a `file.imported { file_id, url }` [event](https://vetta.sh/docs/api/events.md) to webhooks subscribed to it.

## Update a file's metadata

`PATCH /v1/files/{id}` → `200 OK` with the updated file. Scope `files:write`. Only the library metadata changes; the bytes, name, scope and provenance do not.

**title** `string | null`

Set or clear (`null`) the title.

**description** `string | null`

Set or clear the description.

**tags** `string[]`

Replace the tags; `[]` clears them.

**folder** `string | null`

Move the file to this [folder](#folders); `null` takes it out.

```bash theme={"system"}
curl -fsSL -X PATCH https://api.vetta.sh/v1/files/fil_01H9CE... \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "title": "Launch hero (final)", "tags": ["launch", "approved"] }'
```

Any other field in the body is `validation_failed` (400).

## Retrieve a file

`GET /v1/files/{id}` → `200 OK` with file metadata. Append `?download=true` to receive the raw bytes instead of JSON.

The bytes carry an `ETag` (the file's `sha256`) and honour `Range`, `If-Range` and `If-None-Match`; a copy you already hold is answered `304 Not Modified`. A generated image or video is sent `Cache-Control: private, no-cache`, every other file `private, max-age=31536000, immutable`.

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/files/fil_01H9CD... \
  -H "authorization: Bearer sk_live_..."
```

```json Response theme={"system"}
{ "id": "fil_01H9CD...", "name": "invoice.pdf", "scope": "published", "size_bytes": 20481 }
```

## A link that needs no key

`POST /v1/files/{id}/url` → `201 Created`. Scope `files:read`.

A file id is not something a person can watch. `?download=true` above wants an `authorization`
header, and an `<img>` or `<video>` tag sends none — so this mints an address that carries its own
proof instead, good for **seven days**.

```bash theme={"system"}
curl -fsSL -X POST https://api.vetta.sh/v1/files/fil_01H9CD.../url \
  -H "authorization: Bearer sk_live_..."
```

```json Response theme={"system"}
{
  "object": "file_url",
  "url": "https://api.vetta.sh/v1/files/fil_01H9CD.../clip.mp4?org=org_01H9...&expires=1757212800000&sig=...",
  "expires_at": "2026-09-14T04:00:00.000Z"
}
```

`GET /v1/files/{id}/{name}?org=&expires=&sig=` — that URL, with no credential at all — returns the bytes, `inline`, with the content type they
were stored under. The filename is in the path so the address ends in a real extension, which is what
lets a browser tag and a fetch-once publishing provider recognise it before opening it.

The signature covers every other field in the URL, so there is nothing in it you can edit: change the
expiry, the file, the organization or the name and it answers `403`. A generated picture's or clip's
link also carries `in=media`, which is signed too. A forged link and an expired one
answer the same thing. Deleting the file revokes every link ever minted for it.

## List files

`GET /v1/files` → `200 OK`, cursor-paginated, newest first. See [Pagination](https://vetta.sh/docs/api/pagination.md). Filters combine:

| Query | Matches |
| - | - |
| `scope` | `session` or `published`. |
| `session_id` | Files a given session produced. |
| `kind` | `image`, `video`, `audio`, `document`, `model` or `other` — or several, comma-separated. |
| `exclude_kind` | None of these kinds, comma-separated. |
| `agent_id` | Files an agent's sessions made. |
| `ids` | Exactly these files: up to 100 `fil_` ids, comma-separated. |
| `source` | A `source.type` — `upload`, `url_import`, `image_generation`, … |
| `tag` | Files carrying exactly this tag. |
| `search` | A case-insensitive substring of the `title`, `name` or `description` — or, for a generated picture or clip, of the prompt it was made from. |
| `order` | `desc` (newest first, the default) or `asc`. |
| `folder` | Files in this [folder](#folders). Case does not matter. |

Working pictures a skill build cuts from its source videos are left out of every list except an `ids` one. Each is still read by its id.

Everything an agent's pictures and clips need is one call: `?kind=image,video&agent_id=agt_…`.

```bash theme={"system"}
curl -fsSL "https://api.vetta.sh/v1/files?kind=image,video&agent_id=agt_01H9CD...&limit=30" \
  -H "authorization: Bearer sk_live_..."
```

```bash theme={"system"}
curl -fsSL "https://api.vetta.sh/v1/files?kind=image&tag=launch&search=hero" \
  -H "authorization: Bearer sk_live_..."
```

```json Response theme={"system"}
{
  "data": [ { "id": "fil_01H9CD...", "name": "invoice.pdf", "scope": "published", "size_bytes": 20481 } ],
  "has_more": false,
  "next_cursor": null
}
```

## Folders

A folder is a name on a file. A file is in one folder or none, and folders do not nest. A name is 1–64 characters with no `/`. There is no folder object to create: a folder exists while a file is in it.

* **Case.** A name that matches an existing folder in another case takes that folder's spelling. `launch` files into `Launch`.
* **Default.** A file written with no folder lands in the folder of the newest filed file in its session, else in none. So an agent's next render lands beside its last.
* **Once.** The folder is set when a file is written. Writing the same file again never changes it; only a move does.

Grouping by agent, chat, kind or date is a filter on the list, never a folder.

### Move files

`POST /v1/files/move` → `200 OK`. Scope `files:write`. Name `ids` or `from`, not both.

**ids** `string[]`

Up to 100 `fil_` ids to move. An id the organization does not hold is skipped.

**from** `string`

Move every file in this folder: a rename. If `folder` is already in use, the two merge.

**folder** `string | null` *required*

The folder to move to. `null` takes the files out of any folder; with `from`, that empties the folder.

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/files/move \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "from": "launch", "folder": "Launch 2026" }'
```

```json Response theme={"system"}
{ "object": "file_move", "folder": "Launch 2026", "file_ids": ["fil_01H9CD...", "fil_01H9CE..."] }
```

`folder` is the name the files now carry, in the spelling that won. Only the name on each file changes. No file is deleted and no bytes move.

### List folders

`GET /v1/files/folders` → `200 OK`. Scope `files:read`. Every folder, newest first, in one page.

```json Response theme={"system"}
{
  "object": "list",
  "data": [
    {
      "object": "file_folder",
      "name": "Launch 2026",
      "file_count": 12,
      "newest_at": "2026-10-20T09:00:00.000Z",
      "cover_file_ids": ["fil_01H9CD...", "fil_01H9CE..."],
      "agent_id": "agt_01H9AB..."
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

`cover_file_ids` holds up to four of the newest pictures and clips in the folder. `agent_id` is set when one agent made every file in it, else `null`. All of it is worked out when you read it. To read a folder's files, list with `?folder=`.

## Publish a file

`POST /v1/files/{id}/publish` → `200 OK`. Scope `files:write`.

The promotion boundary: a `session`-scoped file becomes a durable, org-level `published` one. Use it when work that started as scratch turns out to be the deliverable — it saves re-uploading the bytes.

**Idempotent.** Publishing an already-published file returns it unchanged rather than erroring, so a retry is safe. No request body.

```bash theme={"system"}
curl -fsSL -X POST https://api.vetta.sh/v1/files/fil_01H9CD.../publish \
  -H "authorization: Bearer sk_live_..."
```

```json Response theme={"system"}
{ "id": "fil_01H9CD...", "object": "file", "name": "invoice.pdf", "scope": "published", "size_bytes": 20481 }
```

The file now outlives its session and is billed as org-level storage. No event is emitted.

**Errors** — `not_found` (404); `forbidden` (403) without `files:write`.

## Delete a file

`DELETE /v1/files/{id}` → `200 OK`. Removes the object.

```json Response theme={"system"}
{ "id": "fil_01H9CD...", "object": "file", "deleted": true }
```

## Delete many files

`POST /v1/files/delete` → `200 OK`. Scope `files:write`. Deletes up to 100 files in one request, each as `DELETE /v1/files/{id}` would. An id the organization does not hold is skipped. Each delete is filed as `file.deleted` in the audit log.

**ids** `string[]` *required*

1–100 `fil_` ids to delete.

```bash theme={"system"}
curl -fsSL https://api.vetta.sh/v1/files/delete \
  -H "authorization: Bearer sk_live_..." \
  -H "content-type: application/json" \
  -d '{ "ids": ["fil_01H9CD...", "fil_01H9CE..."] }'
```

```json Response theme={"system"}
{ "object": "file_delete", "file_ids": ["fil_01H9CD...", "fil_01H9CE..."] }
```

`file_ids` lists the files that were deleted.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.