開発者向け

コンテンツを自社サイトへ

CoTextで承認され、自社サイトチャネルに公開されたコンテンツは、2つの方法でサイトに届きます。公開時にCoTextがサイトへ署名付きWebhookを送信する方法と、サイト側が読み取りAPIからコンテンツを取得する方法です。両方を併用することもできます。Zapier、Make、n8nも同じ方法を使います。

Webhookと署名の検証

チャネルを追加するときに、サイトのWebhook URL(httpsのみ)を入力します。CoTextは署名シークレット(whsec_…)を生成し、一度だけ表示します。すべてのリクエストはStandard Webhooks形式で署名されるため、既存のライブラリ(Node、Python、PHP、Goなど)をそのまま使用できます。

  • webhook-id:イベントIDです。再試行時も同じ値のため、処理済みのIDは処理しないでください。
  • webhook-timestamp:Unix秒です。5分より古いリクエストは拒否してください(リプレイ攻撃対策)。
  • webhook-signature:v1,<base64>。署名対象のテキストは${webhook-id}.${webhook-timestamp}.${raw-body}、アルゴリズムはHMAC-SHA256、キーはwhsec_接頭辞の後に続くbase64値のバイト列です。シークレットのローテーション後24時間は、スペース区切りで2つの署名が届きます。

JSONを解析する前に、生の本文で署名を検証します:

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

// secret:チャネルの追加時にCoTextが一度だけ表示する「whsec_…」の値
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();
  // シークレットのローテーション中は、ヘッダーにスペース区切りで2つの署名が含まれる場合があります。いずれか1つが一致すれば十分です。
  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);
  });
}

本文

{
  "type": "article.published",          // article.updated · article.unpublished · ping
  "timestamp": "2026-10-16T09:30:00.000Z",
  "data": {
    "article": {
      "id": "7c0e…",                     // CoTextのコンテンツID:これをキーに挿入または更新
      "title": "コーヒーの淹れ方ガイド",
      "slug": "coffee-brewing-guide",
      "status": "published",             // 唯一の信頼できる情報源:"published" | "draft"
      "excerpt": "…", "meta_description": "…", "focus_keyword": "…",
      "tags": ["コーヒー"], "category": "ガイド", "content_type": "guide",
      "language": "ja", "language_name": "日本語", "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": "…"
    }
  }
}
  • data.article.idをキーに挿入または更新(upsert)します。article.unpublishedイベントにはidのみが含まれます。
  • イベントの種類はあくまで目安です。投稿の状態はdata.article.statusで確認してください。配信順序は保証されません。
  • 言語:languageはコンテンツの言語コード(tr、en、de、fr、es。CoTextが認識できない場合はnull)、language_nameは保存されている言語名です。同じコンテンツの各言語版はtranslation_group_id(グループの元になったコンテンツのID)を共有します。translationsには、すでに公開されているほかの言語版がURLとともに列挙されます(元のコンテンツのarticle_idはtranslation_group_idと同じで、x-defaultに使用します)。これをもとにhreflangリンクを作成してください。後から公開された言語版によって、先に公開された言語版が再送されることはありません。完全なリストは読み取りAPIで常に取得できます。
  • カバー画像:featured_imageは{ url, alt, width, height, mime, credit, ai }またはnullです。urlは恒久的な公開アドレスです(そのまま表示するか、ファイルをコピーしてください)。creditがnullでない場合は画像と一緒に表示してください。AIで生成した画像ではaiがtrueになります。
  • 構造化データ:structured_dataはschema.orgのJSON-LDオブジェクトのリストです。BlogPostingが1つ、本文にFAQセクション(h2の下に質問のh3と段落の回答)がある場合はFAQPageも含まれます。それぞれをページのheadに<script type="application/ld+json">として配置してください。
  • 2xxを返してください(投稿のURLを伝える場合は{ "url": "https://…" }を付けることもできます)。署名が無効な場合は401を返します。410は「エンドポイント削除」を意味し、再試行されません。408、429、5xxは再試行されます。リダイレクトは追跡されません。

読み取りAPI

ワークスペースのオーナーが設定 › APIキーでキー(ctx_live_…)を作成します。キーは作成時に一度だけ表示され、CoTextはそのハッシュのみを保存します。すべてのリクエストにAuthorization: Bearer ctx_live_…ヘッダーを付けて送信してください。

APIが返すのは、自社サイトチャネルで公開中のコンテンツのみで、そのチャネルに送信されたバージョンです。公開後にCoTextで行った編集のうち、まだ承認・送信されていないものは表示されません。チャネルで公開を取り消したコンテンツはリストから外れます。

  • GET /api/v1/articles:一覧。クエリ:limit(1〜100、デフォルト20)、cursor(前のレスポンスのnext_cursor)、updated_since(ISO 8601、指定日時を含む)、order(desc | asc、updated_at順)、channel(チャネルID)、language(コンテンツの言語コード:tr、en、de、fr、es)。
  • 各アイテムにはlanguage(コード)、language_name、translation_group_id、translationsが含まれます。translationsは、このAPIが提供するほかの言語版を{ language, slug, url }の形式で示し、channelやlanguageのフィルターに関係なく返されます。hreflangリンクや言語切り替えに使用してください。
  • featured_image:公開されたバージョンのカバー画像。webhookと同じ形式({ url, alt, width, height, mime, credit, ai })、またはnullです。
  • structured_data:webhookと同じschema.orgのJSON-LDリスト(BlogPosting、FAQセクションがあればFAQPageも)。
  • GET /api/v1/articles/{slug}:単一のコンテンツ。スラッグの代わりにコンテンツIDも使用できます。
  • GET /api/v1/openapi.json:OpenAPI 3.1の定義(キー不要)。
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": "ja", "language_name": "日本語", "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
}

同期するにはorder=asc&updated_since=<last sync>を使用し、next_cursorが空になるまでページをたどります。削除または公開取り消しされたコンテンツを把握するには、Webhook(article.unpublished)を受信するか、ときどき完全なリストを取得してください。

MCP:AIアシスタントを接続

CoText は MCP サーバー(Model Context Protocol、Streamable HTTP)です。Claude、Cursor などの MCP クライアントが、ワークスペースでコンテンツの計画、生成、編集、承認、公開予約を行えます。すべて CoText のフローを通ります:ブランドボイス、学習した編集ルール、クレジット、バージョン、バージョンに紐づく承認。

オーナーが 設定 > APIキー で MCP:AIアシスタント のアクセス権を持つキーを作成します(content:write)。アシスタントはキーを作成した人の名前で 編集者 の権限で動作します。その人がオーナーまたは編集者でなくなると、キーは使えなくなります。アドレス:https://cotext.net/api/mcp。Claude Code では:

claude mcp add --transport http cotext https://cotext.net/api/mcp \
  --header "Authorization: Bearer ctx_live_…"
  • ツール: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。
  • 生成はキューで行われます:generate_articles はすぐに一括ジョブを返し、アシスタントは get_batch で進行を確認します。承認には3つの確認と最新のリビジョンが必要で、公開されるのは承認済みのバージョンだけです。
  • 書き込み操作はすべて、キーのプレフィックス付きで AIアシスタント としてアクティビティ履歴に表示されます。アシスタントによる本文の編集は別のバージョンとして保存され、CoText はそこからブランドルールを学習しません。アクセスをすぐに止めるにはキーを無効にしてください。
  • claude.ai、ChatGPT など MCP コネクタに対応したアプリで、https://cotext.net/api/mcp をコネクタとして追加します。アプリが CoText に移動させるので、1つ以上のワークスペースを選んでアクセスを許可してください。アシスタントはその各ブランドで作業できます(list_workspaces、brand_id)(PKCE 付き OAuth 2.1。コピーするキーは不要)。接続は 設定 > APIキー に表示され、そこで解除できます。

エラーと制限

  • エラーの本文:{ "error": { "code": "…", "message": "…", "message_en": "…" } }。messageはリクエストの言語(Accept-Languageヘッダー:英語、トルコ語、ドイツ語、スペイン語、フランス語、アラビア語、日本語。それ以外は英語)、message_enは常に英語です。コード:unauthorized、key_revoked、key_expired、insufficient_scope、api_access_disabled、invalid_parameter、not_found、rate_limited。
  • キーごとに1時間あたり600リクエスト。レスポンスにはX-RateLimit-Limit、X-RateLimit-Remaining、X-RateLimit-Resetが含まれます。上限を超えると429とRetry-Afterが返されます。
  • エンドポイントは、あらゆるオリジンからのGETリクエストを受け付けます(CORS、Cookieなし)。ブラウザー上のキーは誰でも見られるため、可能な限りサーバー側で使用してください。キーは公開中のコンテンツを読み取るだけで、何も変更できません。漏えいした可能性がある場合は、無効にして新しいキーを作成してください。