Modules
Content Management
Content types, fields and items. The module every other module reads from.
Content Management is a schema your customer defines and a set of entries they fill in. Your site reads both. It is also the catalog behind ordering, which is why a menu item and a news article are the same kind of record.
The model
- Content type
- A schema. A name, a slug and a list of fields.
- Field
- A
field_key, aname(the label the merchant sees), afield_type, andis_required. - Content item
- One entry. Has a title, a slug and a
dataobject keyed by field key.
Your customer creates types and fields in the dashboard, so a type can gain a field between two of your deploys. Render from the field definitions where you can, rather than hardcoding a shape that will drift.
Listing items
/api/content/itemsread_contentPublished items, filtered by content type. The endpoint you will call most.
| Parameter | Default | Notes |
|---|---|---|
content_type | none | The type slug. Effectively required, see the warning below. |
content_type_id | none | The type UUID. An alternative to the slug. |
page | 1 | One-based. |
limit | 10 | Ask for 50 rather than paging a menu ten at a time. |
search | none | Matches against title and slug. |
is_published | true | Published items only, and an API key cannot ask for anything else. Drafts are visible in the dashboard. |
const cnter = await createCnterClient({
baseUrl: process.env.CNTER_BASE_URL!,
organizationId: process.env.CNTER_ORG_ID!,
apiKey: process.env.CNTER_API_KEY!,
}).ready()
const menu = await cnter.content.items({ content_type: 'menu', limit: 50 }){
"success": true,
"data": [
{
"id": "8f2a1c34-...",
"title": "Beef tartare",
"slug": "beef-tartare",
"is_published": true,
"display_order": 3,
"content_type": { "id": "...", "name": "Menu", "slug": "menu" },
"data": {
"price": 16.5,
"description": "Capers, shallot, egg yolk.",
"image": "https://cdn.../tartare.jpg",
"category": "Starters"
}
}
],
"total": 42,
"pagination": { "page": 1, "limit": 50, "total": 42, "totalPages": 1, "hasMore": false }
}The editable fields live under data, keyed by field key. title and slug are always present. Items come back in the order your customer arranged them in the dashboard, newest first within the same position, so render the array as given.
An empty data array means one of two things: the slug does not exist, which answers 200 with an empty list rather than an error, or nothing of that type is published yet.
One item
/api/content/items/{id}read_contentA single item, plus its content type and that type's field definitions, so you can render without hardcoding a schema.
An id that names no item of this organization answers 404 with code NOT_FOUND, "Content item not found" — and so, for a key, does an item that exists and is not published. An id that is not a uuid answers 400 INVALID_ID; it is nearly always a slug sent where the id goes.
The available types
/api/content/typesread_contentEvery content type in the organization. With include_fields=true, each with its field definitions.
Useful once, while building, to find out what this customer has and what their slugs are. Your site should not need it at runtime. The fields come only when you ask: cnter.content.types({ include_fields: true }) answers each type with fields — id, name, field_key, field_type, is_required and display_order — whether or not anything of that type is published yet. Without the parameter a type is its name, slug, icon and description.
/api/content/docsread_contentDocumentation generated from this organization's own types and fields, as JSON. The same content the dashboard renders under Content then API docs.
Field types
What each field type actually contains in the JSON:
| Type | Shape in `data` |
|---|---|
text | string |
textarea | string |
rich_text | string containing HTML |
number | number, integer |
decimal | number |
date | string, YYYY-MM-DD |
datetime | string, ISO 8601 |
boolean | true or false |
image | string, a CDN URL |
gallery | array of CDN URL strings, in the order set in the dashboard |
video | object, see Video |
file | array, one entry per file: an object { url, name, size, mimeType } or a plain URL string — the editor writes both, depending on how the files were added. Older items can hold a single object or a single URL. Read url when it is an object, the string when it is not |
url | string |
email | string |
select | string, one of the configured options |
multiselect | array of strings |
list | array of strings |
content_link | a content item id, or an array of ids when multiple is on |
Field notes
Images
Images and galleries return public CDN URLs, already cached worldwide. Use them directly in an img tag. Do not proxy or re-host them: you would add a hop, lose the cache and take on the bandwidth.
Gallery order
The order is meaningful. It is the order your customer arranged, so render the array as given rather than sorting it.
Linked content
A content_link field holds ids, not embedded objects. Fetch the linked item separately when you need more than the id.
Rich text
A rich_text field is HTML written by your customer in the dashboard editor. Render it inside a container you style, and sanitise it if your framework does not.
Content as a catalog
When ordering is enabled, one content type is nominated as the catalog and its fields are mapped: which field holds the price, which holds the image, which holds the category, which holds the tax rate. That mapping lives in module settings, not in your code.
The consequence for you is that a cart line is { content_item_id, quantity } and nothing else. The server reads the authoritative price from the item itself. A price sent from a client is ignored entirely. See Online Ordering.
Writing content
Creating and editing items is a dashboard operation, performed by a signed-in user with the owner, admin or manager role. The read_content scope is read-only by design, and a storefront key should never hold more.
If a site genuinely needs to write, that is a session-authenticated call from a trusted backend, not something to route through a storefront key.
An edit refuses a value its field cannot hold with 400 VALIDATION_ERROR, and the message names the field: Field 'Price' must be a number. A slug another item of the same type already has is 409 SLUG_EXISTS; creating never refuses a slug, it appends a counter instead. A required file field takes the array described under Field types, or the single object or URL older items hold. Deleting an item that is not there is a 404, not a success.