A section is one horizontal slice of a page — the hero, the pricing table, the FAQ — together with all of its blocks. Usually you get sections for free inside /v1/pages/:marker. This endpoint is for the moments when you want just one slice and not the whole cake.
When a section beats a whole page
Fetching the whole page is still the default, and for most pages it is the right call: one request, everything inside. A single section wins in a few specific situations:
- Lazy-loading below the fold. The page is long, the reviews carousel lives somewhere around the fourth scroll, and most visitors never get there. Take the page with
?emptyfor its title and meta tags, fetch the first-screen sections by marker, and pull the heavy ones only when the visitor scrolls close to them. - Shared sections. The footer, a newsletter strip, a “Still have questions?” contact block that appears on every page. Keep it once in the CRM and fetch it by marker wherever you need it.
- Refreshing one part. A client-side widget that only cares about one section — say, a promo area you re-check on a timer — does not need to download the whole page every time.
Get one section
/v1/sections/:markerhttps://back.sitecog.com/content/v1/sections/:marker
content. The shape is exactly the same as a section inside a page response, so the same rendering code works for both.Parameters
markerpathstringrequiredhero or reviews. Same rules as page markers: Latin letters, digits and underscores, 2–40 characters, case-sensitive. The section is found by its marker alone — there is no page parameter — so give sections you fetch this way markers that don't repeat on other pages.langquerystringoptionalDefault: all active languages?lang=en, ?lang=en,de or ?lang=en&lang=de. Every code must be an active language of the site, otherwise you get 400 unknown_lang. See Languages & fallbacks.emptyqueryflagoptionalDefault: offcontent — just id, marker, name, index and show. Present = on: ?empty, ?empty=1, ?empty=true; off with 0 or false. Useful for a cheap “is this section switched on?” check before loading anything heavy.x-crm-keyheaderstringrequired?key= if headers are not an option. See Site keys.Example request
curl "https://back.sitecog.com/content/v1/sections/reviews?lang=en" \
-H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"const res = await fetch('https://back.sitecog.com/content/v1/sections/reviews?lang=en', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
});
if (!res.ok) throw new Error(`Content API: ${res.status}`);
const section = await res.json();
if (section.show) {
console.log(section.content.reviews_title.content.en); // "What people say"
}'use client';
import { useEffect, useRef, useState, type ReactNode } from 'react';
// The site key is public by design, so it is fine in the browser
const KEY = process.env.NEXT_PUBLIC_CRM_KEY!;
export function LazySection({ marker, lang, render }: {
marker: string;
lang: string;
render: (section: any) => ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
const [section, setSection] = useState<any>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const io = new IntersectionObserver(async ([entry]) => {
if (!entry.isIntersecting) return;
io.disconnect(); // load once
const res = await fetch(`https://back.sitecog.com/content/v1/sections/${marker}?lang=${lang}`, {
headers: { 'x-crm-key': KEY },
});
if (res.ok) setSection(await res.json());
}, { rootMargin: '400px' }); // start a bit before it is visible
io.observe(el);
return () => io.disconnect();
}, [marker, lang]);
return <div ref={ref}>{section?.show ? render(section) : null}</div>;
}Example response
{
"id": 47,
"marker": "reviews",
"name": "Customer reviews",
"index": 4,
"show": true,
"content": {
"reviews_title": {
"id": 120,
"marker": "reviews_title",
"name": "Title",
"type": "text",
"multilang": true,
"updatedAt": "2026-09-21T11:02:15.000Z",
"content": { "en": "What people say" }
},
"reviews_list": {
"id": 121,
"marker": "reviews_list",
"name": "Reviews",
"type": "array",
"multilang": false,
"updatedAt": "2026-09-25T08:40:03.000Z",
"content": [
{
"author": { "en": "Maria, Berlin" },
"text": { "en": "Finally I can hear my podcast on the U-Bahn." },
"rating": 5
}
]
}
}
}With ?empty the same request returns only the top part — handy when all you need to know is whether the section is on:
{
"id": 47,
"marker": "reviews",
"name": "Customer reviews",
"index": 4,
"show": true
}Response fields
idnumbermarkerstringnamestringindexnumbershowbooleancontent{ [blockMarker]: Block }?empty.content →
idnumbermarkerstringreviews_title.namestringtypestringtext, html, image, video, link, number, color, date, date_range, boolean, object, array. It decides the shape of content.multilangbooleanupdatedAtstring (ISO 8601)contentdepends on type{ file } for a single image, an array of items for array, and so on. Every shape is on the Block types page.The show flag: hidden is not deleted
Editors can switch a section off in the CRM without deleting it — for a seasonal promo, a half-written FAQ or a block that is waiting for legal. The API still returns such a section with "show": false and all its content. Deciding not to render it is your template's job:
const reviews = await getSection('reviews');
return (
<>
<Hero />
{reviews.show && <Reviews section={reviews} />}
</>
);Errors
| Status | Body | What happened |
|---|---|---|
| 400 | {"message":"invalid_marker"} | The marker has characters outside A–Z a–z 0–9 _ or the wrong length. |
| 400 | {"message":"invalid_lang", …} | A code in lang is malformed (think english instead of en). |
| 400 | {"message":"unknown_lang", …} | A language in lang is not active on the site. The body lists the available ones. |
| 401 | {"message":"invalid_key"} | Missing, malformed or revoked key. |
| 404 | {"message":"section_not_found"} | No section with this marker on this site. Typo? Different site key? |
| 429 | {"message":"rate_limit_exceeded"} | Too many requests this minute. See Rate limits. |
The full list with fixes lives on the Errors page.