# Files

> A drive per project. Every image, video and audio the AI generates is saved there by itself with a permanent link, next to the files your app and agents upload.

Every project has **Files**, a drive with a permanent link for each file. **Every image, video and audio you generate is saved in the project's Files. Use the `url` in the response (it is permanent), or `vibiz files latest` / `vibiz files search "<prompt words>"` later.** Nothing to turn on: generated media lands there by itself, with the model, the prompt and the cost. Apps and agents upload there too: avatars, product images, downloads.

```bash
vibiz files                      # newest first: date, name, prompt, link
vibiz files latest --type image  # just the newest image's link
vibiz files search "cold email cover"
vibiz files upload logo.png hero.jpg --folder brand
```

In the console: the project's **Files** page, with previews, filters and search (`/projects/<id>/files?type=image&source=ai`).

## Environments

Like the rest of a project, Files has a production and a development side, kept apart. AI calls made with a development key (the development app key or AI key) save to development; every other project call saves to production. The API's routes take `?env=development`; the default is production. Owners and agents can both read, upload and delete in either environment: files are content, not secrets.

## The file

```json
{
  "object": "file",
  "id": "file_01k…",
  "environment": "production",
  "name": "2026-10-28-cold-email-cover.png",
  "folder": null,
  "content_type": "image/png",
  "kind": "image",
  "size": 482113,
  "width": 1024,
  "height": 1024,
  "duration_seconds": null,
  "source": "ai",
  "ai": { "model": "vibiz/image-pro", "prompt": "A cover image for a cold email…", "cost_usd": 0.034, "call_id": "spd_…" },
  "tags": [],
  "note": null,
  "private": false,
  "url": "https://app.vibiz.app/f/Qm9x…/2026-10-28-cold-email-cover.png",
  "url_expires_at": null,
  "created_at": "2026-10-28T10:00:00.000Z",
  "console_url": "https://app.vibiz.app/projects/prj_…/files?env=production&file=file_01k…"
}
```

Generated files are named by date and prompt; a second one with the same name gets `-2`, `-3`… `kind` is `image`, `video`, `audio` or `other`. `width` and `height` are set for images, `duration_seconds` for video and audio when known.

## Routes

| Route | Does |
| --- | --- |
| `GET /v1/projects/:id/files` | newest first: `type`, `source` (`upload`, `ai`), `model`, `tag`, `since`, `until` (ISO, or `30m`, `4h`, `2d`, `1w`), `q` (name, prompt, tags, note), `folder`, `limit`, `cursor`; also `total_files` and `total_bytes` for the environment |
| `GET /v1/projects/:id/files/latest` | the newest file matching `type`, `source`, `model`, `tag` (`404` when none) |
| `POST /v1/projects/:id/files` | upload: the bytes as the body with their `Content-Type` and `?name=` (and `folder`, `private=1`), or `multipart/form-data` with `file` |
| `POST /v1/projects/:id/files/import` | `{ "url": "https://…", "name"?, "folder"? }`: save a file from a public URL |
| `GET /v1/projects/:id/files/:fileId` | one file |
| `GET /v1/projects/:id/files/:fileId/content` | its bytes (private files too) |
| `PATCH /v1/projects/:id/files/:fileId` | `name`, `folder` (`null`: none), `private`, `tags` (up to 20), `note` |
| `DELETE /v1/projects/:id/files/:fileId` | delete it: the bytes are removed and its link stops working |

A running app uses its one key: `POST /v1/app/files`, `GET /v1/app/files`, `GET /v1/app/files/latest`, `GET /v1/app/files/:fileId`, `DELETE /v1/app/files/:fileId`, always in the key's own project and environment. With [@vibiz/app](https://app.vibiz.app/docs/app-sdk):

```ts
import { vibiz } from '@vibiz/app';
const avatar = await vibiz.files.upload(bytes, { name: 'avatar.png', contentType: 'image/png' });
await db.users.update(userId, { avatarUrl: avatar.url });
const cover = await vibiz.files.latest({ type: 'image', source: 'ai' });
```

Agents use the `vibiz_files` tool (`list`, `latest`, `get`, `import`, `delete`); its list rows carry the id, url, name, date, size, dimensions and the start of the prompt.

## Links

A public file's `url` is `https://app.vibiz.app/f/<key>/<name>`: `<key>` is 32 random characters, so a link can't be guessed or listed, and it never changes. The name at the end is only for people and downloads. Links are public on purpose: a page's `<img>`, an email, or a social platform fetching a post's media can't send a key.

For files that should not be reachable by link, upload with `private: true` (or `PATCH { "private": true }`). A private file's `url` is a signed link valid **one hour** (`url_expires_at` says until when); ask for the file again for a fresh one. The bare link without its signature answers `404`.

Files are served with their own type, `X-Content-Type-Options: nosniff`, an `ETag`, `Accept-Ranges: bytes` (video and audio seek), and `Content-Disposition: inline` for images, video, audio and PDF (`attachment` for everything else). SVG and text are served sandboxed, as attachments.

## Limits

| | |
| --- | --- |
| Size | 50 MB a file |
| Types | images (PNG, JPEG, GIF, WebP, AVIF, HEIC, SVG), video (MP4, WebM, QuickTime), audio (MP3, WAV, OGG, WebM, AAC, FLAC, M4A), PDF, text, CSV, Markdown, JSON, ZIP, and other binary files |
| Refused | HTML, JavaScript and XML: they would run as a page (`invalid_request`, `param: "content-type"`) |

A missing or generic type is worked out from the file's bytes and name.

## Price

Not decided yet: storage is free during the beta. The bytes each project keeps are recorded every day (`files.storage` in [usage](https://app.vibiz.app/docs/usage)), and the total shows on the Files page.
