Skip to content

REST

REST is the simplest way to read content from Manablox: every question is a plain web address, and the answer is JSON. You can try it in your browser, with curl in a terminal, or from any programming language. There is nothing to install.

REST is served by the public API only, and it is read-only. All addresses below start with the public API’s address: http://localhost:3100 in a local project.

With the public API running (pnpm dev:public in your CMS project) and a space made by the Basic setup, open this address in your browser:

http://localhost:3100/v1/permalink/about

You should see the About page as JSON. Or, in a terminal:

Terminal window
curl http://localhost:3100/v1/permalink/about

If the browser shows an error page with status 404, nothing is published at about in the space the public API serves. If it cannot connect at all, the public API is not running.

RouteReturns
GET /v1/permalink/{path}The document at a path, for example /v1/permalink/blog/hello-world
GET /v1/permalinkThe space’s home page (the document with the star in the content tree)
GET /v1/content/{id}One document by its id
GET /v1/contentA list of documents, see Lists
GET /v1/menus/{name}A menu by its technical name, for example /v1/menus/main
GET /v1/assets/{id}An image or file: its details and the addresses of its sizes
GET /v1/typesThe content model of the space, used to generate TypeScript types
GET /openapi.jsonA machine-readable description of all routes (OpenAPI)
GET /A short overview of what this API serves

These query parameters work on most routes:

ParameterMeaningWorks on
localeThe language, for example ?locale=de. Default: the space’s default languagepermalink, content list, menus
expandRelation fields to include in full, comma separated: ?expand=image,authorpermalink, content, content list, menus
Terminal window
curl 'http://localhost:3100/v1/permalink/about?expand=image'
{
"id": "4f0c...",
"type": "page",
"title": "About",
"slug": "about",
"permalink": "about",
"locale": "en",
"parentId": null,
"publishedAt": "2026-09-06T09:00:00.000Z",
"updatedAt": "2026-09-06T09:00:00.000Z",
"fields": {
"summary": "What My Site is about.",
"components": {
"grid": null,
"blocks": [
{
"blockId": "9a1e...",
"type": "teaser",
"fields": {
"headline": "About us",
"body": { "type": "doc", "content": [] },
"image": null
}
}
]
}
}
}
  • type is the technical name of the content type.
  • fields holds every field, by its technical name. The whole document arrives at once; there is nothing to select.
  • A blocks field is { grid, blocks }. Each block has a blockId, a type (the block type’s technical name) and its own fields. Blocks placed on a grid also carry a layout.
  • A rich text field (body) is structured JSON. The SDK’s richTextToHtml turns it into safe HTML, see The SDK.

A field that points to something else (an image, another document, a user) holds only an id by default. Name it in expand to get the whole object instead:

Terminal window
curl 'http://localhost:3100/v1/permalink/blog/hello-world?expand=image'

Now fields.image is an object like this:

{
"id": "c2d4...",
"url": "http://localhost:3100/media/c2d4.../original",
"filename": "team.jpg",
"mimeType": "image/jpeg",
"width": 1600,
"height": 900,
"alt": "Our team",
"variants": {
"thumb": "http://localhost:3100/media/...",
"card": "http://localhost:3100/media/...",
"hero": "http://localhost:3100/media/..."
}
}

url is the original file. variants are ready-made sizes: thumb (320 pixels), card (640) and hero (1920). Use them directly in <img> tags; they are signed by the CMS, so do not try to build such addresses yourself.

expand also works for fields inside blocks. A related document that is not published, or an image nothing published uses, is left out.

GET /v1/content returns documents page by page:

Terminal window
curl 'http://localhost:3100/v1/content?type=article&limit=10'
{ "items": [], "total": 1, "limit": 10, "offset": 0 }

(items holds the documents, in the same shape as above.)

ParameterMeaning
typeOnly this content type, by its technical name
parentIdOnly the direct children of this document id
underEverything below this document id, at any depth
searchA search term
limitHow many to return: 1 to 100, default 25
offsetHow many to skip. For page 2 with 10 per page: offset=10
locale, expandAs above

total is the number of matching documents, so you can work out how many pages there are.

Terminal window
curl http://localhost:3100/v1/menus/main
{
"id": "...",
"name": "Main navigation",
"machineName": "main",
"items": [
{ "id": "...", "label": "About", "url": null, "target": "_self", "content": { "type": "page", "title": "About", "permalink": "about" }, "children": [] },
{ "id": "...", "label": "Shop", "url": "https://shop.example.com", "target": "_blank", "content": null, "children": [] }
]
}

An entry points either to a document (content, shortened above) or to an external address (url). Link to url when it is set, otherwise to / plus content.permalink. Entries can be nested to any depth through children.

StatusMeaning
200Here is your content
304Not modified: your cached copy is still current (see Caching)
400Something in the address is invalid, for example an id that is not an id
404Nothing published here: no such document, menu or image
429Too many requests from your address in a short time; wait a moment
5xxThe CMS could not answer. Show an error page, not a 404

The public API never shows detailed error messages, so it cannot leak anything about the server.

Plain fetch works:

const response = await fetch('http://localhost:3100/v1/permalink/about');
if (response.status === 404) {
// show your 404 page
} else if (!response.ok) {
// the CMS could not answer: show an error page
} else {
const page = await response.json();
console.log(page.title, page.fields.summary);
}

The SDK does the same with less code, and adds retries, a small cache and helpers for rich text, blocks and images. Create it with transport: 'rest' to use these routes.