> ## 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

> Vetta object storage for uploads, imports and published artifacts — the asset library.

**Files** is Vetta's object storage and its media asset library. Use it to upload inputs an [agent](/docs/api/agents) 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](/docs/api/sessions)) or **published/persistent** (retained at the organization level).

## The file object

<ResponseField name="id" type="string">Unique id (e.g. `fil_01H...`).</ResponseField>
<ResponseField name="object" type="string">Always `file`.</ResponseField>
<ResponseField name="name" type="string">File name.</ResponseField>
<ResponseField name="content_type" type="string">MIME type.</ResponseField>
<ResponseField name="size_bytes" type="integer">Size in bytes.</ResponseField>
<ResponseField name="scope" type="string">`session` (temporary) or `published` (persistent).</ResponseField>
<ResponseField name="session_id" type="string | null">Owning session, for session-scoped files.</ResponseField>
<ResponseField name="sha256" type="string">Content hash.</ResponseField>
<ResponseField name="created_at" type="string">Upload timestamp.</ResponseField>
<ResponseField name="kind" type="string">Derived from `content_type`: `image`, `video`, `audio`, `document` or `other`.</ResponseField>
<ResponseField name="title" type="string | null">A human title, editable.</ResponseField>
<ResponseField name="description" type="string | null">Free text, editable and searchable.</ResponseField>
<ResponseField name="tags" type="string[]">Labels to filter by; up to 32, each up to 64 characters.</ResponseField>
<ResponseField name="source" type="object | null">Where the bytes came from: `{ type, job_id, url }`. `type` is `upload`, `url_import`, `image_generation`, `video_generation`, `video_clipping`, `screenshot` or `speech_generation`; `job_id` names the `med_`/`clj_` job that produced it, `url` the address an import fetched. `null` for files written before provenance was recorded.</ResponseField>
<ResponseField name="width" type="integer | null">Pixel width, read from the bytes of a PNG, JPEG, GIF or WebP at write time.</ResponseField>
<ResponseField name="height" type="integer | null">Pixel height, likewise.</ResponseField>
<ResponseField name="duration_seconds" type="number | null">Length of an audio or video file, as its producer reported it. Set for [generated speech](/docs/api/audio); `null` for every other writer — the bytes are not probed.</ResponseField>
<ResponseField name="thumbnail_file_id" type="string | null">Reserved for a preview image; always `null` today.</ResponseField>

## 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](/docs/api/events); an import emits `file.imported`.
</Note>

## 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`](/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.

<ParamField body="file" type="file" required>The file contents (multipart).</ParamField>
<ParamField body="scope" type="string">`session` or `published`. Defaults to `published`.</ParamField>
<ParamField body="session_id" type="string">Required when `scope` is `session`.</ParamField>
<ParamField body="name" type="string">Override the file name.</ParamField>
<ParamField body="title" type="string">A human title for the library.</ParamField>
<ParamField body="description" type="string">A longer description, searchable.</ParamField>
<ParamField body="tags" type="string">Comma-separated labels, e.g. `launch,web`.</ParamField>

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" }`.

<CodeGroup>
  ```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"
  ```
</CodeGroup>

<ResponseExample>
  ```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
  }
  ```
</ResponseExample>

## 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.

<ParamField body="url" type="string" required>A public `http(s)` URL.</ParamField>
<ParamField body="title" type="string">A human title for the library.</ParamField>
<ParamField body="description" type="string">A longer description, searchable.</ParamField>
<ParamField body="tags" type="string[]">Labels to filter by.</ParamField>

```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](/docs/api/events) 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.

<ParamField body="title" type="string | null">Set or clear (`null`) the title.</ParamField>
<ParamField body="description" type="string | null">Set or clear the description.</ParamField>
<ParamField body="tags" type="string[]">Replace the tags; `[]` clears them.</ParamField>

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

```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 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](/docs/api/pagination). Filters combine:

| Query        | Matches                                                               |
| ------------ | --------------------------------------------------------------------- |
| `scope`      | `session` or `published`.                                             |
| `session_id` | Files a given session produced.                                       |
| `kind`       | `image`, `video`, `audio`, `document` or `other`.                     |
| `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`. |

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

## 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 }
```
