# Files API

The files API uploads and deletes workspace files, and downloads them through permanent tokenized URLs or expiring signed URLs returned in file and entry payloads.

## Upload a file

```
POST /api/v1/{workspace}/files
```

Send one file per request using `multipart/form-data`. The API key needs the `files:write` ability (or `*`), and its creator needs the workspace's `create-file` permission. No browser session or CSRF token is required. Enable **Upload files** when creating or editing the key in **Settings → API Keys**.

```shell
curl "https://app.example.com/api/v1/acme/files" \
  -H "Authorization: Bearer $VIRESSO_API_KEY" \
  -H "Accept: application/json" \
  -F "file=@./photo.png"
```

The response is `201 Created` with the file object shown below, without a `data` wrapper. Use its `id` in an [entry attachment field](/docs/api/entries#attachments-and-relations). Uploads also appear in the workspace's file library. Each request uses the workspace in the URL.

Do not set the multipart `Content-Type` manually: curl or `FormData` must include its boundary. See [Code Examples](/docs/api/code-examples#upload-a-file) for Laravel and Node.js uploads.

JSON clients can send a base64-encoded file instead, with `Content-Type: application/json`:

```json
{
  "file": {
    "name": "hello.txt",
    "type": "text/plain",
    "content": "SGVsbG8="
  }
}
```

Multipart uploads support files up to **250 MiB (262,144,000 bytes)** by default, including `.exe`, `.dmg`, and `.apk` installers. The platform upload settings can restrict the permitted types and size. Installer validation checks the filename extension and detected content type, including binary EXE/DMG and ZIP-based APK formats; the client-supplied MIME type is not trusted. Compressed `.dmg` files detected as `application/zlib` must also have the UDIF `koly` signature at the start of their final 512-byte trailer. Files are streamed to storage and served as downloads.

JSON uploads retain a hard limit of 150 MiB decoded, even when the platform limit is higher. Use multipart for larger files. Invalid or missing files return `422` with `errors.file`. URL imports are not supported by this endpoint.

PHP and any reverse proxy must also allow the request. The project's `php.ini` sets `upload_max_filesize` and `post_max_size` to `1G`, leaving room for multipart overhead; `php artisan serve` applies these limits automatically. For production PHP/FrankenPHP, load equivalent settings and configure any proxy body-size limit above 250 MiB. A request rejected before reaching Laravel may return `413`. Existing installations must run `php artisan migrate` to upgrade saved defaults; custom administrator restrictions are preserved.

Uploads are limited to 60 requests per minute per API key by default, separately from downloads. Configure `RATE_LIMIT_FILE_UPLOADS_PER_MINUTE` to change this; local development limits are disabled unless enabled explicitly.

Successful API uploads add the stored file's byte size plus the outgoing response size to workspace bandwidth usage. Base64 uploads count decoded file bytes; multipart and base64 encoding overhead are excluded. Request logs keep these separately as `upload_size` and `response_size`. Rejected uploads contribute only their response bytes. Storage usage is tracked separately, and metrics appear after queued jobs are processed. These upload metrics apply to this API endpoint, not manager or direct-to-S3 uploads.

## Read file details or refresh a temporary URL

```
GET /api/v1/{workspace}/files/{file}
```

Returns the file object directly, including fresh download and image links with their expiry timestamps (`temporary_url`, `temporary_url_expires_at`, `temporary_image_url`, and `temporary_image_url_expires_at`). Use the integer file ID from an upload or entry attachment. The API key needs `files:read` (or `*`), and its creator needs `view-file` permission. Enable **Read files** in **Settings → API Keys**. Upload-only keys need this additional ability to refresh links through this endpoint. Origin restrictions and workspace suspension still apply; foreign-workspace, user-owned, and missing files return `404`. Responses are private and non-cacheable.

```shell
curl "https://app.example.com/api/v1/acme/files/18" \
  -H "Authorization: Bearer $VIRESSO_API_KEY" \
  -H "Accept: application/json"
```

### Choose a URL lifetime

Add `?expires_in=120` to the file-details request to issue both temporary URLs for two minutes.
`expires_in` is an optional integer duration in seconds, from **30 to 600** inclusive.
Omitting it keeps the **600-second (10-minute) default**. Empty, non-integer, and out-of-range
values return `422` with `errors.expires_in`; the API does not silently clamp them.

```shell
curl "https://app.example.com/api/v1/acme/files/18?expires_in=120" \
  -H "Authorization: Bearer $VIRESSO_API_KEY" \
  -H "Accept: application/json"
```

Viresso computes expiry using its server clock. Both returned expiry timestamps match the
requested lifetime. Upload and entry responses retain their 10-minute default. Apply
`expires_in` only when requesting file details: adding it to an issued signed URL invalidates
the signature. Shorter lifetimes do not add early revocation or retract downloaded copies.

## Temporary downloads

Share the returned `temporary_url` when a recipient should have a link that expires at `temporary_url_expires_at` (10 minutes by default). The URL is a bearer credential: anyone holding it can download during that period, without an API key. It does not expose the file's permanent access token.

The signed route is `GET /api/v1/{workspace}/files/{file}/temporary?uuid=...&expires=...&signature=...`. Use the complete returned URL unchanged. An expired, missing, or modified signature returns `403`; a deleted file, missing storage object, or workspace mismatch returns `404`. Suspended workspaces return `403`.

```shell
curl --location "$TEMPORARY_URL" --output downloaded-file
```

Local storage streams the file. Storage with temporary URL support redirects with `307` to a storage URL that expires at the **same original timestamp**, rather than granting another 10 minutes. Both paths use private, `no-store` cache directives and download-safe content headers. A storage URL already issued before deletion may remain usable until its expiry. Expiry prevents new requests; it cannot revoke downloaded copies or necessarily stop a transfer already in progress.

`download_url` and `image_url` remain permanent for compatibility. Adding a temporary URL does **not** make those links expire or make the whole file private. Share only `temporary_url` or `temporary_image_url` for expiring access, and fetch file details or its entry again when you need a fresh link. Clients caching API payloads must honor `temporary_url_expires_at`.

File details and temporary downloads use the configured file request rate limit (`RATE_LIMIT_FILES_PER_MINUTE`, 1200 by default). Details are limited per API key; temporary downloads are limited per workspace/file, shared across its signed links.

## Temporary inline images and resizing

Use `temporary_image_url` in an `<img>` element. It points to Viresso's signed endpoint and expires at the timestamp indicated by `temporary_image_url_expires_at`. Both fields are `null` for non-images or files without valid URL credentials.

Viresso verifies access, fetches the image from the existing **images.viresso.com** proxy, and streams that response inline with its image content type. All resizing and format conversion happens at the existing proxy. Viresso does not redirect the browser to the proxy or expose the permanent file token in the temporary link.

Append transform parameters to the returned URL:

```js
const src = new URL(file.temporary_image_url);
src.searchParams.set('width', '640');
src.searchParams.set('height', '360');
src.searchParams.set('fit', 'cover');
src.searchParams.set('format', 'webp');
src.searchParams.set('quality', '80');
document.querySelector('img').src = src.toString();
```

| Parameter | Accepted values |
|---|---|
| `width`, `height` | Integer pixels, 1–4096 |
| `fit` | `contain`, `cover`, `scale-down`, `crop`, `pad` |
| `format` | `auto`, `jpeg`, `png`, `webp`, `avif` |
| `quality` | Integer, 1–100 |

Only these five transform parameters may change without invalidating the signature. The proxy applies its own supported format/transform behavior. The browser's `Accept` header is forwarded for format negotiation. Changing dimensions never extends expiry. Do not change the path, `uuid`, `expires`, or `signature`, or add unrelated query parameters.

Expired/invalid links and suspended workspaces return `403`; deleted or foreign-workspace files return `404`; invalid transform values return `422` (malformed signed query parameters can return `403`). Proxy failures, unexpected redirects, and non-image proxy responses return a generic `502` without exposing upstream error bodies or URLs. Missing proxy images return `404`.

Responses are inline and `private, no-store`, so a new request always passes Viresso's access checks even when the proxy has cached a variant. Image variants and temporary downloads share the per-workspace/file rate limit. Fetch file details or its entry again for a new link. Previously displayed or saved images cannot be revoked by expiry.

The proxy must be able to reach the original file. Temporary image requests use the same remote proxy in local development; local-only files are unavailable unless that existing proxy can reach their source. The existing `image_url` and download behavior remain unchanged.

## Delete a file

```
DELETE /api/v1/{workspace}/files/{file}
```

Use the integer `id` returned by an upload or entry attachment. The API key needs `files:delete` (or `*`), and its creator needs the workspace's `delete-file` permission. Enable **Delete files** in **Settings → API Keys**. The upload ability (`files:write`) alone does not allow deletion. No browser session, CSRF token, or request body is required.

```shell
curl -X DELETE "https://app.example.com/api/v1/acme/files/18" \
  -H "Authorization: Bearer $VIRESSO_API_KEY" \
  -H "Accept: application/json"
```

A successful deletion returns `204 No Content` with an empty body. Deletion permanently removes the file record and its references from attachment fields in every entry locale in the workspace. Single attachments become `null`; multiple attachments lose only the deleted file. Entries and other files remain available. The normal `file.deleted` event and configured webhooks are dispatched.

The deleted file's tokenized download URL returns `404`. Stored content shared with another file record is preserved; unreferenced storage is removed by scheduled pruning. Previously issued temporary storage URLs may remain usable until they expire, and external caches may retain copies.

| Status | Meaning |
|---|---|
| `204` | File deleted; no response body |
| `403` | Invalid key, origin restriction, missing ability or creator permission, or suspended workspace |
| `404` | Missing workspace, invalid file ID, or file not found in the workspace (including already deleted files) |
| `429` | Deletion rate limit exceeded; check `Retry-After` |

Deletions are limited to 60 requests per minute per API key by default, separately from uploads and downloads. Configure `RATE_LIMIT_FILE_DELETES_PER_MINUTE` to change this; local development limits are disabled unless enabled explicitly.

## Download a file

```
GET /api/v1/{workspace}/files/{file}/{uuid}/{token}
```

Downloads a file by its integer ID, UUID, and access token.

| Parameter | Location | Required | Description |
|---|---|---|---|
| `{workspace}` | URL | Yes | Workspace name |
| `{file}` | URL | Yes | File ID (integer) |
| `{uuid}` | URL | Yes | File UUID |
| `{token}` | URL | Yes | File access token |

### Authentication

This endpoint does **not** require an API key. Access is controlled by the tokenized URL and the route workspace. A suspended workspace returns `403 Forbidden`. Otherwise, the API returns `404` if the file does not belong to the workspace, the UUID/token pair is invalid, or the storage object is missing.

### Example

```shell
curl "https://app.example.com/api/v1/acme/files/18/2f5f1b3c-0f66-4a1a-a252-7e49e9c9f3d1/{token}" \
  --output hero.jpg
```

### Response behavior

The response depends on the configured storage disk:

- **Disks without temporary URLs (including the default local disk):** Streams the file directly as `application/octet-stream` with download-safe response headers.
- **Disks with temporary URL support (such as S3):** Redirects to a storage URL signed for 10 minutes. The app refreshes its cached redirect every 5 minutes and marks the redirect response as non-cacheable so clients do not reuse expired storage URLs.

### File response shape

Upload responses, file-detail responses, and files rendered as part of entry responses include:

```json
{
  "id": 18,
  "name": "hero.jpg",
  "mime_type": "image/jpeg",
  "size": 2048576,
  "tags": ["homepage"],
  "download_url": "https://app.example.com/api/v1/acme/files/18/2f5f1b3c-0f66-4a1a-a252-7e49e9c9f3d1/{token}",
  "temporary_url": "https://app.example.com/api/v1/acme/files/18/temporary?uuid=2f5f1b3c-0f66-4a1a-a252-7e49e9c9f3d1&expires=1779271200&signature=...",
  "temporary_url_expires_at": "2026-05-20T10:00:00+00:00",
  "image_url": "https://images.viresso.com/api/v1/acme/files/18/2f5f1b3c-0f66-4a1a-a252-7e49e9c9f3d1/{token}/hero.jpg",
  "temporary_image_url": "https://app.example.com/api/v1/acme/files/18/temporary-image?uuid=2f5f1b3c-0f66-4a1a-a252-7e49e9c9f3d1&expires=1779271200&signature=...",
  "temporary_image_url_expires_at": "2026-05-20T10:00:00+00:00",
  "alt_text": "Hero image",
  "created_at": "2026-05-20T09:30:00.000000Z",
  "updated_at": "2026-05-20T09:45:00.000000Z"
}
```

| Field | Description |
|---|---|
| `id` | Integer file ID |
| `name` | Display name |
| `mime_type` | MIME type (e.g. `image/jpeg`) |
| `size` | File size in bytes |
| `download_url` | Permanent tokenized workspace API download URL |
| `temporary_url` | Signed download URL valid for the requested lifetime (10 minutes by default); no permanent token exposed |
| `temporary_url_expires_at` | ISO 8601 expiry timestamp for `temporary_url`; both are `null` if a URL cannot be issued |
| `image_url` | Permanent image proxy URL for images, or `null` for non-images |
| `temporary_image_url` | Signed Viresso URL streaming the existing proxy inline, with optional resizing; `null` for non-images |
| `temporary_image_url_expires_at` | ISO 8601 expiry of `temporary_image_url` |
| `alt_text` | Alternative text for accessibility |
| `tags` | Free-form labels |

In local mode, image URLs point to the original download; image transforms require the configured image proxy. Image proxy URLs accept optional `width`, `height`, `fit`, `format`, and `quality` query parameters for server-side image variants. The tokenized `download_url` always returns the original file.

## Related pages

- [Files](/docs/content/files) — File management in the manager.
- [API Overview](/docs/api/overview) — Base URL, headers, and error handling.
- [API Authentication](/docs/api/authentication) — API keys and hosted authentication.
