Files
File endpoints of the REST API
All endpoints below require the API-Key
header unless noted otherwise.
File objects in responses have this shape:
{
"id": "5f2d4a1e-...",
"name": "photo.png",
"key": "projects/demo/photo.png",
"mimeType": "image/png",
"size": 102400,
"folderId": "9c8b7a6d-...",
"visibility": "public",
"url": "api/hosting/projects/demo/photo.png",
"createdAt": "2026-07-15 12:00:00",
"updatedAt": "2026-07-15 12:00:00"
}key is the object's path relative to the storage root — it identifies the
file and determines its public URL. url is relative to your instance's base
URL; resolve it against that to get the public hosting link (the SDK does this
for you).
visibility is public or private. url is always the canonical hosting
URL regardless of visibility — for a private file it only works with a
signed URL or valid credentials.
List files
GET /api/filesReturns every file on the instance. Supports the list query parameters.
curl "https://files.example.com/api/files?limit=20&orderBy=size&orderDirection=DESC" \
-H "API-Key: your-api-key"{ "data": { "files": [ ... ] }, "error": null }Get a file
GET /api/files?id=<file-id>
GET /api/files?key=<object-key>Fetch a single file by its id or by its key:
curl "https://files.example.com/api/files?key=projects/demo/photo.png" \
-H "API-Key: your-api-key"{ "data": { "file": { ... } }, "error": null }Responds 404 when no file matches.
Search files by name
GET /api/files?name=<file-name>Matches the exact file name across every folder — useful because the same
name can now exist in more than one folder. Also supports the list query
parameters. An empty result is a success with an empty files array.
Upload files
POST /api/filesSend a multipart/form-data body:
| Field | Type | Description |
|---|---|---|
files | file | One or more files to upload (repeat the field for multiple files) |
folder | string | Optional target folder path, e.g. projects/demo. Nested folders are created automatically. Defaults to the root folder. |
visibility | string | Optional, public or private. Defaults to DEFAULT_FILE_VISIBILITY (public unless configured otherwise). Applies to every file in the request. |
curl -X POST https://files.example.com/api/files \
-H "API-Key: your-api-key" \
-F "folder=projects/demo" \
-F "files=@./photo.png" \
-F "files=@./notes.txt"{ "data": { "files": [ ... ] }, "error": null }Files are stored under the name you upload them with, so you control the
public URL. Names are unique per folder: the same name in two different
folders is fine, but uploading over an existing name responds 409.
The request body is streamed straight to disk, so upload size is bounded by your volume rather than by memory. Uploads land in a staging area and are moved into place only once the whole request has been received — if anything fails partway through, nothing appears in the target folder.
| Status | Cause |
|---|---|
409 | A file with that name already exists in the target folder |
413 | A file exceeded MAX_UPLOAD_SIZE, or too many files in one request |
507 | Not enough free space on the volume |
Rename a file or change its visibility
PUT /api/filesJSON body — name, visibility, or both. At least one is required, or the
request responds 400:
{ "id": "<file-id>", "name": "new-name.png", "visibility": "private" }The name must be a single path segment — separators and .. respond 400.
Renaming onto a name already used in the same folder responds 409. An
unrecognised visibility responds 400.
Changing visibility alone never touches the bytes on disk. Renaming changes the key, which invalidates any signed URL previously minted for the old key.
Sign a URL
POST /api/files/signMints a time-limited URL that reads a file without an API key — the way to hand
a private object to a browser, an <img> tag, or a third party.
JSON body:
{ "key": "projects/demo/report.pdf", "expiresIn": 3600 }| Field | Type | Description |
|---|---|---|
id | string | The file id. Either this or key is required. |
key | string | The object key. Either this or id is required. |
expiresIn | number | Seconds the link stays valid. Defaults to DEFAULT_PRESIGN_TTL_SECONDS, clamped to MAX_PRESIGN_TTL_SECONDS. |
{
"data": {
"url": "api/hosting/projects/demo/report.pdf?exp=1786291200&sig=...",
"expiresAt": "2026-08-09T12:00:00.000Z"
},
"error": null
}Like url on a file object, the returned url is relative to your instance's
base URL. Signing a public file is allowed; the link simply also works unsigned.
Signatures are stateless, so an individual link cannot be revoked. Rotating
AUTH_SECRET invalidates every outstanding signed URL at once — that is the
only way to withdraw them early.
Delete a file
DELETE /api/filesJSON body:
{ "id": "<file-id>" }Removes the file from disk and the database. Responds 404 if no such file
exists; a file already missing from disk still has its record cleaned up and
reports success.
Delete several files
Send ids instead of id to delete a batch in one request:
{ "ids": ["<file-id>", "<file-id>"] }Each id is handled on its own, so one bad id does not stop the rest. The response lists both outcomes:
{
"data": {
"deleted": ["<file-id>"],
"failed": [{ "id": "<file-id>", "message": "File not found" }]
},
"error": null
}Responds 404 only when none of the ids exist, and 400 for an empty list or
more than MAX_DELETE_BATCH (default 500) ids.
Download a file
GET /api/files/download/<file-id>Streams the raw file with Content-Disposition: attachment, so it works
directly as a download link in authenticated contexts.
curl -OJ https://files.example.com/api/files/download/<file-id> \
-H "API-Key: your-api-key"Supports byte ranges and conditional requests, so an interrupted download can be resumed rather than restarted:
curl -C - -OJ https://files.example.com/api/files/download/<file-id> \
-H "API-Key: your-api-key"Serve a file
GET /api/hosting/<object-key>Streams the file inline with the correct Content-Type, which makes it
suitable for embedding in web pages:
<img src="https://files.example.com/api/hosting/projects/demo/photo.png" />This is the URL carried in the url field of file objects and shown as the
"hosting URL" in the dashboard.
Public files need no authentication. A private file is served only to a caller that presents one of:
- a valid signature (
exp+sig) fromPOST /api/files/sign, - an
API-Keyheader, - a dashboard session cookie.
Anything else responds 404 — the same response as a key that does not exist,
so the endpoint cannot be used to discover which private keys are real. Private
responses also carry Cache-Control: private, no-store so a proxy or CDN in
front of Vaze cannot retain them.
Add ?download=1 to serve with Content-Disposition: attachment instead of
inline. It grants no extra access and is not part of the signature, so it can be
appended to a signed URL.
Hosted responses are sent with Content-Security-Policy: sandbox and
X-Content-Type-Options: nosniff. Assets render normally; uploaded HTML and
SVG render without executing scripts, so hosted content can never act against
your instance with your session.
Public hosting URLs are exactly that — anyone who knows a file's key can fetch
it. Upload with visibility: private, or set DEFAULT_FILE_VISIBILITY=private
on the instance, for anything you don't want reachable by URL.
Range requests and caching
Both /api/hosting/<key> and /api/files/download/<id> are range-aware and
revalidatable. Every response carries Accept-Ranges: bytes.
Byte ranges
A Range header returns 206 Partial Content with a Content-Range. This is
what lets a browser seek in an audio or video file — Safari in particular
refuses to play a video at all from a server that can't serve ranges.
curl -r 0-1023 https://files.example.com/api/hosting/clips/demo.mp4| Request | Result |
|---|---|
Range: bytes=0-499 | 206, first 500 bytes |
Range: bytes=500- | 206, byte 500 to the end |
Range: bytes=-500 | 206, final 500 bytes |
| Start past end of file | 416 with Content-Range: bytes */<size> |
An end past the last byte is clamped rather than rejected. Multi-range requests
(bytes=0-9,20-29) are answered with the complete file — a server may always
decline to honour a range.
Conditional requests
Public responses carry an ETag and Last-Modified derived from the file's
size and modification time. Send either back to skip the transfer:
curl -H 'If-None-Match: "a-6899f2c1"' \
https://files.example.com/api/hosting/projects/demo/photo.png
# → 304 Not Modified, no bodyIf-None-Match takes precedence over If-Modified-Since when both are sent.
If-Range is honoured with strong comparison: if the validator is stale, the
whole file is returned rather than a range that would corrupt the cached copy.
Cache-Control on public responses is
public, max-age=<HOSTING_CACHE_MAX_AGE>, must-revalidate, defaulting to
max-age=0 — always revalidate, which is cheap because a match costs a bodiless
304. Raise HOSTING_CACHE_MAX_AGE when keys on your instance are effectively
immutable.
Private files are sent Cache-Control: private, no-store and carry no
validators — offering a revalidation path would contradict no-store. Byte
ranges still work, so seeking in a private video behaves normally.
Vaze