Jeder Request an die Content API trägt einen Site-Key. Er sagt uns, von welcher Website du liest — und das ist so ziemlich alles, was er tut. Kein OAuth-Tanz, kein Token-Refresh, kein Signieren von Requests um Mitternacht. Ein String in einem Header, und du bist drin.
Was ein Site-Key ist
Ein Site-Key sieht so aus:
pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40- Das Präfix
pk_, gefolgt von genau 32 Hex-Zeichen in Kleinbuchstaben:^pk_[a-f0-9]{32}$. - Er gehört zu einer Website. Allein der Key entscheidet, wessen Inhalte du bekommst — einen Parameter „site id“ gibt es nirgends in der API.
- Er ist read-only und sieht nur veröffentlichte Inhalte.
- Eine Website kann mehrere Keys gleichzeitig haben — so wird schmerzfreies Rotieren möglich (mehr dazu unten).
Das pk_ ist eine Anspielung auf die „Publishable Keys“, die du vielleicht von Payment-Anbietern kennst: ein Key, der dafür gemacht ist, in öffentlichem Code zu leben. Warum das völlig in Ordnung ist, klären wir gleich.
Wo du einen Key bekommst
Öffne deine Website im CRM
Melde dich bei Diil an und wähl die Website, deren Inhalte du lesen willst.Geh zu Einstellungen → Content-API-Keys
Hier wohnen alle Keys der Website.Erstelle einen Key
Du bekommst einen frischenpk_…-String. Kopier ihn.Leg ihn in eine Umgebungsvariable
Nicht direkt in den Code — dein zukünftiges Ich wird dir beim Rotieren dankbar sein. Muster für beliebte Frameworks findest du unten.
So schickst du den Key mit
Pack den Key in den Request-Header x-crm-key. Das ist der Standardweg, und er funktioniert vom Server genauso wie aus dem Browser — CORS erlaubt den Header von jeder Origin.
curl "https://back.sitecog.com/content/v1/pages/home?lang=en" \
-H "x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': 'pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40' },
});
const page = await res.json();GET /content/v1/pages/home?lang=en HTTP/1.1
Host: back.sitecog.com
x-crm-key: pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40Der Fallback ?key=
Wenn du wirklich keinen Header setzen kannst, übergib den Key stattdessen als Query-Parameter:
curl "https://back.sitecog.com/content/v1/pages/home?lang=en&key=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40"Wann ist das okay?
- Schnelle Checks — eine URL in die Adresszeile des Browsers kopieren, um zu sehen, was ein Endpoint liefert.
- Tools, die nur eine URL nehmen — eine No-Code-Integration, ein Feed-Importer, ein Plugin für einen Static Site Generator ohne Header-Option.
Überall sonst nimm lieber den Header. URLs landen gern in Server-Logs, im Browserverlauf und in Analytics, und auch wenn der Key kein Geheimnis ist, gibt es keinen Grund, ihn überall zu verstreuen. Außerdem bleiben so deine URLs kurz und dein Code aufgeräumt.
Warum der Key im Browser sicher ist
Kurze Antwort: weil er nichts kann, was deine Besucher nicht sowieso können, indem sie deine Website öffnen. Ein Site-Key ist von vornherein öffentlich. Das kann er und das kann er nicht:
| Ein Site-Key… | |
|---|---|
| liest veröffentlichte Seiten, Sections, Blöcke und Blogposts seiner Website | ja |
| ändert, erstellt oder löscht irgendetwas | nein — die API ist read-only |
| sieht Entwürfe oder unveröffentlichte Blogposts | nein — nur veröffentlichte Inhalte |
| liest Inhalte deiner anderen Websites | nein — ein Key, eine Website |
Es ist dieselbe Idee wie beim Publishable Key eines Payment-Anbieters: Er bestimmt, wessen Daten gezeigt werden, gibt aber keine Macht über sie. Alles, was er lesen kann, landet sowieso auf deiner öffentlichen Website.
Das Einzige, was ein kopierter Key kann, ist dein Request-Kontingent verbrauchen: Jeder Key hat sein eigenes Limit von 600 Requests pro Minute (siehe Rate Limits). Fängt jemand damit an, rotier den Key — das dauert ein paar Minuten.
Den Key in Umgebungsvariablen halten
Der Key ist öffentlich — wozu also Env-Variablen? Weil Keys sich ändern: Ein rotierter Key sollte eine Konfigurationsänderung sein, keine Codeänderung. Außerdem verlangen die meisten Frameworks ein Präfix, bevor sie eine Variable in Browser-Code lassen:
# Next.js — Server Components, Route Handlers (landet nie im Browser)
CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# Next.js — Client Components (wird beim Build ins Bundle eingesetzt)
NEXT_PUBLIC_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# Vite (React, Vue, Svelte…) — wird beim Build eingesetzt
VITE_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40
# Nuxt — überschreibt runtimeConfig.public.crmKey
NUXT_PUBLIC_CRM_KEY=pk_3f9c2a7e1b4d4c0e8a6f5b2d9c1e7a40// app/page.tsx — eine Server Component
export default async function Home() {
const res = await fetch('https://back.sitecog.com/content/v1/pages/home?lang=en', {
headers: { 'x-crm-key': process.env.CRM_KEY! },
next: { revalidate: 60 },
});
const page = await res.json();
return <h1>{page.content.hero.content.hero_title.content.en}</h1>;
}'use client';
import { useEffect, useState } from 'react';
export function HeroTitle() {
const [title, setTitle] = useState('');
useEffect(() => {
fetch('https://back.sitecog.com/content/v1/blocks/hero_title?section=hero&lang=en', {
headers: { 'x-crm-key': process.env.NEXT_PUBLIC_CRM_KEY! },
})
.then((r) => r.json())
.then((block) => setTitle(block.content.en ?? ''));
}, []);
return <h1>{title}</h1>;
}// src/content.ts
export async function getPage(marker: string, lang = 'en') {
const res = await fetch(`https://back.sitecog.com/content/v1/pages/${marker}?lang=${lang}`, {
headers: { 'x-crm-key': import.meta.env.VITE_CRM_KEY },
});
if (!res.ok) throw new Error(`Content API: ${res.status}`);
return res.json();
}export default defineNuxtConfig({
runtimeConfig: {
public: {
crmKey: '', // wird aus NUXT_PUBLIC_CRM_KEY befüllt
},
},
});<script setup>
const { public: { crmKey } } = useRuntimeConfig();
const { data: page } = await useFetch('https://back.sitecog.com/content/v1/pages/home', {
query: { lang: 'en' },
headers: { 'x-crm-key': crmKey },
});
</script>
<template>
<h1>{{ page.content.hero.content.hero_title.content.en }}</h1>
</template>Keys widerrufen und rotieren
Jeder Key lässt sich im CRM in derselben Liste unter Einstellungen → Content-API-Keys widerrufen. Ein widerrufener Key funktioniert nicht mehr: Jeder Request damit bekommt 401 invalid_key. Um Keys ohne einen einzigen fehlgeschlagenen Request zu tauschen, lass sie sich überlappen:
Erstelle einen neuen Key
Lass den alten erst mal in Ruhe — beide funktionieren parallel.Deploye mit dem neuen Key
Aktualisier die Umgebungsvariable überall, wo der alte Key genutzt wurde, bau neu, falls dein Framework ihn einsetzt, und deploye.Prüf, ob die Website weiterhin Inhalte lädt
Öffne ein paar Seiten, schau in die Logs. Keine 401er? Gut.Widerrufe den alten Key
Jetzt kannst du gefahrlos den Stecker ziehen.
Key-Fehler
| Status | Body | Was passiert ist |
|---|---|---|
| 401 | {"message":"invalid_key"} | Der Key fehlt, ist falsch formatiert (nicht pk_ + 32 Hex) oder widerrufen. |
| 429 | {"message":"rate_limit_exceeded"} | Zu viele Requests — oder zu viele falsche Keys von deiner IP in dieser Minute (siehe unten). |
Die Sperre bei falschen Keys
Damit sich Key-Raten nicht lohnt, zählen wir Requests mit ungültigen Keys pro IP. Mehr als 20 Requests mit falschem Key pro Minute von einer IP, und diese IP bekommt für den Rest der Minute 429 — auch mit gültigem Key. Retry-After-Header gibt es nicht; das Fenster wird zu Beginn der nächsten Minute zurückgesetzt.
Der Klassiker, um da versehentlich reinzulaufen: Du widerrufst einen alten Key, aber ein Server oder ein vergessener Cronjob nutzt ihn noch. Das Server-Side-Rendering feuert fleißig 401er, und eine Minute später ist der ganze Server gesperrt — samt neuem Key. Also:
const res = await fetch(url, { headers: { 'x-crm-key': process.env.CRM_KEY } });
if (res.status === 401) {
// Ein falscher oder widerrufener Key repariert sich nicht von selbst. Retries verbrennen
// nur das Budget für falsche Keys und sorgen dafür, dass diese IP gesperrt wird.
throw new Error('Content API: invalid site key, check CRM_KEY');
}
if (res.status === 429) {
// Bis zur nächsten Minute zurückhalten (etwa 60 s plus ein bisschen Jitter).
}Alle anderen Codes findest du auf der Seite Fehler.