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.
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 brandIn 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
{
"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:
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), and the total shows on the Files page.