API reference

File management

Folders

{ "id": 4, "parent_id": null, "name": "Produktbilder" }
Endpoint Access Description
GET /folders 🔑 All folders (flat, nested via parent_id)
POST /folders ✏️ Body { "name": "…", "parent_id": null } → 201
PUT /folders/{id} ✏️ Rename { "name": "…" } and/or move { "move": true, "parent_id": 4 } (null = root folder; 400 folder.cycle when moving into itself or a subfolder, 404 for an unknown folder) → 204
DELETE /folders/{id} ✏️ Deletes the folder and its subfolders; the files they contain move to the main folder (folder_id: null) → 204

Files

File object

{
  "id": 7,
  "folder_id": 4,
  "name": "sneaker.jpg",
  "path": "2026/10/sneaker-1a2b3c4d.jpg",
  "mime": "image/jpeg",
  "size": 482113,
  "width": 2400,
  "height": 1600,
  "alt": "Weißer Sneaker auf Holzboden",
  "variants": [
    { "width": 320,  "height": 213,  "path": "2026/10/sneaker-1a2b3c4d-320w.jpg" },
    { "width": 640,  "height": 426,  "path": "2026/10/sneaker-1a2b3c4d-640w.jpg" },
    { "width": 960,  "height": 640,  "path": "2026/10/sneaker-1a2b3c4d-960w.jpg" },
    { "width": 1280, "height": 853,  "path": "2026/10/sneaker-1a2b3c4d-1280w.jpg" },
    { "width": 1920, "height": 1280, "path": "2026/10/sneaker-1a2b3c4d-1920w.jpg" }
  ],
  "created_at": 1791191677,
  "private": false,
  "url": "/media/2026/10/sneaker-1a2b3c4d.jpg",
  "srcset": "/media/2026/10/sneaker-1a2b3c4d-320w.jpg 320w, …, /media/2026/10/sneaker-1a2b3c4d.jpg 2400w",
  "tag": "<img src=\"/media/…\" srcset=\"…\" sizes=\"100vw\" width=\"2400\" height=\"1600\" alt=\"…\" loading=\"lazy\" decoding=\"async\">"
}
Field Description
name Display name (original file name, can be changed)
path Storage path under /media/ (cannot be changed)
width, height Pixel dimensions for raster images (JPEG, PNG, GIF, WebP), otherwise 0
variants Automatically generated downscaled versions for srcset – only for JPEG and PNG, only for widths smaller than the original (320, 640, 960, 1280, 1920 px)
url Public URL of the original
srcset Ready-made srcset attribute (variants + original); empty without variants
tag Ready-made responsive <img> element (images only)
private Private file: /media/… delivers it (including variants and WebP versions) only when signed in or with a signed link, otherwise 404 – see the manual

GET /files 🔑

Query Description
folder Folder ID or root (default: main folder)
all=1 Files from all folders
q Search in name and alt text
type MIME main type, e.g. image, video, application

Sorting: newest first.

curl -H "Authorization: Bearer $TOKEN" "https://example.com/api/v1/files?all=1&type=image&q=sneaker"

GET /files/{id} 🔑

Single file.

POST /files ✏️

Upload as multipart/form-data. Any number of file fields, optionally one folder_id field and one private field (1/true = create as private file) – both must come before the files.

curl -H "Authorization: Bearer $TOKEN" \
     -F folder_id=4 \
     -F file=@sneaker.jpg \
     -F file=@datenblatt.pdf \
     https://example.com/api/v1/files

Response 201: array of the created file objects.

Limits: 64 MB per file, 256 MB per request. File names are sanitized (lowercase, only a-z0-9._-) and given a random suffix; uploads go to media/JJJJ/MM/.

PUT /files/{id} ✏️

{ "name": "sneaker-weiss.jpg", "alt": "Weißer Sneaker", "move": true, "folder_id": null }
Field Description
name New display name (empty = unchanged)
alt Alt text
move + folder_id folder_id is only applied if move: true (null = main folder)
private true/false makes the file private or public – takes effect on /media/… immediately. Without the field: unchanged

POST /files/{id}/signed-url 🔑

Creates a signed link to the file – meant for private files, harmless for public ones. Optional body:

{ "ttl": 604800 }

ttl = validity in seconds, default 86400 (1 day), allowed 60 to 2592000 (30 days), otherwise 400 (file.ttlInvalid). Response:

{ "url": "/media/2026/10/angebot-1a2b3c4d.pdf?exp=1791796477&sig=Q2x…", "expires_at": 1791796477 }

url is relative to the instance. The signature (HMAC-SHA256 over path and expiry time, secret per instance, not in the bundle) is valid for the original including variants and WebP versions, also with ?w=/?format= appended, and can neither be extended nor transferred to other files. Individual links cannot be revoked. Does not clear the page cache.

DELETE /files/{id} ✏️

Deletes the database entry, the original and all variants. 204. With ?if_unused=1 only if the file is not used anywhere (checked immediately before deleting, see POST /files/usage); otherwise 409 file.inUse with refs and more.

POST /files/{id}/replace ✏️

Replaces the content of a file in place: ID, path and therefore all usages stay, type, size, dimensions and image variants (including the WebP versions) are recalculated, the old variants are removed. multipart/form-data with a field file (up to 64 MB). The file extension must stay the same (400 file.replaceTypeMismatch, params.ext). Response 200 with the file object. Triggers the event content.changed (file/updated).

POST /files/usage 🔑

Where are files used? Body { "ids": [31, 32] } (at most 200, otherwise 400 file.tooManyIDs). Response { "usage": { "31": { "used": true, "refs": [{ "kind": "page", "id": 1, "title": "Home", "path": "/" }], "more": 0 }, "32": { "used": false, "refs": [], "more": 0 } } }. Unknown IDs are missing from the result.

used is authoritative and follows the same cautious check as the clean-up (when in doubt, used). refs are the places found (at most 10, more counts further ones): kind = page, item (with collection = slug), global, snippet, collection, redirect, source, workflow, pen. They are information for people – for references that cannot be assigned to a place, refs can be empty although used: true. Read-only (does not clear the page cache).

GET /cleanup ✏️ · POST /cleanup ✏️

Clean-up: GET lists unused files and CSS/JS assets (criteria in the manual).

{
  "files": [{ "id": 31, "name": "altes-foto.png", "path": "2026/10/altes-foto-1a2b.png", "url": "/media/2026/10/altes-foto-1a2b.png", "mime": "image/png", "size": 3512, "width": 1200, "height": 630, "folder_id": null, "imported": false }],
  "files_bytes": 3512,
  "assets": [{ "id": 7, "name": "webflow-alt", "kind": "css", "size": 13, "imported": true }]
}

POST with { "files": [31], "assets": [7] } deletes the specified items – but only what is still unused when checked again. Response { "files": 1, "assets": 1, "bytes": 3512, "skipped": 0 } (skipped: in use by now or not found).