> ## 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 and published artifacts.

**Files** is Vetta's object storage. Use it to upload inputs an [agent](/docs/api/agents) can read and to hold artifacts an agent publishes. 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>

## 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`, or [publishing](#publish-a-file) a session file. An agent's `publish_file` tool writes a **session**-scoped file: promote it with `POST /v1/files/{id}/publish` to keep it. |

<Note>
  Anything an agent should keep beyond a run is promoted to a **published** file. Scratch work stays session-scoped. Promotion emits no [event](/docs/api/events) — the event stream carries session activity only, so read the file back if you need to confirm it.
</Note>

## Getting bytes out of a session

Files travel **outward**: 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[]` — and there is no `read_file` tool; the built-in toolset is `bash`, `read`, `write`, `edit`, `ls`, `find`, `browser`, `read_skill`, `publish_file`, `web_search`, `web_fetch`, `generate_image` and `generate_video`, and the file tools address the sandbox filesystem, not file ids. To put an input in front of an agent today, put it in the session's first `message` or have the agent fetch it.

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

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

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

## 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. Filter with `?scope=` and `?session_id=`. See [Pagination](/docs/api/pagination).

```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); `insufficient_scope` (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 }
```
