Use this for the main programmatic download route.
Use Invisible Folder from your app without guessing.
This page only covers the routes you would call from software. Browser account flows, dashboard endpoints, account linking, and internal service-to-service hooks are intentionally left out.
Use this when you want the shortest possible download URL.
This is the route intended for software-driven downloads.
Create and replace one file at a time with explicit access and metadata settings.
Start here
- Primary domain:
https://invisiblefolder.net. - Short domain:
https://invis.zipfor the short download URL format. - For software integrations, use
GET /a/{reference_id}withX-Invisiblefolder-Download: 1. This keeps credentials and download intent out of the URL. - Create one file with
POST /api/v1/files, then replace it later withPOST /api/v1/files/{reference_id}/replace. - General creation requires
files.upload. Replacement is independent and requiresfiles.replace. - List and inspect the files owned by an API key with
GET /api/v1/files,GET /api/v1/files/{reference_id}, and thefiles.readscope. - List upload-ready group references with
GET /api/v1/groupsand thegroups.readscope. - Scoped API keys can read metadata for API Available, Password Protected, and System Locker Simple files. Advanced files use their short-lived access token instead.
- For a specialized small-public-image flow with a separate replacement code, use
POST /api/restricted-image. - This page only covers software-facing access. Account pages, browser session endpoints, System Locker linking, and service-token webhook routes are intentionally omitted.
Access quick reference
| File access mode | What you send | Notes |
|---|---|---|
| Public or hidden | No extra credential | The route can stream the file immediately when the file is allowed to be shared without extra protection. |
| Password-protected | X-Invisiblefolder-Password |
For GET downloads, send it in X-Invisiblefolder-Password. |
| System Locker Simple | X-Systemlocker-Key, X-Systemlocker-Username + X-Systemlocker-Password, or a valid X-Invisiblefolder-Token |
Advanced tokens also download Simple files when they match the file owner's Invisible Folder account and required System Locker system. |
| System Locker Advanced | invisiblefolder_token field or X-Invisiblefolder-Token header |
The token must match the required System Locker system and the file owner's Invisible Folder account. |
Programmatic download route
| Method + path | Purpose | Result shape |
|---|---|---|
GET /a/{reference_id}GET https://invis.zip/{reference_id} |
Cacheable programmatic download endpoint (recommended for apps). | Success: raw file bytes (download attachment). Failure: JSON with error and error_human. |
Set X-Invisiblefolder-Download: 1 to bypass the browser preview. Known service and automation user agents also receive the file directly. GET request bodies are not supported; use the headers below over HTTPS.
GET or POST /a/{reference_id}/download is also available when a client always wants the attachment. That route does not require X-Invisiblefolder-Download.
Credential headers
- Password-protected files:
X-Invisiblefolder-Password. - System Locker Simple:
X-Systemlocker-Key,X-Systemlocker-Username+X-Systemlocker-Password, or a validX-Invisiblefolder-Tokenfrom System Locker Advanced. - System Locker Advanced:
X-Invisiblefolder-Token.
Responses include an ETag based on the reference, filename, and stored revision. Protected files are cached privately and revalidated before reuse; a matching validator returns HTTP 304 without transferring the file again.
Create and replace files
The native v1 write API accepts one file per request. It uses the reference_id returned at creation for later replacements.
| Method + path | Required scope | Result |
|---|---|---|
POST /api/v1/files |
files.upload |
Creates one file and returns HTTP 201. |
POST /api/v1/files/{reference_id}/replace |
files.replace |
Replaces an owned file while retaining its reference and returns HTTP 200. |
GET /api/v1/groups |
groups.read |
Lists owned groups and the public group references accepted by file uploads. |
Send the API key in Authorization: Bearer YOUR_API_KEY or X-Api-Key. The two file-write endpoints use multipart/form-data.
files.replace can replace any file owned by that account. Account plan entitlements still limit the access types a key may assign, so a request cannot create or switch to a permission unavailable on the plan.
Finding a group reference
Use GET /api/v1/groups with groups.read to list the API key owner's groups. Results are sorted by name and do not contain password hashes or other private group state.
{
"data": [
{
"group_id": "b68e93d0-bc8b-4eb7-900e-479c7608a1f4",
"name": "Release files",
"permission": "system_locker_simple",
"systemlocker_system_id": "YOUR_SYSTEM_ID",
"has_password": false,
"file_count": 3
}
]
}
Pass group_id unchanged in the upload's group access object. You can also copy the same ID from the group row on the Groups page.
Request fields
| Field | Create | Replace |
|---|---|---|
file |
Required file body. | Required replacement file body. |
access |
Required JSON object describing direct or group access. | Optional. When omitted, current access is preserved. When supplied, it replaces the complete access setting. |
metadata |
Optional JSON object of string values. Blank values are not stored. | Optional metadata patch. Omitted keys are preserved, nonblank values are inserted or overwritten, and "" or null deletes a key. |
expected_revision |
Not used. | Optional non-negative integer. A stale value returns HTTP 409 with revision_conflict. |
Access objects
{"type":"public"}
{"type":"hidden"}
{"type":"private"}
{"type":"api_available"}
{"type":"password_protected","password":"YOUR_FILE_PASSWORD"}
{"type":"group","group_id":"YOUR_GROUP_REFERENCE"}
- Direct access types are subject to the account plan's permission entitlements.
- System Locker access is assigned through an owned group configured for the appropriate System Locker permission and system.
- Replacing access clears the old group or password configuration as needed. Omitting
accessduring replacement preserves it exactly.
Metadata patch behavior
{
"version": "1.4.3",
"channel": "",
"obsolete_key": null
}
- This example writes
versionand deleteschannelandobsolete_key. - Metadata values must be strings or
null. A whitespace-only string is retained as a value. - Metadata supports up to 100 keys. Names are limited to 96 characters and combined names and values are limited to 65,535 bytes per request.
__revisionsand__revision_timeare server-managed and cannot be supplied. Successful replacements increment and update them after applying the user metadata patch.
Success response
{
"data": {
"reference_id": "abc123def456",
"file": {
"name": "release.bin",
"mime_type": "application/octet-stream",
"size": 12345,
"uploaded_at": "2026-08-22T18:00:00.000000Z"
},
"access": {"type": "api_available"},
"metadata": {"version": "1.4.3", "__revisions": "2"},
"revision": 2
}
}
Replacement retains the same public reference and download count. The response never includes the internal upload UUID, storage path, or a password hash.
Chunked uploads for large files
Use upload sessions for files from 96 MB through 1 GB. Creation requires files.upload; replacement requires files.replace.
| Method + path | Purpose |
|---|---|
POST /api/v1/uploads/chunked |
Starts a new file upload. |
POST /api/v1/uploads/chunked/replacements/{reference_id} |
Starts a replacement while the current file remains available. |
PUT /api/v1/uploads/chunked/{upload_id}/parts/{order_number} |
Accepts one raw part body. |
GET /api/v1/uploads/chunked/{upload_id} |
Returns received parts and processing status. |
DELETE /api/v1/uploads/chunked/{upload_id} |
Cancels a session that is not processing or complete. |
Initialize
{
"idempotency_key": "YOUR_UUID",
"file_name": "release.bin",
"file_size": 240000000,
"mime_type": "application/octet-stream",
"file_sha256": "SHA256_OF_COMPLETE_FILE",
"access": {"type": "group", "group_id": "YOUR_GROUP_REFERENCE"},
"metadata": {"version": "2.0.0"}
}
Send this JSON to the creation endpoint. access uses the same direct and group objects shown above. For a replacement, access and metadata are optional, and expected_revision is supported.
The response supplies an upload_id and an ordered chunks array. Each entry contains its order_number, exact byte size, and current status.
Send parts and finish
curl --request PUT \
--header "Authorization: Bearer YOUR_API_KEY" \
--header "Content-Type: application/octet-stream" \
--header "X-Chunk-SHA256: SHA256_OF_THIS_PART" \
--data-binary @part-0.bin \
"https://invisiblefolder.net/api/v1/uploads/chunked/UPLOAD_ID/parts/0"
Send consecutive byte ranges using the returned sizes. Hash the complete file and each part with a standard SHA-256 library; incremental hashing keeps client memory use bounded. Parts may be retried safely with the same order number and hash.
After all parts are received, poll the status endpoint until stage is COMPLETE or FAILED. A completed session returns the same file descriptor used by regular uploads in result.data.
Read your files
Use the owner-facing file API to catalogue uploads created by the API key's account and inspect one file's saved metadata. It works for every access mode, including private and System Locker files, because it reads account inventory only. It never downloads file bytes.
| Method + path | Required scope | Result |
|---|---|---|
GET /api/v1/files |
files.read |
Lists files owned by the API key's account. |
GET /api/v1/files/{reference_id} |
files.read |
Returns one owned file with its saved metadata. |
files.read is separate from download scopes. Granting it lets an integration catalogue its account's files; it does not bypass a file's download policy or provide a file-content URL.
List filters and sorting
| Parameter | Meaning |
|---|---|
group |
Exact group name. If the account has identically named groups, files from each matching group are included. |
access |
Effective access name: public, hidden, private, api_available, password_protected, system_locker_simple, or system_locker_advanced. |
mime_type |
Exact MIME type, such as application/zip. |
uploaded_after, uploaded_before |
Inclusive RFC 3339 timestamps. |
sort, direction |
Sort by uploaded (default), name, size, or downloads; direction is desc by default or asc. |
limit, cursor |
Return 1–100 files (20 by default). Send the returned next_cursor unchanged with the same filters and sort to get the next page. |
Responses
List items contain the public reference_id, file details, access names, and revision. A group-backed file shows the group name and its effective access name. The single-file response additionally includes safe metadata values.
{
"data": [
{
"reference_id": "abc123def456",
"file": {
"name": "release.bin",
"mime_type": "application/octet-stream",
"size": 12345,
"downloads": 7,
"uploaded_at": "2026-08-22T18:00:00.000000Z"
},
"access": {
"type": "group",
"group": "Release files",
"permission": "hidden"
},
"revision": 2
}
],
"pagination": {
"limit": 20,
"next_cursor": "NEXT_PAGE_CURSOR",
"previous_cursor": null
},
"filters": {
"group": null,
"access": null,
"mime_type": null,
"uploaded_before": null,
"uploaded_after": null
},
"sort": {"field": "uploaded", "direction": "desc"}
}
File metadata
Use this read-only v1 endpoint to inspect an eligible file's descriptor and saved metadata. It does not expose storage paths, password hashes, or restricted-image replacement codes.
This feature is useful for checking if a file has changed and needs to be updated. Metadata is not yet available for unprotected files.
| Method + path | Authentication | Success response |
|---|---|---|
GET /api/v1/files/{reference_id}/metadata |
API key for API Available, Password Protected, and System Locker Simple files. Advanced files require invisiblefolder_token or X-Invisiblefolder-Token. |
JSON containing a safe file descriptor and a metadata object keyed by metadata name. |
Scopes and filters
- The file's effective download permission determines which keys can access metadata. For example, API keys require
downloads.api_available,downloads.password_protected, ordownloads.system_locker_simpledepending on the file's permissions. System Locker Advanced files accept only a valid, short-livedinvisiblefolder_tokenfor the matching owner and system. - Without a filter, the response includes every metadata entry that is safe to disclose, including revision entries such as
__revisionsand__revision_time, which update every time the file is replaced. - To request only specific metadata entries, repeat the query parameter:
?keys[]=__revisions&keys[]=your_key. Unknown keys are omitted. - The restricted-image replacement code is never returned, even if it is explicitly requested.
Restricted image upload
This specialized endpoint is intended for profile pictures: each request accepts one small image, creates a public upload, and uses a separate secret replacement code. Use the native v1 file endpoints for general uploads, access selection, and metadata.
| Method + path | Request | Success response |
|---|---|---|
POST /api/restricted-imagePOST https://invis.zip/api/restricted-image |
multipart/form-data with one file image. Maximum size: 50 KB. |
JSON with response_code, message, upload_id, and upload_code. |
Authentication and replacement
- Send the API key in the
Authorization: Bearer YOUR_API_KEYheader (anX-Api-Keyheader is also accepted). - The key must have the
files.upload_restricted_imagescope and belong to a plan with API access. - When creating an image, send only
file. A successful response has HTTP201and returns anupload_idand secretupload_code. - Store the returned
upload_idandupload_codesecurely in your client. To replace the profile picture later, send the newfiletogether with both values in another request to the same endpoint. - Replacement succeeds with HTTP
200and keeps the same upload identity. The upload code is not recoverable from the API if the client loses it.
upload_code like a secret. It authorizes replacement of that image, so do not expose it in public URLs, logs, or client-visible page content.
System Locker access
| Mode | What to send | Notes |
|---|---|---|
| Simple with key | systemlocker_key |
Useful when your client already has the System Locker key and does not want to prompt for username/password. |
| Simple with username/password | systemlocker_username and systemlocker_password |
Use this when you need a traditional credential prompt. |
| Advanced token | invisiblefolder_token or X-Invisiblefolder-Token |
The token can download Advanced or Simple files, provided it matches the required System Locker system and the owning Invisible Folder account. |
libcurl C++ examples
1) Programmatic download with an Advanced token
CURL* curl = curl_easy_init();
if (curl) {
FILE* out = fopen("download.bin", "wb");
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "X-Invisiblefolder-Download: 1");
headers = curl_slist_append(headers, "X-Invisiblefolder-Token: YOUR_TOKEN");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_URL, "https://invisiblefolder.net/a/abc123def456");
curl_easy_setopt(curl, CURLOPT_WRITEDATA, out);
CURLcode rc = curl_easy_perform(curl);
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
if (rc == CURLE_OK && status == 200) {
// Success: file bytes were written to download.bin
} else {
// Failure: non-200 responses return JSON with error and error_human
}
fclose(out);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}
2) System Locker Simple download with username and password
CURL* curl = curl_easy_init();
if (curl) {
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "X-Invisiblefolder-Download: 1");
headers = curl_slist_append(headers, "X-Systemlocker-Username: YOUR_SYSTEMLOCKER_USERNAME");
headers = curl_slist_append(headers, "X-Systemlocker-Password: YOUR_SYSTEMLOCKER_PASSWORD");
// Or use a System Locker key instead:
/*
headers = curl_slist_append(headers, "X-Systemlocker-Key: YOUR_SYSTEMLOCKER_KEY");
*/
FILE* out = fopen("download.bin", "wb");
curl_easy_setopt(curl, CURLOPT_URL, "https://invisiblefolder.net/a/abc123def456");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, out);
CURLcode rc = curl_easy_perform(curl);
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
if (rc == CURLE_OK && status == 200) {
// Success: file bytes were written to download.bin
} else {
// Failure: non-200 responses return JSON with error and error_human
}
fclose(out);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
}
3) Create a native file upload
CURL* curl = curl_easy_init();
if (curl) {
curl_mime* form = curl_mime_init(curl);
struct curl_slist* headers = nullptr;
curl_mimepart* file = curl_mime_addpart(form);
curl_mime_name(file, "file");
curl_mime_filedata(file, "release.bin");
curl_mimepart* access = curl_mime_addpart(form);
curl_mime_name(access, "access");
curl_mime_data(access, "{\"type\":\"api_available\"}", CURL_ZERO_TERMINATED);
curl_mimepart* metadata = curl_mime_addpart(form);
curl_mime_name(metadata, "metadata");
curl_mime_data(metadata, "{\"version\":\"1.4.2\",\"channel\":\"stable\"}", CURL_ZERO_TERMINATED);
headers = curl_slist_append(headers, "Authorization: Bearer YOUR_API_KEY");
curl_easy_setopt(curl, CURLOPT_URL, "https://invisiblefolder.net/api/v1/files");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_MIMEPOST, form);
// HTTP 201 returns JSON. Save data.reference_id for downloads and replacement.
curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_mime_free(form);
curl_easy_cleanup(curl);
}
4) Replace a native file and patch metadata
// Build a new multipart form and add the replacement file as "file".
curl_mimepart* metadata = curl_mime_addpart(form);
curl_mime_name(metadata, "metadata");
curl_mime_data(metadata, "{\"version\":\"1.4.3\",\"old_key\":null}", CURL_ZERO_TERMINATED);
// access is omitted here, so the file keeps its current access setting.
// expected_revision is optional; include it when you want conflict detection.
curl_mimepart* revision = curl_mime_addpart(form);
curl_mime_name(revision, "expected_revision");
curl_mime_data(revision, "2", CURL_ZERO_TERMINATED);
curl_easy_setopt(curl, CURLOPT_URL,
"https://invisiblefolder.net/api/v1/files/SAVED_REFERENCE_ID/replace");
// Send the form with an API key that has files.replace. HTTP 200 means success.
5) Create a profile picture upload
CURL* curl = curl_easy_init();
if (curl) {
curl_mime* form = curl_mime_init(curl);
curl_mimepart* part = curl_mime_addpart(form);
struct curl_slist* headers = nullptr;
curl_mime_name(part, "file");
curl_mime_filedata(part, "profile.png");
headers = curl_slist_append(headers, "Authorization: Bearer YOUR_API_KEY");
curl_easy_setopt(curl, CURLOPT_URL, "https://invisiblefolder.net/api/restricted-image");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_MIMEPOST, form);
CURLcode rc = curl_easy_perform(curl);
long status = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status);
if (rc == CURLE_OK && status == 201) {
// Parse the JSON response and securely store upload_id and upload_code.
}
curl_slist_free_all(headers);
curl_mime_free(form);
curl_easy_cleanup(curl);
}
6) Replace the profile picture later
// Use the upload_id and upload_code saved from the create response.
curl_mimepart* id = curl_mime_addpart(form);
curl_mime_name(id, "upload_id");
curl_mime_data(id, "SAVED_UPLOAD_ID", CURL_ZERO_TERMINATED);
curl_mimepart* code = curl_mime_addpart(form);
curl_mime_name(code, "upload_code");
curl_mime_data(code, "SAVED_UPLOAD_CODE", CURL_ZERO_TERMINATED);
// Add the new image as the "file" part, then POST the same form and API key
// to https://invisiblefolder.net/api/restricted-image. A 200 response means
// the image was replaced.
Things to plan around
- The native write API accepts one file per request. Parallelize separate requests when uploading several files.
- Keep the returned
reference_id; it identifies the file for downloads and later native replacements. - Clients must retain the secret
upload_codereturned when an image is created if they need to replace it later. - Private-or-higher files are not exposed through this route; denied access uses the same not-found error shape as missing references.
- Non-200 responses from the programmatic download route are JSON, even though successful responses are raw file bytes.
- If you automate against exact response fields, keep integration tests around the routes you use most.