Vaze
API Reference

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/files

Returns 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/files

Send a multipart/form-data body:

FieldTypeDescription
filesfileOne or more files to upload (repeat the field for multiple files)
folderstringOptional target folder path, e.g. projects/demo. Nested folders are created automatically. Defaults to the root folder.
visibilitystringOptional, public or private. Defaults to the instance's default visibility, set under Settings in the dashboard (public unless changed). 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.

StatusCause
409A file with that name already exists in the target folder
413A file exceeded the maximum upload size set under Settings, or too many files in one request
507Not enough free space on the volume

Report canceled uploads

POST /api/files/canceled

An upload aborted partway through its request is recorded as canceled by the upload route itself. Files a client drops from its own queue before sending never reach the server, so report their names to keep the activity history complete. Needs the files:write scope.

{ "names": ["holiday.mov", "notes.txt"] }
{ "data": null, "error": null }

Rename a file or change its visibility

PUT /api/files

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

Move files

POST /api/files/move

JSON body, with the destination given as either folderId or folder:

{ "id": "<file-id>", "folder": "projects/archive" }
FieldTypeDescription
idstringThe file to move. Send this or ids.
idsstring[]Several files to move into the same folder. See below.
folderIdstringThe destination folder's id. Responds 404 if it doesn't exist.
folderstringThe destination folder's key, created (with any missing parents) like an upload's folder field. "" is the root.

A moved file keeps its id, name and visibility; its key and url change to the new location, which breaks any public link or signed URL pointing at the old one. The response carries the file as it is now:

{ "data": { "file": { "key": "projects/archive/photo.png", ... } }, "error": null }
StatusMeaning
400Missing id or destination, or an invalid folder key
404No such file, or no folder with that folderId
409A file or folder with the same name already exists in the destination

Moving a file into the folder it is already in succeeds without changing anything.

Move several files

Send ids instead of id to move a batch into one folder:

{ "ids": ["<file-id>", "<file-id>"], "folderId": "<folder-id>" }

Each file is moved on its own, so a name clash or a bad id does not stop the rest. The response lists the files that were moved and the ids that were not:

{
  "data": {
    "files": [{ "id": "<file-id>", "key": "projects/archive/a.png", ... }],
    "failed": [{ "id": "<file-id>", "message": "\"b.png\" already exists in the destination folder" }]
  },
  "error": null
}

Responds 404 only when none of the ids exist, and 400 for an empty list or more than MAX_MOVE_BATCH (default 500) ids.

Sign a URL

POST /api/files/sign

Mints 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 }
FieldTypeDescription
idstringThe file id. Either this or key is required.
keystringThe object key. Either this or id is required.
expiresInnumberSeconds the link stays valid. Defaults to the instance's default signed link lifetime and is clamped to its maximum, both set under Settings.
{
  "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/files

JSON 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"

Download several files as a zip

POST /api/files/download

JSON body:

{ "ids": ["<file-id>", "<other-id>"] }

Streams one zip containing every requested file, private ones included. Each entry keeps its path below the deepest folder the files share: files picked from one folder sit at the top of the archive, while a selection spanning projects/web and projects/api keeps web/ and api/ apart. The archive is named after that shared folder (projects.zip), or files.zip when the files only share the root.

curl -OJ https://files.example.com/api/files/download \
  -H "API-Key: your-api-key" \
  -H "Content-Type: application/json" \
  -d '{"ids": ["<file-id>", "<other-id>"]}'

Every id must resolve: an unknown id, or a file whose bytes are missing from disk, responds 404 and no archive is sent. Duplicate ids are collapsed, and more than MAX_DOWNLOAD_BATCH (default 1000) ids responds 400. The ids may also be sent as repeated ids form fields (application/x-www-form-urlencoded), so a plain HTML form can start the download.

Entries are stored uncompressed, which keeps the server's CPU idle and lets the response carry an exact Content-Length. Archives over 4 GB use ZIP64. The zip is generated per request, so unlike a single-file download it cannot be resumed with a byte range.

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) from POST /api/files/sign,
  • an API-Key header,
  • 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.

Each day's requests, bytes sent and misses are counted for the dashboard's hosting traffic, leaving out requests made from a signed-in dashboard or app session. See Stats.

Public hosting URLs are exactly that — anyone who knows a file's key can fetch it. Upload with visibility: private, or make new files private by default under Settings in the dashboard, 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
RequestResult
Range: bytes=0-499206, first 500 bytes
Range: bytes=500-206, byte 500 to the end
Range: bytes=-500206, final 500 bytes
Start past end of file416 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 body

If-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=<public cache lifetime>, must-revalidate, defaulting to max-age=0 — always revalidate, which is cheap because a match costs a bodiless 304. Raise the public cache lifetime under Settings 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.

On this page