Documentation

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.

Primary domain invisiblefolder.net

Use this for the main programmatic download route.

Short links invis.zip

Use this when you want the shortest possible download URL.

For apps GET /a/{reference_id}

This is the route intended for software-driven downloads.

API-key status Scoped file writes

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.zip for the short download URL format.
  • For software integrations, use GET /a/{reference_id} with X-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 with POST /api/v1/files/{reference_id}/replace.
  • General creation requires files.upload. Replacement is independent and requires files.replace.
  • List and inspect the files owned by an API key with GET /api/v1/files, GET /api/v1/files/{reference_id}, and the files.read scope.
  • List upload-ready group references with GET /api/v1/groups and the groups.read scope.
  • 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.
All examples below assume you are using libcurl from C++ and want predictable non-browser behavior.

Access quick reference

Credential formats supported by the software-facing download route
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

Software-facing endpoints documented on this page
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.
Private files are not exposed through this software-facing route. Denied access is intentionally returned as the same not-found response shape used for missing references.

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 valid X-Invisiblefolder-Token from 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.

Native upload-support endpoints
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.

API-key write scopes authorize account-owned upload operations; they do not require the file's download password or another download credential. A key with 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

Multipart fields for native file writes
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 access during replacement preserves it exactly.

Metadata patch behavior

{
  "version": "1.4.3",
  "channel": "",
  "obsolete_key": null
}
  • This example writes version and deletes channel and obsolete_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.
  • __revisions and __revision_time are 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.

Chunked upload endpoints
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.

A replacement does not change the downloadable file until processing completes. Storage checks use the replacement's resulting size, so the existing file's bytes are not counted twice.

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.

Owner-facing file endpoints
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

Optional query parameters for GET /api/v1/files
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.

File metadata endpoint
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, or downloads.system_locker_simple depending on the file's permissions. System Locker Advanced files accept only a valid, short-lived invisiblefolder_token for the matching owner and system.
  • Without a filter, the response includes every metadata entry that is safe to disclose, including revision entries such as __revisions and __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.

Restricted image upload endpoint
Method + path Request Success response
POST /api/restricted-image
POST 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_KEY header (an X-Api-Key header is also accepted).
  • The key must have the files.upload_restricted_image scope and belong to a plan with API access.
  • When creating an image, send only file. A successful response has HTTP 201 and returns an upload_id and secret upload_code.
  • Store the returned upload_id and upload_code securely in your client. To replace the profile picture later, send the new file together with both values in another request to the same endpoint.
  • Replacement succeeds with HTTP 200 and keeps the same upload identity. The upload code is not recoverable from the API if the client loses it.
Treat 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

System Locker credential options accepted by the download route
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.
The System Locker linking flow and webhook endpoints exist for partner systems, not for end-user integrations, so they are deliberately not documented on this page.

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_code returned 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.