Data interface
The records of the API api in the namespace ns are accessed via /data/<ns>/<api>. A simple CRUD approach applies:
| Method | Effect |
|---|---|
POST /data/<ns>/<api> | Creates a record. It receives a server-assigned identifier (_id) and metadata (creating account, point in time). |
PUT /data/<ns>/<api>[/<id>] | Changes a record and updates the metadata (last account, point in time). Only the fields to be changed are transferred. Substructures must always be transferred in full. |
DELETE /data/<ns>/<api>/<id> | Deletes a record. |
GET /data/<ns>/<api>/<id> | Returns a record. |
GET /data/<ns>/<api> | Returns the first page (100 records) of all records, in the format of a query. |
POST /data/<ns>/<api>/query | Extended search with filter, sorting and paging, see Queries. |
POST, PUT and DELETE respond with the affected record.
With PUT there are three ways to specify the identifier:
- only in the URL (
/data/<ns>/<api>/<id>), it is missing in the record, - only in the record (
_id), the URL is/data/<ns>/<api>, - both ways, in which case both must be identical, otherwise an error is reported.
The CMS reports errors with status 400 and { "error": "<message>" }, including for unknown records. It reports missing rights with 403.
Structure of a record
{
"_id": "894758ed-e701-4839-a085-3b77bb864d52",
"_cms": {
"created": "2026-10-21T09:12:44.123Z",
"createdBy": "<account identifier>",
"lastModified": "2026-10-21T10:01:02.456Z",
"modifiedBy": "<account identifier>"
},
"name": "Nicolas"
}
_cms is maintained by the CMS and cannot be set. _cms.lastModified is suitable for retrieving only changed records (see Queries).
Examples
const cms = 'https://cms.example.org'
const headers = {
Authorization: 'Basic ' + btoa('CMS_User_01:<password>'),
'Content-Type': 'application/json',
}
const people = `${cms}/data/tutorial/people`
async function call(url, options) {
const res = await fetch(url, { headers, ...options })
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`)
return res.json()
}
// Create
const created = await call(people, { method: 'POST', body: JSON.stringify({ name: 'Nicolas', email: 'nicolas@example.org' }) })
// Read a single record
const one = await call(`${people}/${created._id}`)
// Change: only the changed fields
await call(`${people}/${created._id}`, { method: 'PUT', body: JSON.stringify({ age: 43 }) })
// Search
const { items, filterCount } = await call(`${people}/query`, {
method: 'POST',
body: JSON.stringify({ filter: { age: { $gte: 18 } }, sort: { name: 'ascending' }, pageSize: 20 }),
})
// Delete
await call(`${people}/${created._id}`, { method: 'DELETE' })
The same with curl:
CMS=https://cms.example.org
USER='CMS_User_01:<password>'
curl -X POST -u "$USER" -H 'Content-Type: application/json' -d '{ "name": "Nicolas", "email": "nicolas@example.org" }' "$CMS/data/tutorial/people"
curl -u "$USER" "$CMS/data/tutorial/people/<id>"
curl -X PUT -u "$USER" -H 'Content-Type: application/json' -d '{ "age": 43 }' "$CMS/data/tutorial/people/<id>"
curl -X POST -u "$USER" -H 'Content-Type: application/json' -d '{ "filter": { "age": { "$gte": 18 } } }' "$CMS/data/tutorial/people/query"
curl -X DELETE -u "$USER" "$CMS/data/tutorial/people/<id>"
Retrieval for display (?exported)
With the parameter ?exported (for GET and POST query), the CMS returns the records in a form that can be displayed directly:
- HTML fields contain finished HTML instead of the internal draft-js format.
- In file fields (and their thumbnails),
pathcontains the complete retrieval URL (without host and port).
curl -u "$USER" "$CMS/data/tutorial/file/<id>?exported"
Records retrieved this way are not intended to be written back.
Files
Retrieving
To retrieve a file, the account needs read rights on the API whose record contains the file. Retrieved normally, a file field looks like this:
{
"path": "ABCD",
"name": "myself.png",
"mime": "image/png",
"size": 1289173
}
path is an identifier assigned by the CMS that is unique across all namespaces and APIs. The content is located under /media/<ns>/<api>/<path>, i.e. analogous to the data interface, only with media instead of data and with the identifier of the file instead of that of the record: /media/tutorial/file/ABCD.
With ?exported, the URL is already in the field:
{
"path": "/media/tutorial/file/ABCD",
"name": "myself.png",
"mime": "image/png",
"size": 1289173
}
/media supports partial retrieval (HTTP Range), which is important for playing videos. For direct embedding in <img> or <video>, a short-lived token as parameter ?token=… helps.
Uploading
Records with files are transferred as multipart/form-data. The JSON of the record is placed as a string (JSON.stringify) in the form value data. Each file field contains at least:
{
"path": "12345",
"name": "myself.png"
}
Here, path is the name of another form value that contains the file content.
curl -X POST -u "$USER" \
-F 'data={ "title": "A first image", "image": { "path": "image1", "name": "myself.png" } }' \
-F 'image1=@myself.png' \
"$CMS/data/tutorial/file"
const form = new FormData()
form.append('data', JSON.stringify({ title: 'A first image', image: { path: 'image1', name: 'myself.png' } }))
form.append('image1', file) // e.g. from an <input type="file">
await fetch(`${cms}/data/tutorial/file`, {
method: 'POST',
headers: { Authorization: headers.Authorization }, // the browser sets Content-Type itself
body: form,
})
This applies equally to POST and PUT.