The smallest endpoint in the API and the one your language switcher has been waiting for. It answers a single question — “which languages does this site speak right now?” — and answers it in the exact order your editors arranged them in the CRM.
Reach for it when you need to:
- render a language switcher in the header without hard-coding
enanddein five places; - check that the language from the URL (
/de/pricing) actually exists before you pass it to?lang; - generate localized routes,
hreflangtags or a sitemap per language.
List active languages
/v1/langshttps://back.sitecog.com/content/v1/langs
?lang either.Parameters
Nothing to tune here: no path parameters, no query parameters. Just the key.
x-crm-keyheaderstringrequired?key= if headers are not an option. See Site keys.Example request
curl https://back.sitecog.com/content/v1/langs \
-H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"const res = await fetch('https://back.sitecog.com/content/v1/langs', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
});
if (!res.ok) throw new Error(`Content API: ${res.status}`);
const langs = await res.json();
console.log(langs.map((l) => l.key)); // ["en", "de"]// lib/langs.ts — a server-side helper, cached for a minute
export type Lang = { key: string; title: string; index: number };
export async function getLangs(): Promise<Lang[]> {
const res = await fetch('https://back.sitecog.com/content/v1/langs', {
headers: { 'x-crm-key': process.env.CRM_KEY! },
next: { revalidate: 60 },
});
if (!res.ok) throw new Error(`Content API: ${res.status}`);
return res.json();
}Example response
[
{ "key": "en", "title": "English", "index": 0 },
{ "key": "de", "title": "German", "index": 1 }
]Response fields
An array of objects, one per active language. Three fields each — we did promise it was small.
keystringen, de) or a regional variant like pt-BR. This is exactly the value you pass to ?lang. Case-sensitive: pt-BR and pt-br are different codes.titlestringindexnumberBuild a language switcher in React
The usual setup: the language lives in the URL (/en/…, /de/…), the switcher is built from /v1/langs, and every content request carries the same code in ?lang. Here is a switcher that knows nothing about specific languages — add Italian in the CRM tomorrow and it simply shows up.
type Lang = { key: string; title: string; index: number };
export function LangSwitcher({ langs, current, path }: {
langs: Lang[]; // straight from GET /v1/langs
current: string; // the language of the page being shown
path: string; // the rest of the URL, e.g. "/pricing"
}) {
return (
<nav aria-label="Language">
{langs.map((lang) => (
<a
key={lang.key}
href={`/${lang.key}${path}`}
hrefLang={lang.key}
aria-current={lang.key === current ? 'true' : undefined}
>
{lang.title}
</a>
))}
</nav>
);
}Pairing it with ?lang
The key you get here is the key you send everywhere else. Check the code from the URL against the list first: an unknown code in ?lang is a 400 unknown_lang, and nobody wants an error page because someone typed /fr/ by hand.
import { notFound } from 'next/navigation';
import { getLangs } from '@/lib/langs';
import { LangSwitcher } from '@/components/LangSwitcher';
export default async function Home({ params }: { params: Promise<{ lang: string }> }) {
const { lang } = await params;
const langs = await getLangs();
// Unknown language in the URL → a normal 404 instead of a 400 from the API
if (!langs.some((l) => l.key === lang)) notFound();
const res = await fetch(`https://back.sitecog.com/content/v1/pages/home?lang=${lang}`, {
headers: { 'x-crm-key': process.env.CRM_KEY! },
next: { revalidate: 60 },
});
const page = await res.json();
return (
<>
<LangSwitcher langs={langs} current={lang} path="" />
<h1>{page.content.hero.content.hero_title.content[lang]}</h1>
</>
);
}Need a default for visitors who land on /? That is your call — many sites simply take the first language in the list. The API itself never picks a language for you and never substitutes a missing translation; how to handle gaps gracefully is covered in Languages & fallbacks.
Errors
| Status | Body | What happened |
|---|---|---|
| 401 | {"message":"invalid_key"} | Missing, malformed or revoked key. |
| 405 | {"message":"method_not_allowed"} | Only GET (and HEAD) are accepted. The API is read-only. |
| 429 | {"message":"rate_limit_exceeded"} | Too many requests this minute. See Rate limits. |
The full list with fixes lives on the Errors page.