A veces no necesitas una página, ni siquiera una sección: necesitas exactamente una cosa. El teléfono de la cabecera. El banner de promo encima de la tienda. El precio que marketing cambia cada viernes. Este endpoint te da un único bloque por su marcador, y nada más.
Candidatos típicos:
- Un teléfono o un email en la cabecera: un bloque
textque vive en la página de serviciocommony aparece en todas partes. - Un banner de promo: un bloque
objectcon título, imagen y enlace, que encaja en un layout que ya renderizas desde el código. - Un precio: un bloque
numberque lee tu checkout o tu landing sin cargar toda la página de precios.
Obtener un bloque
/v1/blocks/:markerhttps://back.sitecog.com/content/v1/blocks/:marker
content. La misma forma que un bloque dentro de la respuesta de una página o sección, solo que sin el envoltorio.Parámetros
markerpathstringobligatoriophone o promo_banner. Letras latinas, dígitos y guiones bajos, de 2 a 40 caracteres, distingue mayúsculas.sectionquerystringopcionalPor defecto: cualquier sección?section=header. Los marcadores de bloque solo son únicos dentro de una sección, así que así dices exactamente a qué title te refieres. Sin él gana la primera coincidencia: el bloque más antiguo con ese marcador. Mira más abajo.langquerystringopcionalPor defecto: todos los idiomas activos?lang=en, ?lang=en,de o ?lang=en&lang=de. Cada código tiene que ser un idioma activo del sitio; si no, recibes 400 unknown_lang. No afecta a los valores sin traducciones, como números o colores.x-crm-keyheaderstringobligatorio?key= si las cabeceras no son una opción. Mira Claves de sitio.Aquí no hay ?empty: un bloque sin su content sería una caja sin nada dentro. El parámetro sencillamente no aplica a este endpoint.
Ejemplo de petición
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 — un 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; // la cabecera sobrevive sin teléfono
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>;
}Ejemplos de respuesta
El envoltorio es siempre el mismo; solo content cambia según el tipo de bloque.
Un bloque de texto
{
"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" }
}El texto es un mapa de idiomas: una clave por idioma, en el orden del CRM. Un idioma sin ningún valor guardado directamente no aparece en el mapa; uno que existe pero está en blanco llega como "". No hay respaldo en el servidor, así que elige tú un idioma de reserva: mira el helper en Consejos.
Un bloque de tipo objeto
{
"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
}
}Un objeto es un conjunto de campos con el marcador del campo como clave, y cada campo sigue las mismas reglas que un bloque suelto: los campos de texto son mapas de idiomas, un enlace es { url, target, title }, una imagen es { file } (o un mapa por idioma), y los números, fechas y booleanos llegan tal como se guardaron. Solo se devuelven los valores; las definiciones de los campos se quedan en el CRM.
Y el precio
{
"id": 140,
"marker": "price",
"name": "Base plan price",
"type": "number",
"multilang": false,
"updatedAt": "2026-09-26T16:20:00.000Z",
"content": 149
}Los números, colores, fechas, rangos de fechas y booleanos llegan como el valor en bruto guardado (o null si están vacíos): sin mapa de idiomas, así que ?lang no los toca. Todas las formas están en la página Tipos de bloque.
Campos de la respuesta
idnumbermarkerstringnamestringtypestringtext, html, image, video, link, number, color, date, date_range, boolean, object, array. Compruébalo antes de leer content si el mismo componente renderiza bloques distintos.multilangbooleantrue si cada idioma tiene su propio archivo, y entonces content es un mapa de URLs por idioma en lugar de un único file.updatedAtstring (ISO 8601)contentdepends on type | nulltext y html, { file } para una sola imagen o vídeo, { url, target, title } para un enlace, un objeto de campos para object, un array de esos objetos para array, un valor en bruto (o null) para números, colores, fechas y booleanos. Detalles: Tipos de bloque.Los marcadores son únicos por sección: usa ?section
Los marcadores de bloque solo tienen que ser únicos dentro de su sección. Eso es lo que permite a los editores reutilizar nombres razonables: la sección hero tiene un title, la sección faq tiene un title, y nadie tiene que inventarse title_2_final.
La otra cara: /v1/blocks/title a secas es ambiguo. Sin ?section la API devuelve la primera coincidencia, el bloque más antiguo con ese marcador, que puede ser o no el que querías, y puede cambiar si alguien vuelve a crear un bloque. Añade la sección y la respuesta es exacta:
# Ambiguo: el "title" que se creara primero
curl "https://back.sitecog.com/content/v1/blocks/title" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"
# Exacto: el title de la sección FAQ
curl "https://back.sitecog.com/content/v1/blocks/title?section=faq" -H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"No pidas los bloques de uno en uno
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;El endpoint de un solo bloque brilla cuando de verdad necesitas un valor en un sitio que por lo demás no tiene nada que ver con esa página: el teléfono en una cabecera global, un banner en un layout, un precio en un widget de checkout.
Errores
| Estado | Cuerpo | Qué ha pasado |
|---|---|---|
| 400 | {"message":"invalid_marker"} | El marcador tiene caracteres fuera de A–Z a–z 0–9 _ o una longitud incorrecta. |
| 400 | {"message":"invalid_lang", …} | Un código en lang está mal formado. |
| 400 | {"message":"unknown_lang", …} | Un idioma en lang no está activo en el sitio. El cuerpo lista los disponibles. |
| 401 | {"message":"invalid_key"} | Clave ausente, mal formada o revocada. |
| 404 | {"message":"block_not_found"} | No hay ningún bloque con este marcador, o no lo hay en la sección que indicaste en ?section. |
| 429 | {"message":"rate_limit_exceeded"} | Demasiadas peticiones en este minuto. Mira Límites. |
La lista completa, con soluciones, está en la página Errores.
Consejos desde las trincheras
// idioma pedido → idioma por defecto → primer valor no vacío
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"