CnterDocumentationSign in

Start here

  • What Cnter is
  • Core concepts
  • Quickstart

Platform

  • Authentication
  • Conventions
  • Errors
  • Organization
  • The dashboard

Modules

  • Content Management
  • Online Ordering
  • Restaurant Ordering
  • Promotions
  • Reservations
  • Bookings
  • Contact Form

Payments

  • AgnusPAY
  • Tax

Services

  • Address autocomplete
  • Video
  • Email

Reference

  • API reference

Modules

Content Management

Content types, fields and items. The module every other module reads from.

A full brief for whoever builds against this.

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, a name (the label the merchant sees), a field_type, and is_required.
Content item
One entry. Has a title, a slug and a data object 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#

GET/api/content/itemsread_content

Published items, filtered by content type. The endpoint you will call most.

ParameterDefaultNotes
content_typenoneThe type slug. Effectively required, see the warning below.
content_type_idnoneThe type UUID. An alternative to the slug.
page1One-based.
limit10Ask for 50 rather than paging a menu ten at a time.
searchnoneMatches against title and slug.
is_publishedtruePublished items only, and an API key cannot ask for anything else. Drafts are visible in the dashboard.

Always send content_type

Omit it and the filter is dropped: the endpoint returns items from every content type in the organization. There is no error, and the response looks perfectly valid, so a storefront quietly serves whatever sorts first instead of the menu. Send it on every request, including each paginated page.

An empty list is two different problems

A data array with nothing in it means either that no content type has that slug, or that the type exists and nothing in it is published. Both answer 200 — a slug that does not exist is not an error, it is a filter that matched nothing. Only published items come back by default, so the first thing to check is not your code but whether the merchant has pressed publish.

On your server. The client reads the address from the manifest.
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 })
json
{
  "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#

GET/api/content/items/{id}read_content

A 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#

GET/api/content/typesread_content

Every 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.

GET/api/content/docsread_content

Documentation 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:

TypeShape in `data`
textstring
textareastring
rich_textstring containing HTML
numbernumber, integer
decimalnumber
datestring, YYYY-MM-DD
datetimestring, ISO 8601
booleantrue or false
imagestring, a CDN URL
galleryarray of CDN URL strings, in the order set in the dashboard
videoobject, see Video
filearray, 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
urlstring
emailstring
selectstring, one of the configured options
multiselectarray of strings
listarray of strings
content_linka 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.

VideoThe player and thumbnails — videos play while still being prepared.Online OrderingTurning a catalog into a paid order.
PreviousThe dashboardNextOnline Ordering

On this page

  • The model
  • Listing items
  • One item
  • The available types
  • Field types
  • Field notes
  • Content as a catalog
  • Writing content