Sometimes you don't need a page or even a section — you need exactly one thing. The phone number in the header. The promo banner on top of the shop. The price that marketing changes every Friday. This endpoint hands you a single block by its marker, and nothing else.
Typical candidates:
- A phone number or email in the header — a
textblock that lives on thecommonservice page and shows up everywhere. - A promo banner — an
objectblock with a title, an image and a link, dropped into a layout you already render from code. - A price — a
numberblock your checkout or landing page reads without loading the whole pricing page.
Get one block
/v1/blocks/:markerhttps://back.sitecog.com/content/v1/blocks/:marker
content. Same shape as a block inside a page or section response — just without the wrapping.Parameters
markerpathstringrequiredphone or promo_banner. Latin letters, digits and underscores, 2–40 characters, case-sensitive.sectionquerystringoptionalDefault: any section?section=header. Block markers are unique only within a section, so this is how you say exactly which title you mean. Without it, the first match wins — the oldest block with that marker. See below.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. Has no effect on values that have no translations, like numbers or colours.x-crm-keyheaderstringrequired?key= if headers are not an option. See Site keys.There is no ?empty here: a block without its content would be a box with nothing in it. The parameter simply doesn't apply to this endpoint.
Example request
curl "https://back.sitecog.com/content/v1/blocks/phone?section=header&lang=en" \
-H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"const res = await fetch(
'https://back.sitecog.com/content/v1/blocks/phone?section=header&lang=en',
{ headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' } },
);
if (!res.ok) throw new Error(`Content API: ${res.status}`);
const block = await res.json();
console.log(block.content.en); // "+66 2 123 4567"// components/HeaderPhone.tsx — a Server Component
export async function HeaderPhone({ lang }: { lang: string }) {
const res = await fetch(
`https://back.sitecog.com/content/v1/blocks/phone?section=header&lang=${lang}`,
{
headers: { 'x-crm-key': process.env.CRM_KEY! },
next: { revalidate: 60 },
},
);
if (!res.ok) return null; // the header survives without a phone
const phone = await res.json();
const value: string = phone.content[lang] ?? '';
if (!value) return null;
return <a href={`tel:${value.replace(/\s+/g, '')}`}>{value}</a>;
}Example responses
The wrapper is always the same; only content changes with the block type.
A text block
{
"id": 88,
"marker": "phone",
"name": "Phone in header",
"type": "text",
"multilang": true,
"updatedAt": "2026-09-12T07:45:10.000Z",
"content": { "en": "+66 2 123 4567", "de": "+66 2 123 4567" }
}Text is a language map: one key per language, in CRM order. A language with no saved value at all is simply missing from the map; one that exists but is blank comes as "". There is no server-side fallback, so pick a backup language yourself — see the helper in Tips.
An object block
{
"id": 131,
"marker": "promo_banner",
"name": "Promo banner",
"type": "object",
"multilang": false,
"updatedAt": "2026-09-28T10:05:00.000Z",
"content": {
"title": { "en": "Autumn sale: 20% off", "de": "Herbst-Sale: 20 % Rabatt" },
"image": { "file": "https://cdn.example.com/storage/your-site/autumn.jpg" },
"link": {
"url": "https://example.com/sale",
"target": "_self",
"title": { "en": "Shop now", "de": "Jetzt kaufen" }
},
"ends": "2026-10-15",
"active": true
}
}An object is a set of fields keyed by field marker, and each field follows the same rules as a standalone block: text fields are language maps, a link is { url, target, title }, an image is { file } (or a per-language map), numbers, dates and booleans come as stored. Only values are returned — the field definitions stay in the CRM.
And the price
{
"id": 140,
"marker": "price",
"name": "Base plan price",
"type": "number",
"multilang": false,
"updatedAt": "2026-09-26T16:20:00.000Z",
"content": 149
}Numbers, colours, dates, date ranges and booleans come back as the raw stored value (or null if empty) — no language map, so ?lang doesn't touch them. Every shape is listed on the Block types page.
Response fields
idnumbermarkerstringnamestringtypestringtext, html, image, video, link, number, color, date, date_range, boolean, object, array. Check it before reading content if the same component renders different blocks.multilangbooleantrue if every language has its own file, and content is then a map of URLs by language instead of a single file.updatedAtstring (ISO 8601)contentdepends on type | nulltext and html, { file } for a single image or video, { url, target, title } for a link, an object of fields for object, an array of such objects for array, a raw value (or null) for numbers, colours, dates and booleans. Details: Block types.Markers are unique per section — use ?section
Block markers only have to be unique inside their section. That is what lets editors reuse sensible names: the hero section has a title, the faq section has a title, and nobody has to invent title_2_final.
The flip side: /v1/blocks/title on its own is ambiguous. Without ?section the API returns the first match — the oldest block with that marker — which may or may not be the one you meant, and may change if someone recreates a block. Add the section and the answer is exact:
# Ambiguous: whichever "title" was created first
curl "https://back.sitecog.com/content/v1/blocks/title" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"
# Exact: the title of the FAQ section
curl "https://back.sitecog.com/content/v1/blocks/title?section=faq" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"Don't fetch blocks one by one
const title = await getBlock('title', 'promo');
const text = await getBlock('text', 'promo');
const image = await getBlock('image', 'promo');const promo = await fetch('https://back.sitecog.com/content/v1/sections/promo?lang=en', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
}).then((r) => r.json());
const { title, text, image } = promo.content;The single-block endpoint shines when you really need one value in a place that otherwise has nothing to do with that page — the phone in a global header, a banner in a layout, a price in a checkout widget.
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. |
| 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":"block_not_found"} | No block with this marker — or none in the section you named in ?section. |
| 429 | {"message":"rate_limit_exceeded"} | Too many requests this minute. See Rate limits. |
The full list with fixes lives on the Errors page.
Tips from the trenches
// requested language → default language → first non-empty value
export function t(map: Record<string, string> | undefined, lang: string, fallback = 'en') {
if (!map) return '';
return map[lang] || map[fallback] || Object.values(map).find(Boolean) || '';
}
t(phone.content, 'de'); // "+66 2 123 4567"