Entwickler

Bring deine Inhalte auf deine eigene Website

Inhalte, die in CoText freigegeben und auf dem Kanal Deine eigene Website veröffentlicht werden, erreichen deine Website auf zwei Wegen: CoText sendet deiner Website beim Veröffentlichen einen signierten Webhook, oder deine Website ruft die Inhalte über die Lese-API ab. Du kannst beides nutzen. Zapier, Make und n8n nutzen dieselben Wege.

Webhooks und Signaturprüfung

Beim Hinzufügen des Kanals gibst du die Webhook-URL deiner Website ein (nur https). CoText erzeugt einen Signaturschlüssel (whsec_…) und zeigt ihn nur einmal an. Jede Anfrage wird im Format Standard Webhooks signiert, sodass du die fertigen Bibliotheken (Node, Python, PHP, Go …) direkt verwenden kannst.

  • webhook-id: die Ereignis-ID. Sie bleibt bei Wiederholungen gleich; verarbeite keine ID, die du schon behandelt hast.
  • webhook-timestamp: Unix-Sekunden. Lehne Anfragen ab, die älter als 5 Minuten sind (Replay-Angriffe).
  • webhook-signature: v1,<base64>. Der signierte Text ist ${webhook-id}.${webhook-timestamp}.${raw-body}, der Algorithmus HMAC-SHA256, und der Schlüssel sind die Bytes des Base64-Werts nach dem Präfix whsec_. In den 24 Stunden nach dem Erneuern eines Schlüssels kommen zwei durch Leerzeichen getrennte Signaturen an.

Prüfe die Signatur am unveränderten Body, bevor du das JSON parst:

import { createHmac, timingSafeEqual } from "node:crypto";

// secret: der Wert "whsec_…", den CoText beim Hinzufügen des Kanals einmal anzeigt
export function verify(rawBody, headers, secret) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  const sigs = headers["webhook-signature"] ?? "";
  if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest();
  // Während ein Schlüssel erneuert wird, kann der Header zwei durch Leerzeichen getrennte Signaturen enthalten: Ein Treffer genügt.
  return sigs.split(" ").some((s) => {
    const [v, b64] = s.split(",");
    const got = Buffer.from(b64 ?? "", "base64");
    return v === "v1" && got.length === expected.length && timingSafeEqual(got, expected);
  });
}

Body

{
  "type": "article.published",          // article.updated · article.unpublished · ping
  "timestamp": "2026-10-16T09:30:00.000Z",
  "data": {
    "article": {
      "id": "7c0e…",                     // CoText-Inhalts-ID: anhand dieser einfügen oder aktualisieren
      "title": "Ein Leitfaden zum Kaffeebrühen",
      "slug": "leitfaden-kaffee-bruehen",
      "status": "published",             // einzige verlässliche Quelle: "published" | "draft"
      "excerpt": "…", "meta_description": "…", "focus_keyword": "…",
      "tags": ["Kaffee"], "category": "Ratgeber", "content_type": "guide",
      "language": "de", "language_name": "Deutsch", "translation_group_id": "7c0e…",
      "translations": [{ "language": "tr", "url": "https://…", "article_id": "9d41…", "channel_kind": "custom" }],
      "html": "<h2>…</h2><p>…</p>", "markdown": "## …",
      "canonical_url": null, "author_name": null,
      "featured_image": { "url": "https://…/kapak.jpg", "alt": "…", "width": 1600, "height": 900, "mime": "image/jpeg", "credit": null, "ai": false },
      "published_at": "2026-10-16T09:30:00.000Z", "version_id": "…"
    }
  }
}
  • Füge anhand von data.article.id ein oder aktualisiere (Upsert); ein article.unpublished-Ereignis enthält nur die id.
  • Der Ereignistyp ist nur ein Hinweis; den Zustand des Beitrags liefert data.article.status. Die Reihenfolge der Zustellung ist nicht garantiert.
  • Sprachen: language ist der Code der Inhaltssprache (tr, en, de, fr, es; null, wenn CoText sie nicht erkennt) und language_name der gespeicherte Name. Die Sprachversionen desselben Inhalts teilen eine translation_group_id (die ID der Quelle der Gruppe); translations listet die anderen bereits veröffentlichten Versionen mit ihren Adressen (die article_id der Quelle entspricht der translation_group_id; nutze sie für x-default). Baue daraus deine hreflang-Links. Eine später veröffentlichte Version sendet die früheren nicht erneut; die Lese-API hat immer die vollständige Liste.
  • Titelbild: featured_image ist { url, alt, width, height, mime, credit, ai } oder null. url ist eine dauerhafte öffentliche Adresse (direkt einbinden oder die Datei kopieren). Ist credit nicht null, zeige ihn mit dem Bild an; bei KI-generierten Bildern ist ai true.
  • Strukturierte Daten: structured_data ist eine Liste von schema.org-JSON-LD-Objekten: ein BlogPosting und, wenn der Text einen FAQ-Abschnitt hat (h2, darunter h3-Fragen mit Absatz-Antworten), ein FAQPage. Setze jedes Objekt im Seitenkopf in <script type="application/ld+json">.
  • Antworte mit 2xx (optional mit { "url": "https://…" }, um die Adresse des Beitrags zu melden). Antworte mit 401, wenn die Signatur ungültig ist. 410 bedeutet „Endpunkt entfernt“ und wird nicht wiederholt; 408, 429 und 5xx werden wiederholt; Weiterleitungen werden nicht verfolgt.

Lese-API

Der Inhaber des Arbeitsbereichs erstellt unter Einstellungen > API-Schlüssel einen Schlüssel (ctx_live_…). Der Schlüssel wird nur einmal beim Erstellen angezeigt; CoText speichert nur einen Hash davon. Sende bei jeder Anfrage den Header Authorization: Bearer ctx_live_….

Die API liefert nur Inhalte, die auf dem Kanal Deine eigene Website live sind, in der an diesen Kanal gesendeten Version: Änderungen, die nach dem Veröffentlichen in CoText gemacht, aber noch nicht freigegeben und gesendet wurden, erscheinen nicht, und vom Kanal zurückgezogene Inhalte fallen aus der Liste.

  • GET /api/v1/articles: Liste. Abfrage: limit (1–100, Standard 20), cursor (der next_cursor der vorherigen Antwort), updated_since (ISO 8601, einschließlich), order (desc | asc, nach updated_at), channel (Kanal-ID), language (Code der Inhaltssprache: tr, en, de, fr, es).
  • Jeder Eintrag enthält language (Code), language_name, translation_group_id und translations: die anderen Sprachversionen, die diese API ausliefert, als { language, slug, url } – unabhängig vom Filter channel oder language. Nutze sie für hreflang-Links und einen Sprachumschalter.
  • featured_image: das Titelbild der veröffentlichten Version, im selben Format wie im Webhook ({ url, alt, width, height, mime, credit, ai }), oder null.
  • structured_data: dieselbe schema.org-JSON-LD-Liste wie im Webhook (BlogPosting, bei FAQ-Abschnitt zusätzlich FAQPage).
  • GET /api/v1/articles/{slug}: ein einzelner Inhalt; statt des Slugs funktioniert auch die Inhalts-ID.
  • GET /api/v1/openapi.json: die OpenAPI-3.1-Definition (kein Schlüssel nötig).
curl https://cotext.net/api/v1/articles?limit=20 \
  -H "Authorization: Bearer ctx_live_…"

{
  "data": [
    {
      "id": "7c0e…", "title": "…", "slug": "…", "excerpt": "…", "meta_description": "…",
      "tags": ["…"], "category": "…", "content_type": "blog",
      "language": "de", "language_name": "Deutsch", "translation_group_id": "7c0e…",
      "translations": [{ "language": "tr", "slug": "…", "url": "https://…" }],
      "html": "<p>…</p>", "markdown": "…",
      "published_at": "…", "updated_at": "…", "version_id": "…", "canonical_url": null,
      "featured_image": { "url": "https://…/kapak.jpg", "alt": "…", "width": 1600, "height": 900, "mime": "image/jpeg", "credit": null, "ai": false }
    }
  ],
  "next_cursor": "WyIyMDI2LTEwLTE2…",
  "has_more": true
}

Zum Synchronisieren nutze order=asc&updated_since=<letzte Synchronisierung> und folge den Seiten, bis next_cursor leer ist. Um von gelöschten oder zurückgezogenen Inhalten zu erfahren, hör auf den Webhook (article.unpublished) oder rufe ab und zu die vollständige Liste ab.

MCP: KI-Assistenten verbinden

CoText ist ein MCP-Server (Model Context Protocol, Streamable HTTP): Claude, Cursor und andere MCP-Clients können in deinem Arbeitsbereich Inhalte planen, erstellen, bearbeiten, freigeben und die Veröffentlichung planen. Alles läuft durch den Ablauf von CoText: Markenstimme, gelernte Redaktionsregeln, Credits, Versionen und versionsgebundene Freigabe.

Der Inhaber erstellt unter Einstellungen > API-Schlüssel einen Schlüssel mit dem Zugriff MCP: KI-Assistent (content:write). Der Assistent arbeitet mit Editor-Rechten im Namen der Person, die den Schlüssel erstellt hat; ist sie nicht mehr Inhaber oder Editor, funktioniert der Schlüssel nicht mehr. Adresse: https://cotext.net/api/mcp. In Claude Code:

claude mcp add --transport http cotext https://cotext.net/api/mcp \
  --header "Authorization: Bearer ctx_live_…"
  • Werkzeuge: workspace_overview, get_brand_voice, list_content, get_content, create_idea, plan_quick_pack, generate_articles, get_batch, update_content, run_ai_editor, submit_for_review, approve_content, request_changes, schedule_publish, cancel_publish.
  • Die Generierung läuft in der Warteschlange: generate_articles liefert sofort einen Stapel, den der Assistent mit get_batch verfolgt. Die Freigabe verlangt drei Bestätigungen und die aktuelle Revision; veröffentlicht wird nur die freigegebene Version.
  • Jede Schreibaktion erscheint im Aktivitätsprotokoll als KI-Assistent mit dem Präfix des Schlüssels. Textänderungen des Assistenten werden als eigene Version gespeichert, und CoText lernt daraus keine Markenregeln. Widerrufe den Schlüssel, um den Zugriff sofort zu beenden.
  • Füge in claude.ai, ChatGPT und anderen Apps mit MCP-Connector-Unterstützung https://cotext.net/api/mcp als Connector hinzu: Die App leitet dich zu CoText weiter, du wählst einen oder mehrere Arbeitsbereiche und erlaubst den Zugriff – der Assistent kann dann in jeder ihrer Marken arbeiten (list_workspaces, brand_id) (OAuth 2.1 mit PKCE; kein Schlüssel zum Kopieren). Verbindungen erscheinen unter Einstellungen > API-Schlüssel und lassen sich dort trennen.

Fehler und Limits

  • Fehler-Body: { "error": { "code": "…", "message": "…", "message_en": "…" } }. message ist in der Sprache der Anfrage (Header Accept-Language: Englisch, Türkisch, Deutsch, Spanisch, Französisch, Arabisch oder Japanisch; sonst Englisch); message_en ist immer Englisch. Codes: unauthorized, key_revoked, key_expired, insufficient_scope, api_access_disabled, invalid_parameter, not_found, rate_limited.
  • 600 Anfragen pro Stunde und Schlüssel. Antworten enthalten X-RateLimit-Limit, X-RateLimit-Remaining und X-RateLimit-Reset; über dem Limit erhältst du 429 und Retry-After.
  • Die Endpunkte akzeptieren GET-Anfragen von jedem Ursprung (CORS, ohne Cookies). Ein Schlüssel im Browser ist für alle sichtbar, nutze ihn also möglichst auf dem Server; er liest nur Live-Inhalte und kann nichts ändern. Wenn du glaubst, dass er offengelegt wurde, widerrufe ihn und erstelle einen neuen.