API reference

Collections

Collections are custom, freely definable data structures (blog, team, products, FAQ …) that are used in templates via data("slug").

Wherever {ref} appears, you can use the ID or slug: /collections/3/items ≙ /collections/blog/items.

Collection schema

{
  "id": 1,
  "slug": "blog",
  "name": "Blog",
  "folder": "Website A",
  "public_api": true,
  "live": false,
  "singleton": false,
  "detail_page_id": 7,
  "detail_field": "slug",
  "lang": "de",
  "fields": [
    { "name": "titel", "label": "Titel", "type": "text", "required": true },
    { "name": "slug",  "label": "URL",   "type": "text", "required": true },
    { "name": "datum", "label": "Datum", "type": "date", "required": false },
    { "name": "bild",  "label": "Bild",  "type": "image", "required": false },
    { "name": "text",  "label": "Text",  "type": "markdown", "required": false },
    { "name": "kategorie", "label": "Kategorie", "type": "select", "required": false, "options": ["News", "Technik"] },
    { "name": "autor", "label": "Autor", "type": "reference", "required": false, "collection": 2 },
    { "name": "tags", "label": "Tags", "type": "references", "required": false, "collection": 3 }
  ],
  "created_at": 1791191665,
  "updated_at": 1791191665,
  "item_count": 12
}
Field Description
slug a-z0-9_-, starts with a letter. Unique across collections and REST sources
folder Folder in the admin list, for organization only (empty = none)
public_api true = read endpoints reachable without authentication (published entries only), also cross-origin (CORS: Access-Control-Allow-Origin: *)
live true = live data: the entries are created on the live server (e.g. by a workflow from a form) and are preserved when importing a bundle
singleton true = exactly one entry (website settings, color codes …): GET …/items, /data/{slug} and data() in the template return it as an object (or null) instead of a list. A second entry (also via batch) returns 409, the CSV import 400; the easiest way to create and change it is PUT …/item. Switching is only possible with at most one entry (409)
detail_page_id Detail page: ID of a (catch-all) page under which the entries appear – path = page + / + value of detail_field. The admin shows the preview of this page when editing an entry. 0 = none; unknown page: 400
detail_field Field containing the URL part (default slug, must be a field – otherwise 400)
lang Language of all entries (BCP 47, e.g. de, en; empty = no language). Required to link entries as language versions
fields[].name Field name in the template (item.titel): [a-zA-Z_][a-zA-Z0-9_]*, unique, not id
fields[].label Display name in the admin (default: name)
fields[].required Required field
fields[].options Allowed values for select
fields[].collection For reference/references: ID of the linked collection (must exist; can be the collection itself)
item_count Number of entries (read-only)

Field types

Type Stored as Notes
text string
textarea string Multi-line
html string Output in the template with |safe
markdown string Markdown source; output as HTML in the template with |markdown. The API returns the source
number number Strings such as "3.5" are converted
boolean bool If the value is missing, false is stored
date string Recommended: ISO YYYY-MM-DD (sortable)
url string
select string Must be one of the options (if any are defined)
image number File ID → in the template {{ image(item.bild) }}
file number File ID → in the template file(item.datei).url
reference number Entry ID of the collection collection. Also as a string
references number[] List of entry IDs, also as a string "1, 2". Duplicates are dropped, the order is kept
blocks array Block tree (also as JSON text). Strictly validated on create and on partial updates that contain the field (400 with a block.* code and params.field). In the template {% blocks item.field %}. CSV export as JSON text; on import, invalid trees are left empty with a warning

The items endpoints return references as IDs. In the template (data(), item()) and via GET /data/{slug} they are resolved: instead of the ID, the linked entry appears, one level deep. Deleted entries and – without drafts – unpublished entries are dropped. Filters compare the IDs; for references, equality means “contains” (data("blog", "tags", 3)), __ne means “does not contain”. The CSV import accepts IDs or the text of a target entry (preferably slug), the export writes IDs separated by commas.

Empty values ("", null, missing) are stored as null. Fields that are not in the schema are discarded.

Endpoint Access Description
GET /collections 🔑 All collections
POST /collections ✏️ Create → 201
GET /collections/{ref} 🔑 / 🌐 with public_api Single collection
PUT /collections/{ref} ✏️ Merge update. fields – if specified – is completely replaced
DELETE /collections/{ref} ✏️ Deletes the collection with all entries → 204

Note on changing the schema: existing entries are not migrated. Values of renamed or removed fields remain stored until the entry is saved the next time – then they are discarded.

Without authentication, non-public collections return 404 (not 401), so that their existence cannot be detected.

Entries

Entry object

{
  "id": 5,
  "collection_id": 1,
  "data": {
    "titel": "Hallo Welt",
    "slug": "hallo-welt",
    "datum": "2026-10-05",
    "bild": 7,
    "text": "Erster **Beitrag**",
    "kategorie": "News"
  },
  "sort": 0,
  "published": true,
  "translation_group": "",
  "created_at": 1791191665,
  "updated_at": 1791191665
}

GET /collections/{ref}/items 🔑 / 🌐

All entries in manual order (sort). With authentication including drafts, publicly only published: true. For a singleton, the one entry as an object (or null).

For filtering, sorting and limiting, use GET /data/{slug}.

GET /collections/{ref}/items/{id} 🔑 / 🌐

Single entry. Publicly: drafts return 404.

POST /collections/{ref}/items ✏️

{
  "data": { "titel": "Hallo Welt", "slug": "hallo-welt", "bild": 7 },
  "published": true
}

published is optional (default true). The entry is appended at the end. 201, 400 on validation errors, e.g. field "Titel" is required.

Several entries at once: send an array instead of an object (max. 1000 entries per request):

[
  { "data": { "titel": "Erster", "slug": "erster" } },
  { "data": { "titel": "Zweiter", "slug": "zweiter" }, "published": false }
]

All or nothing: if one entry is invalid, none is created; the message names the index (starting at 0), e.g. item 1: field "Titel" is required. The entries are appended at the end in the order of the array. Response 201 with the array of created entries (including id). The page cache is cleared once; the created event fires per entry, as with a single POST. An empty or too large array returns 400.

PUT /collections/{ref}/items/{id} ✏️

Partial update: only the fields specified in data are changed, the rest is kept.

{ "data": { "titel": "Neuer Titel" }, "published": false }

GET /collections/{ref}/export 🔑

All entries (including drafts) as CSV, Content-Disposition: attachment; filename="<slug>-<datum>.csv". Columns: id, published, then the fields in schema order (header row = field names). UTF-8 with BOM, comma as delimiter; yes/no as true/false, numbers with a decimal point, images/files as file ID.

POST /collections/{ref}/import ✏️

CSV import into an existing collection. multipart/form-data:

Field
file CSV file (UTF-8, optionally with BOM, max. 32 MB / 50,000 rows). The delimiter , ; or tab is detected from the header row.
mapping optional, JSON array with one entry per column: field name, "" (ignore), "_published" (yes/no → published), "_draft" (yes/no → draft) or "_id" (entry ID, only for key=_id). Without it: mapping by field name/label, column id → _id, Webflow columns Draft/Archived → _draft.
key optional, matching: a mapped field (e.g. slug) or _id. Rows whose value already belongs to an entry update it (only the mapped fields; status only with a status column), otherwise a new entry is created. Empty value → new entry. Errors: value duplicated in the file, value in several entries, unknown _id. Without key, all rows are appended.

Query: dry_run=1 (write nothing – returns columns, mapping, preview, errors and the counters), replace=1 (without key: delete all existing entries; with key: only those that don’t appear in the file – the others keep their ID). Everything in one transaction; new entries are created before deleting so that no deleted ID is reassigned.

{
  "delimiter": ";", "rows": 2, "columns": [{ "name": "Titel", "samples": ["Yoga", "Pilates"] }],
  "mapping": ["titel", "preis", "_published", ""], "key": "titel",
  "preview": [{ "data": { "titel": "Yoga", "preis": 1234.5 }, "published": true, "action": "update", "id": 12 }],
  "errors": [], "error_count": 0,
  "warnings": [{ "row": 3, "column": "Bild", "message": "no file for \"https://…\"", "code": "csv.noFileFor", "params": { "value": "https://…" } }], "warning_count": 1,
  "created": 1, "updated": 1, "deleted": 0, "imported": 2
}

Conversion per field type: number (1234.5, 1.234,5, 1,234.5; comma only = decimal comma), yes/no (true/false, ja/nein, yes/no, 1/0, x), date → JJJJ-MM-TT (ISO, TT.MM.JJJJ, RFC 1123, Webflow format), image/file (file ID or source URL of an imported file – otherwise empty with a warning), otherwise text. After that, the usual checks apply (required fields, select options). row is the row in the file (header row = 1); empty rows are skipped.

Without dry_run: all or nothing in one transaction – on errors 400 with error (code csv.rowError/csv.cellError for a single error, otherwise csv.errors), errors and error_count. Entries in errors/warnings carry code and params where available. Success 200 with created, updated, deleted and imported (= new + updated). Does not trigger created events (unlike POST …/items). Dry runs don’t clear the page cache.

POST /collections/import ✏️

New collection from a CSV file, otherwise as above. Form fields: file, name (only for the suggestion), optionally collection (JSON as for POST /collections: name, slug, fields) and mapping. Without collection, the server suggests slug, field names and types (response field collection) – types based on all values of the column: yes/no, number (without leading zero), date, URL, HTML, multi-line text, text; Webflow management columns and an id column are ignored. With dry_run=1, only suggestion and preview; without it, the server creates the collection and entries (201, collection contains the new id). key and replace are not available here.

curl -X POST -H "Authorization: Bearer $TOKEN" -F file=@blog.csv -F name=Blog "https://example.com/api/v1/collections/import"

POST /collections/{ref}/items/reorder ✏️

Sets the order. Body: array of entry IDs in the desired order. Entries not listed keep their sort value. 204.

[5, 2, 9, 1]

DELETE /collections/{ref}/items/{id} ✏️

204.

PUT /collections/{ref}/item ✏️

For singletons only: creates or changes the one entry (partial update as with PUT …/items/{id}, with base_updated_at conflict check). Body { "data": { "primaerfarbe": "#7c5cff" }, "published": true }, response: the entry. For other collections 400.

Language versions of entries

As for pages: GET/POST/DELETE /collections/{ref}/items/{id}/translations, link with { "collection": "blog-en", "item_id": 12 } (collection = ID or slug, missing = the same collection; optionally "move": true). The language of an entry is that of its collection (lang) – a collection has one language; for several languages create separate collections (blog + blog-en). Entries of collections without a language cannot be linked (400 translation.noLang). url/path point to the collection’s detail page (detail_page_id + value of detail_field), empty without a detail page. Changing a collection’s language checks the groups of its entries (409 translation.langTaken; removing it while entries are linked: 409 translation.collectionLinked).