Grundlagen
Authentifizierung
Es gibt zwei Wege, sich zu authentifizieren:
| Methode | Für wen | Wie |
|---|---|---|
| API-Token | Externe Clients, Skripte, Sync-Jobs, Headless-Frontends | Header Authorization: Bearer ic_… oder bei GET ?token=ic_… |
| Session-Cookie | Die Admin-App im Browser | Cookie ic_session nach POST /auth/login |
API-Tokens werden im Admin unter Einstellungen → API-Tokens oder per POST /tokens angelegt. Der Klartext-Token wird nur einmal bei der Erstellung angezeigt; gespeichert wird nur ein SHA-256-Hash.
curl -H "Authorization: Bearer ic_Xy…" https://example.com/api/v1/pages
Für einfache GET-Abrufe (Browser-Adresszeile, Tools ohne Header-Unterstützung) geht der Token auch als URL-Parameter:
curl "https://example.com/api/v1/collections/faq/items?token=ic_Xy…"
?token= gilt nur für GET/HEAD – schreibende Anfragen brauchen den Header. Achtung: URLs landen in Browser-Verlauf und Proxy-/Webserver-Logs; dafür am besten einen Nur-Lese-Token verwenden. Ein Token in der URL macht Anfragen auch cross-origin authentifiziert: Bei öffentlichen Collections liefert ein fremdes Frontend mit ?token= dann auch Entwürfe.
Wird ein Authorization: Bearer-Header gesendet, zählt nur er: Ist der Token ungültig, gilt die Anfrage als nicht authentifiziert (geschützte Endpunkte → 401), auch wenn zusätzlich ein gültiges Cookie mitkommt.
Session-Cookies sind HttpOnly und SameSite=Lax, ihre Gültigkeit beträgt 14 Tage. Mit dem Startparameter -secure-cookies (bzw. IC_SECURE_COOKIES=1) wird zusätzlich das Secure-Flag gesetzt – hinter HTTPS unbedingt aktivieren.
CSRF-Schutz
Alle schreibenden Anfragen (POST, PUT, DELETE), die über ein Session-Cookie authentifiziert sind, müssen den Header
X-IC-CSRF: 1
mitsenden (der Wert ist beliebig, er darf nur nicht leer sein). Fehlt er, antwortet der Server mit 403 missing X-IC-CSRF header. Browser senden eigene Header cross-origin nur nach einem CORS-Preflight, den der Server nicht beantwortet – dadurch sind Cookie-Sessions gegen CSRF geschützt.
Anfragen mit API-Token brauchen diesen Header nicht.
Berechtigungen
Jeder Endpunkt hat eine der folgenden Zugriffsstufen:
| Stufe | Wer darf | In dieser Doku |
|---|---|---|
| öffentlich | jeder, ohne Login | 🌐 |
| lesen | Admin-Session oder beliebiger API-Token | 🔑 |
| schreiben | Admin-Session oder API-Token mit can_write |
✏️ |
| nur Session | eingeloggter Admin im Browser (kein Token) | 👤 |
Lese-Tokens, die einen schreibenden Endpunkt aufrufen, erhalten 403 token is read-only. Das gilt auch für nicht öffentliche HTTP-Eingänge der Workflows (/flows/… mit POST/PUT/PATCH/DELETE; GET bleibt erlaubt), für die Vorschau-Endpunkte (POST /pages/{id}/preview, POST /pens/{uuid}/preview) sowie für Läufe und Versionen von Workflows und eigenen Nodes. GET /sources liefert Lese-Tokens die Header-Namen ohne Werte. Öffentliche HTTP-Eingänge übernehmen X-IC-Webhook-Hop nur zusammen mit einer X-IC-Delivery-ID einer eigenen Zustellung; ihre Fehlerantwort an Anonyme ist 500 mit workflow.failed ohne Details. Nicht öffentliche GET-Eingänge lehnt der Server bei Sitzungs-Cookie und Sec-Fetch-Site: cross-site|same-site ab.
Collections können zusätzlich als öffentlich lesbar markiert werden (public_api: true). Dann sind ihre Lese-Endpunkte ohne Authentifizierung erreichbar, liefern aber nur veröffentlichte Einträge (siehe Collections).
Formate & Konventionen
- Request- und Response-Bodies sind JSON (
Content-Type: application/json), außer beim Datei-Upload (multipart/form-data) und der Seitenvorschau (Antworttext/html). - Maximale JSON-Body-Größe: 16 MB.
- Zeitstempel sind Unix-Sekunden (
int), z. B."updated_at": 1791191665. - IDs sind Ganzzahlen.
DELETEantwortet bei Erfolg mit204 No Contentohne Body.POSTzum Anlegen antwortet mit201 Createdund dem angelegten Objekt.PUTist ein Merge: Es werden nur die Felder geändert, die im Body vorkommen; fehlende Felder behalten ihren Wert. Felder wieid,created_atwerden ignoriert.- Gleichzeitiges Bearbeiten:
PUTauf Seiten, Snippets, Assets, globale Blöcke, Workflows und Collection-Einträge akzeptiert optional"base_updated_at"– dasupdated_atdes Stands, den der Client geladen hat. Wurde inzwischen gespeichert, antwortet der Server mit409und{ "error": "…", "code": "common.conflict", "conflict": true, "updated_at": <aktueller Stand> }, ohne etwas zu ändern. Ohne das Feld wird wie bisher überschrieben. Mehrere Änderungen auf einmal prüftPOST /batch. - Listen sind nie
null, sondern mindestens[]. - Jede Antwort unter
/api/v1trägt den HeaderX-IC-Build– die Build-Kennung der eingebetteten Admin-App (auch bei Fehlern und ohne Anmeldung). Der Admin vergleicht sie mit der Kennung, mit der er geladen wurde, und bietet nach einem Server-Update das Neuladen an.
Fehler
Fehler werden immer als JSON mit einem Feld error geliefert, bei den meisten Fehlern zusätzlich mit einem maschinenlesbaren code und – falls die Meldung variable Teile hat – params:
{ "error": "slug 'api' is reserved", "code": "page.slugReserved", "params": { "slug": "api" } }
errorist immer englisch und für Menschen gedacht (Logs, Skripte, Rückfall).codehat die Formbereich.name(z. B.auth.invalidCredentials,page.slugTaken,workflow.unknownNodeType,item.fieldRequired) und ist stabil – Codes werden nicht umbenannt; auf sie (nicht auf den Text) sollten Clients reagieren. Fehler ohnecode(z. B. reine Serverfehler) haben nurerror.paramsenthält die variablen Teile (Namen, Slugs, Felder, Anzahlen) für die Platzhalter der Übersetzung.- Das Admin übersetzt über den Code: Wörterbuch-Schlüssel
errors.<code>mit denparams; unbekannte Codes zeigenerror. - Steht vor der Meldung ein Präfix (z. B.
item 3: field "Titel" is requiredbeim Batch-Anlegen), beschreibtcodedie eigentliche Ursache. - Listen von Meldungen (Fehler/Warnungen beim CSV-Import, Protokoll des Website-Imports) tragen je Eintrag ebenfalls
code/params; Statusobjekte (Website-Import-Job, Bundle-Übertragung) haben dafürerror_code/error_paramsnebenerror. - Laufzeitfehler aus Workflows (Function-Code, Template-Syntax mit Zeile/Spalte) sind reiner Text ohne Code.
| Status | Bedeutung |
|---|---|
400 Bad Request |
Ungültiges JSON, Validierungsfehler (Slug, Pflichtfeld, Typ …) |
401 Unauthorized |
Nicht authentifiziert / ungültiger Token / falsche Zugangsdaten |
403 Forbidden |
Lese-Token bei Schreibzugriff, fehlender CSRF-Header, nur-Session-Endpunkt |
404 Not Found |
Objekt existiert nicht (oder ist für dich nicht sichtbar) |
405 Method Not Allowed |
Methode für diesen Pfad nicht vorgesehen |
409 Conflict |
Name/Slug bereits vergeben, Setup bereits erledigt |
422 Unprocessable Entity |
Template-Fehler beim Rendern |
429 Too Many Requests |
Rate-Limit überschritten (mit Retry-After) oder zu viele fehlgeschlagene Logins |
500 Internal Server Error |
Unerwarteter Fehler (Details im Server-Log) |
502 Bad Gateway |
Eine REST-Datenquelle war nicht erreichbar oder lieferte einen Fehler |
Rate-Limit
Alle Anfragen unter /api/ – auch die HTTP-Eingänge der Workflows – sind je Client-IP begrenzt: ohne Anmeldung 120 pro Minute, mit Token oder Admin-Session das Zehnfache; ein kurzer Vorrat (die Hälfte davon) fängt Spitzen ab. Darüber antwortet der Server mit 429 und Retry-After (Sekunden). Aufrufe, die Workflows intern an die API stellen, zählen nicht. Webseiten, Medien und CSS/JS sind nicht begrenzt (sie kommen meist aus dem Cache).
Die Grenze stellt -rate-limit bzw. IC_RATE_LIMIT ein (Anfragen pro Minute ohne Anmeldung, 0 = aus).
Hinter einem Reverse-Proxy (Apache, Caddy, Traefik, Docker) gilt als Client-IP der letzte Eintrag in X-Forwarded-For – aber nur, wenn die Verbindung von einer lokalen oder privaten Adresse kommt. Den Eintrag hängt der Proxy selbst an, Besucher können ihn nicht fälschen. Das gilt auch für die Sperre nach zehn fehlgeschlagenen Logins.