Développeurs

Intégrez vos contenus à votre propre site

Les contenus approuvés dans CoText et publiés sur le canal Votre propre site arrivent sur votre site de deux façons : CoText envoie un webhook signé à votre site lors de la publication, ou votre site récupère les contenus via l'API de lecture. Vous pouvez utiliser les deux. Zapier, Make et n8n passent par les mêmes chemins.

Webhooks et vérification de signature

Lorsque vous ajoutez le canal, vous saisissez l'URL de webhook de votre site (https uniquement). CoText génère un secret de signature (whsec_…) et ne l'affiche qu'une seule fois. Chaque requête est signée au format Standard Webhooks : vous pouvez donc utiliser telles quelles les bibliothèques existantes (Node, Python, PHP, Go…).

  • webhook-id : l'identifiant de l'événement. Il reste identique d'une tentative à l'autre ; ne traitez pas un identifiant déjà traité.
  • webhook-timestamp : secondes Unix. Rejetez les requêtes de plus de 5 minutes (attaques par rejeu).
  • webhook-signature : v1,<base64>. Le texte signé est ${webhook-id}.${webhook-timestamp}.${raw-body}, l'algorithme est HMAC-SHA256, et la clé correspond aux octets de la valeur base64 qui suit le préfixe whsec_. Pendant les 24 heures qui suivent le renouvellement d'un secret, deux signatures séparées par une espace sont envoyées.

Vérifiez la signature sur le corps brut, avant d'analyser le JSON :

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

// secret : la valeur "whsec_…" que CoText affiche une seule fois lors de l'ajout du canal
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();
  // Pendant le renouvellement d'un secret, l'en-tête peut contenir deux signatures séparées par une espace : une seule correspondance suffit.
  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);
  });
}

Corps

{
  "type": "article.published",          // article.updated · article.unpublished · ping
  "timestamp": "2026-10-16T09:30:00.000Z",
  "data": {
    "article": {
      "id": "7c0e…",                     // identifiant du contenu CoText : insérer ou mettre à jour selon celui-ci
      "title": "Guide pour bien préparer son café",
      "slug": "guide-preparation-cafe",
      "status": "published",             // source de vérité unique : "published" | "draft"
      "excerpt": "…", "meta_description": "…", "focus_keyword": "…",
      "tags": ["café"], "category": "Guide", "content_type": "guide",
      "language": "fr", "language_name": "Français", "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": "…"
    }
  }
}
  • Insérez ou mettez à jour selon data.article.id (upsert) ; un événement article.unpublished ne contient que l'id.
  • Le type d'événement n'est qu'une indication ; consultez data.article.status pour connaître l'état de l'article. L'ordre de livraison n'est pas garanti.
  • Langues : language est le code de la langue du contenu (tr, en, de, fr, es ; null si CoText ne la reconnaît pas) et language_name le nom enregistré. Les versions linguistiques d'un même contenu partagent un translation_group_id (l'identifiant de la source du groupe) ; translations liste les autres versions déjà en ligne, avec leurs adresses (l'article_id de la source est égal à translation_group_id : utilisez-le pour x-default). Construisez vos liens hreflang à partir de ces données. Une version publiée plus tard ne renvoie pas les précédentes ; l'API de lecture fournit toujours la liste complète.
  • Image de couverture : featured_image vaut { url, alt, width, height, mime, credit, ai } ou null. url est une adresse publique permanente (affichez-la directement ou copiez le fichier). Si credit n'est pas null, affichez-le avec l'image ; pour les images générées par IA, ai vaut true.
  • Données structurées : structured_data est une liste d'objets JSON-LD schema.org : un BlogPosting et, si le texte contient une FAQ (un h2 suivi de questions en h3 avec des réponses en paragraphes), un FAQPage. Placez chacun dans l'en-tête de la page, dans <script type="application/ld+json">.
  • Renvoyez un code 2xx (éventuellement avec { "url": "https://…" } pour indiquer l'adresse de l'article). Renvoyez 401 si la signature n'est pas valide. 410 signifie « point de terminaison supprimé » et n'est pas retenté ; 408, 429 et 5xx sont retentés ; les redirections ne sont pas suivies.

API de lecture

Le propriétaire de l'espace de travail crée une clé dans Paramètres > Clés API (ctx_live_…). La clé n'est affichée qu'une seule fois, à sa création ; CoText n'en conserve qu'une empreinte (hash). Envoyez l'en-tête Authorization: Bearer ctx_live_… avec chaque requête.

L'API ne renvoie que les contenus en ligne sur le canal Votre propre site, dans la version envoyée à ce canal : les modifications faites dans CoText après publication mais pas encore approuvées et envoyées n'apparaissent pas, et les contenus dépubliés du canal sortent de la liste.

  • GET /api/v1/articles : liste. Paramètres : limit (1–100, 20 par défaut), cursor (le next_cursor de la réponse précédente), updated_since (ISO 8601, inclusif), order (desc | asc, selon updated_at), channel (identifiant du canal), language (code de langue du contenu : tr, en, de, fr, es).
  • Chaque élément contient language (code), language_name, translation_group_id et translations : ses autres versions linguistiques servies par cette API, sous la forme { language, slug, url }, quel que soit le filtre channel ou language. Utilisez-les pour les liens hreflang et un sélecteur de langue.
  • featured_image : la couverture de la version publiée, au même format que dans le webhook ({ url, alt, width, height, mime, credit, ai }), ou null.
  • structured_data : la même liste JSON-LD schema.org que dans le webhook (BlogPosting, plus FAQPage s'il y a une FAQ).
  • GET /api/v1/articles/{slug} : un seul contenu ; l'identifiant du contenu fonctionne aussi à la place du slug.
  • GET /api/v1/openapi.json : la définition OpenAPI 3.1 (sans clé).
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": "fr", "language_name": "Français", "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
}

Pour synchroniser, utilisez order=asc&updated_since=<dernière synchronisation> et parcourez les pages jusqu'à ce que next_cursor soit vide. Pour être informé des contenus supprimés ou dépubliés, écoutez le webhook (article.unpublished) ou récupérez régulièrement la liste complète.

MCP : connectez votre assistant IA

CoText est un serveur MCP (Model Context Protocol, Streamable HTTP) : Claude, Cursor et d'autres clients MCP peuvent planifier, générer, modifier, approuver et programmer du contenu dans votre espace de travail. Tout passe par le flux de CoText : voix de marque, règles éditoriales apprises, crédits, versions et approbation liée à la version.

Le propriétaire crée une clé avec l'accès MCP : assistant IA dans Réglages > Clés API (content:write). L'assistant travaille avec les droits d'éditeur, au nom de la personne qui a créé la clé ; si elle n'est plus propriétaire ou éditrice, la clé cesse de fonctionner. Adresse : https://cotext.net/api/mcp. Dans Claude Code :

claude mcp add --transport http cotext https://cotext.net/api/mcp \
  --header "Authorization: Bearer ctx_live_…"
  • Outils : 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.
  • La génération passe par la file d'attente : generate_articles renvoie un lot immédiatement et l'assistant le suit avec get_batch. L'approbation demande trois confirmations et la révision actuelle ; la publication n'envoie que la version approuvée.
  • Chaque écriture apparaît dans le journal d'activité comme assistant IA avec le préfixe de la clé. Les modifications de texte de l'assistant sont enregistrées comme une version distincte et CoText n'en tire pas de règles de marque. Révoquez la clé pour couper l'accès immédiatement.
  • Dans claude.ai, ChatGPT et les autres applications compatibles avec les connecteurs MCP, ajoutez https://cotext.net/api/mcp comme connecteur : l'application vous envoie vers CoText, vous choisissez un ou plusieurs espaces de travail et autorisez l'accès ; l'assistant peut ensuite travailler dans chacune de leurs marques (list_workspaces, brand_id) (OAuth 2.1 avec PKCE ; aucune clé à copier). Les connexions apparaissent dans Réglages > Clés API et peuvent y être déconnectées.

Erreurs et limites

  • Corps d'erreur : { "error": { "code": "…", "message": "…", "message_en": "…" } }. message est dans la langue de la requête (en-tête Accept-Language : anglais, turc, allemand, espagnol, français, arabe ou japonais ; sinon anglais) ; message_en est toujours en anglais. Codes : unauthorized, key_revoked, key_expired, insufficient_scope, api_access_disabled, invalid_parameter, not_found, rate_limited.
  • 600 requêtes par heure et par clé. Les réponses contiennent X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset ; au-delà de la limite, vous recevez un 429 et Retry-After.
  • Les points de terminaison acceptent les requêtes GET de toute origine (CORS, sans cookies). Une clé utilisée dans le navigateur est visible par tous : utilisez-la côté serveur si possible ; elle ne lit que les contenus en ligne et ne peut rien modifier. Si vous pensez qu'elle a fuité, révoquez-la et créez-en une nouvelle.