VorcaroFiles
private downloads
New upload
Public API reference

API documentation

Programmatic access to VorcaroFiles. Two HTTP surfaces plus a discovery endpoint — everything you need to upload, list, download and rotate files without the browser UI.

Discovery
/api/v1/config
Allowed TTLs, max upload size, download cap options. Start here when building a client.
Uploads
/upload
Single-request streamed upload (raw body + header metadata).
JSON API
/api/v1/
Metadata, listing, folder management.
Downloads
/d/{file_id}
HEAD probe, POST stream, GET HTML page (browser-only).
Auth model: the folder URL is its own credential — anyone with /v/{folder_id} can list/modify files in that folder. If a folder has an additional password, send it in the X-Folder-Password HTTP request header on every JSON call (it's a header, not a query param or body field). For uploads to a locked folder, exchange the password for an unlock_token via POST /api/v1/folders/{id}/unlock and send that token in the X-Unlock-Token upload header. Direct file downloads via /d/{file_id} are always open — the file ID alone is the credential. There is no "list my folders" endpoint — clients are responsible for keeping the IDs they create.
Encryption is server-side, not end-to-end. When you set encrypt=true, the file is encrypted at rest using the password, and the server decrypts it on download. Passwords are never stored, but they ARE transmitted to the server. If you need true end-to-end privacy, encrypt the file on the client before uploading and treat the password as out-of-band metadata.

Conventions

Defaults applied to every endpoint. The rest of this page assumes them.

Encoding
All strings are UTF-8. Upload metadata headers that may carry non-ASCII (X-Filename, X-Password) are URL-encoded (percent-encoded) UTF-8.
Timestamps
expires_at and similar Unix-seconds fields are UTC. *_iso mirrors are ISO-8601 UTC (2026-05-22T18:00:00Z).
created_day
Integer: days since 1970-01-01 UTC. To convert to Unix seconds, multiply by 86400.
Cache
JSON responses use Cache-Control: no-store, except /api/v1/config (public, max-age=60). Binary downloads (HEAD/POST /d/{id}) use private, no-store — bytes won't be cached by CDNs or shared proxies.
Errors
JSON endpoints return {"error": "human msg", "code": "stable_code"}. The stable code is safe to switch on; the message is for humans.
Relative URLs
download_url and folder_url are relative paths (/d/..., /v/...). Combine with config.base_url for absolute URLs.
CORS
Not enabled. Browser-based cross-origin clients won't work — call from a backend or extension context.
Filenames
Non-ASCII names in downloads are encoded as Content-Disposition: attachment; filename*=UTF-8''<percent-encoded> (RFC 5987).
Folder auth
Always sent as the HTTP request header X-Folder-Password: <password>. Never as a query param or request body field.
Nosniff
Download responses (GET/HEAD/POST /d/{id}) always set X-Content-Type-Options: nosniff.

Meta endpoints

Discovery and health. Always public, no auth.

get/healthzliveness probe

Always returns 200 OK with a fixed JSON payload. Use it for load balancer / monitoring health checks.

Response — 200 OK

{ "status": "ok", "service": "masterfiles" }

The endpoint does not touch the database. A 200 only means the HTTP handler is alive — not that downloads will succeed.

get/api/v1/configserver limits

Returns the values a client needs to construct a valid upload — without hardcoding any settings. Cached for 60 seconds.

Response — 200 OK

{
  "site_domain": "files.example.com",
  "base_url": "https://files.example.com",
  "allow_anonymous_upload": true,
  "max_upload_bytes": 5368709120,         // 0 = unlimited
  "default_ttl_hours": 168,
  "allowed_ttl_hours": [1, 24, 72, 168, 720, 8760, 0],
  "download_cap_options": [0, 1, 3, 5, 10, 25, 100],
  "empty_folder_days": 7,
  "folder_unlock_ttl_hours": 12,
  "upload_protocol": "single-request/1.0",
  "upload_endpoint": "/upload"
}

Pin client behavior to this response. The accepted X-Ttl-Hours and X-Max-Downloads values in upload headers come from allowed_ttl_hours and download_cap_options here. 0 in those arrays means "never expires" / "unlimited downloads" respectively, when those values are allowed.

Upload

One streamed POST /upload: the request body is the raw file, and every option travels in request headers. No chunking, no handshake, no resumable session — the server writes the bytes straight to storage as they arrive.

post/uploadupload file

Send the file bytes as the body (application/octet-stream). The server validates, optionally encrypts inline, stores the file, and (optionally) creates a new folder.

Required headers

nametypedescription
Content-Lengthint64Total body size in bytes (must be ≤ max_upload_bytes for non-premium folders)
X-FilenamestringURL-encoded (percent-encoded) original file name. Defaults to untitled.bin if absent.

Optional metadata headers

Filenames are anonymized by default. Uploading report-Q3.pdf stores it as e3a4f9b1...c7.pdf (random 32-hex, original extension preserved if short + alphanumeric). To keep the original name, send X-Anonymize: false.
headertypedescription
X-Content-TypestringMIME type (e.g. image/png). Defaults to application/octet-stream.
X-Encryptbooltrue to encrypt the file server-side with X-Password
X-PasswordstringURL-encoded. Required when X-Encrypt=true. Never stored. Transmitted to the server (not E2E).
X-Ttl-HoursintLifetime in hours. 0 = never. Strictly validated against config.allowed_ttl_hours — any other value is rejected with 400.
X-Max-DownloadsintDelete after this many successful downloads. 0 or absent = unlimited. config.download_cap_options is the catalog for UIs; arbitrary positive integers are accepted by the API.
X-AnonymizeboolDefault true. Replaces the stored filename with a random 32-hex string, preserving the extension if it's short and alphanumeric. Send false (or 0) to keep the original name.
X-Folder-IdstringAttach to an existing folder. Omit to create a new one.
X-Unlock-TokenstringFor password-protected folders. Obtain via POST /api/v1/folders/{id}/unlock (JSON response includes the token).
403 conditions: (a) anonymous uploads are disabled server-wide via upload.allow_anonymous=false, OR (b) the target folder is password-protected and no valid unlock cookie / X-Unlock-Token was sent.

Response — 201 Created

JSON body plus these headers (header / JSON key):

header / json keydescription
X-File-Id / file_idPublic file ID (used in /d/<id>)
X-Folder-Id / folder_idParent folder ID
X-Folder-New / folder_newtrue if a new folder was created
X-Download-Url / download_urlDownload URL — absolute (https://<domain>/d/...) when site.domain is configured, otherwise relative (/d/...)
X-Folder-Url / folder_urlFolder admin URL — same absolute/relative rule. Keep secret.
expires_at / expires_isoUnix seconds + ISO-8601 UTC expiration

Other responses

  • 400 — invalid metadata header, X-Ttl-Hours not in config.allowed_ttl_hours, empty file, or body length did not match Content-Length
  • 403 — anonymous disabled or folder locked (see above)
  • 411 — missing Content-Length
  • 413 — body exceeds upload.max_size_bytes (response includes X-Max-Size-Bytes)

Folder API

Manage folder content programmatically. All responses are application/json.

No password set? Any holder of the folder ID can call these endpoints.
Password set? Send the password as the HTTP request header X-Folder-Password: <password> on every request. Wrong/missing password → 401.
Repeated failed attempts trigger throttling on every password-checked endpoint (GET, DELETE, password change, unlock) — the server returns 429 with a Retry-After header (seconds).
About premium: some folders are flagged premium by an admin (manual, no payment / no self-serve API). Premium folders: (1) are never auto-deleted when empty; (2) bypass upload.max_size_bytes; (3) accept ttl_hours=0 as "never expires" — for non-premium, ttl_hours=0 silently falls back to config.default_ttl_hours (e.g. 168h). Read FolderResource.premium to know which side you're on. There is no public endpoint to toggle this.
get/api/v1/folders/{id}folder + file list

Returns folder metadata and the list of non-deleted files inside.

Response — 200 OK

{
  "id": "3kQ...",
  "created_day": 20596,
  "file_count": 2,
  "total_bytes": 2048,
  "premium": false,
  "password_set": true,
  "password_set_day": 20597,             // omitted when password_set=false
  "empty_folder_deletes_at": 1780012800,    // only when empty + non-premium + cleanup enabled
  "empty_folder_deletes_iso": "2026-05-29T00:00:00Z",
  "files": [ /* array of file objects */ ]
}

empty_folder_deletes_at is omitted when the folder is premium, non-empty, or empty-folder cleanup is disabled (config.empty_folder_days = 0).

delete/api/v1/folders/{id}delete folder

Synchronous hard delete — no soft-delete window, no recovery. Files are removed from disk and the folder row is deleted before the response returns.

{
  "deleted": true,
  "folder_id": "3kQ...",
  "files_deleted": 2
}
post/api/v1/folders/{id}/unlockexchange password → token

Programmatic alternative to the browser unlock flow. Returns an unlock_token you can send in the X-Unlock-Token header to upload to a locked folder. Same token format as the browser mf_v_<folder_id> cookie. Lifetime matches config.folder_unlock_ttl_hours.

Request body

{ "password": "hunter2" }

Response — 200 OK

{
  "unlock_token": "qm9...32 url-safe chars",
  "folder_id": "3kQ...",
  "expires_at": 1779451200,
  "expires_iso": "2026-05-22T06:00:00Z"
}

Other responses

  • 400 — missing/malformed body, or the folder has no password set
  • 401 — wrong password
  • 404 — folder not found
  • 429 — rate-limited (shared with all other password-checked endpoints on this folder)
delete/api/v1/folders/{id}/files/{file_id}delete a file

Synchronous hard delete. Removes the file from disk and the DB row before the response returns. The file_id must belong to the given folder, otherwise 404.

{
  "deleted": true,
  "file_id": "V5pY...",
  "folder_id": "3kQ..."
}
post/api/v1/folders/{id}/files/{file_id}/regeneratenew download URL

Generates a new public file ID. The old /d/{old_file_id} URL stops working immediately.

The underlying encrypted blob is not moved or re-encrypted — only the URL token rotates.

Rate limit: 1 regeneration per hour per file. Returns 429 Too Many Requests otherwise.

Response — 200 OK

{
  "file_id": "newPublicId...",
  "old_file_id": "oldPublicId...",
  "download_url": "/d/newPublicId..."
}
post/api/v1/folders/{id}/passwordset / change / remove

Manages the folder password (Argon2id, irrecoverable). To change an existing password, you still send the current one in X-Folder-Password.

Request body

{ "new_password": "hunter2" }  // or "" to remove

Minimum 4 characters. Removing the password makes the folder publicly accessible again (still requires the folder ID).

Response — 200 OK

{
  "password_set": true,           // false if new_password was ""
  "folder_id": "3kQ..."
}

File API

Lookup file metadata by its public ID. No password needed — the file ID itself is the credential.

get/api/v1/files/{id}metadata

Returns metadata only (no binary). Use POST /d/{id} to download the bytes or HEAD /d/{id} for a lighter probe.

Response — 200 OK

{
  "id": "V5pY...",
  "original_name": "file.txt",
  "content_type": "text/plain",
  "size_bytes": 1024,
  "encrypted": true,
  "cipher": "age-v1",
  "download_count": 0,
  "max_downloads": 5,                // omitted when unlimited
  "expires_at": 1779494400,
  "expires_iso": "2026-05-22T18:00:00Z",
  "created_day": 20596,
  "download_url": "/d/V5pY...",
  "regenerated_at": 1779408000      // Unix seconds of last URL rotation; omitted if never rotated
}

Lifetime — first event wins

A file is removed (turns into 404 on download) when any of these triggers fires first:

  • expires_at reached
  • download_count >= max_downloads after a successful download
  • Explicit delete via Folder API
  • Parent folder deleted

download_count increments after a successful byte stream from POST /d/{id}. Aborted downloads don't count. HEAD /d/{id} never increments.

Note: The parent folder_id is intentionally not exposed here. Anyone with the file ID can fetch metadata, but they should not learn which folder it belongs to.

Download

For programmatic downloads, use POST — it streams the raw bytes as Content-Disposition: attachment. The GET variant returns an HTML page meant for browsers and is not considered API. Use HEAD to probe a file's status without downloading or incrementing the counter. All download responses include X-Content-Type-Options: nosniff.

head/d/{file_id}probe

Returns the same status as POST (200 / 404) plus metadata headers. Does not decrypt, does not require a password, does not increment download_count.

Response — 200 OK

headerdescription
Content-TypeOriginal MIME type
Content-LengthPlaintext size — only when file is not encrypted
Content-Dispositionattachment; filename*=UTF-8''<percent-encoded>
X-File-Size-BytesPlaintext size (always present)
X-Encryptedtrue or false
X-Download-CountCurrent download count
X-Max-DownloadsCap value — omitted when unlimited
X-Expires-AtUnix seconds — omitted when never expires
post/d/{file_id}stream bytes

Request body (optional)

Form field password is required when the file is encrypted, ignored otherwise.

password=hunter2

Response

  • 200 OK — binary stream of the file. After a successful stream, download_count increments by 1; if it reaches max_downloads, the file is removed.
  • 401 Unauthorized — missing or wrong password
  • 404 Not Found — file expired, deleted, download cap reached, or unknown

Example

curl -X POST "https://files.example.com/d/V5pY..." \
    -d "password=hunter2" \
    -o file.txt

End-to-end example

Discovery → upload → list → rotate URL → download. Replace files.example.com with your domain.

1 · Discover limits

curl -s "https://files.example.com/api/v1/config"
# → use allowed_ttl_hours, max_upload_bytes, etc. to validate before uploading

2 · Upload the file

# The body IS the file. Metadata goes in headers.
# X-Filename and X-Password are URL-encoded (percent-encoded).
curl -i -X POST "https://files.example.com/upload" \
    -H "Content-Type: application/octet-stream" \
    -H "X-Filename: file.txt" \
    -H "X-Encrypt: true" \
    -H "X-Password: hunter2" \
    -H "X-Ttl-Hours: 168" \
    -H "X-Max-Downloads: 5" \
    --data-binary @file.txt

# → 201 Created + JSON body:
# {
#   "file_id": "V5pY...",
#   "folder_id": "3kQ...",
#   "folder_new": true,
#   "original_name": "file.txt",
#   "encrypted": true,
#   "size_bytes": 1024,
#   "folder_url": "https://files.example.com/v/3kQ...",
#   "download_url": "https://files.example.com/d/V5pY...",
#   "expires_at": 1779494400,
#   "expires_iso": "2026-05-22T18:00:00Z"
# }

3 · List the folder later

curl -s "https://files.example.com/api/v1/folders/<folder_id>"
# If the folder is password-protected, add the request HEADER:
curl -s "https://files.example.com/api/v1/folders/<folder_id>" \
    -H "X-Folder-Password: hunter2"

3b · Upload to a password-protected folder (programmatic)

# Exchange password for an unlock_token:
curl -s -X POST "https://files.example.com/api/v1/folders/<folder_id>/unlock" \
    -H "Content-Type: application/json" \
    -d '{"password":"hunter2"}'
# → { "unlock_token": "qm9...", "expires_at": ... }

# Then send it on the upload:
curl -i -X POST "https://files.example.com/upload" \
    -H "X-Filename: file.txt" \
    -H "X-Folder-Id: <folder_id>" \
    -H "X-Unlock-Token: qm9..." \
    --data-binary @file.txt

4 · Rotate the download URL

curl -X POST "https://files.example.com/api/v1/folders/<folder_id>/files/<file_id>/regenerate"
# → { "file_id": "newId...", "old_file_id": "oldId...", "download_url": "/d/newId..." }
# The old /d/oldId... URL now returns 404.

5 · Download

curl -X POST "https://files.example.com/d/<file_id>" \
    -d "password=hunter2" \
    -o file.txt
# download_count increments after the stream completes.

Errors

All JSON endpoints return errors as {"error": "human msg", "code": "stable_code"}. Switch on code, not on the message text.

HTTP status codes

statusmeaning
400Invalid form data, malformed headers, missing required field, or ttl_hours outside the allowed whitelist
401Wrong or missing password (file encryption or folder password)
403Anonymous uploads disabled OR folder locked and no valid unlock cookie / token
404Resource not found, expired, deleted, or download cap reached
405HTTP method not allowed on this endpoint
413Upload exceeds upload.max_size_bytes (non-premium folders only). Response includes X-Max-Size-Bytes.
421Misdirected request — host header doesn't match the configured domain. Can be emitted by ANY route.
429Rate limited — folder password attempts (all password-checked endpoints) or /regenerate (1/hour per file). Retry-After header included.
500Internal server error. Can be emitted by ANY route.

Stable code values

codemeaning
bad_requestMissing or malformed input
password_requiredFolder password header missing
wrong_passwordFolder password didn't verify
no_passwordTried to unlock a folder that has no password
password_too_shortNew folder password has fewer than 4 characters
not_foundGeneric "doesn't exist or you can't see it"
expiredFile past expires_at
rate_limitedThrottled — check Retry-After
internalServer-side bug — safe to retry once after a backoff