Desarrolladores

Lleva tu contenido a tu propio sitio

El contenido aprobado en CoText y publicado en el canal Tu propio sitio web llega a tu sitio de dos formas: CoText envía a tu sitio un webhook firmado al publicar, o tu sitio obtiene el contenido de la API de lectura. Puedes usar ambas. Zapier, Make y n8n usan las mismas vías.

Webhooks y verificación de firma

Al añadir el canal, introduces la URL del webhook de tu sitio (solo https). CoText genera un secreto de firma (whsec_…) y lo muestra una sola vez. Cada solicitud se firma en el formato Standard Webhooks, así que puedes usar tal cual las bibliotecas existentes (Node, Python, PHP, Go…).

  • webhook-id: el ID del evento. No cambia entre reintentos; no proceses un ID que ya hayas gestionado.
  • webhook-timestamp: segundos Unix. Rechaza las solicitudes con más de 5 minutos de antigüedad (ataques de repetición).
  • webhook-signature: v1,<base64>. El texto firmado es ${webhook-id}.${webhook-timestamp}.${raw-body}, el algoritmo es HMAC-SHA256 y la clave son los bytes del valor base64 que sigue al prefijo whsec_. Durante las 24 horas siguientes a la rotación de un secreto llegan dos firmas separadas por un espacio.

Verifica la firma sobre el cuerpo sin procesar, antes de analizar el JSON:

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

// secret: el valor "whsec_…" que CoText muestra una sola vez al añadir el 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();
  // Mientras se rota un secreto, el encabezado puede traer dos firmas separadas por un espacio: basta con que coincida una.
  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);
  });
}

Cuerpo

{
  "type": "article.published",          // article.updated · article.unpublished · ping
  "timestamp": "2026-10-16T09:30:00.000Z",
  "data": {
    "article": {
      "id": "7c0e…",                     // ID de contenido de CoText: inserta o actualiza según este valor
      "title": "Guía para preparar café",
      "slug": "guia-para-preparar-cafe",
      "status": "published",             // única fuente de verdad: "published" | "draft"
      "excerpt": "…", "meta_description": "…", "focus_keyword": "…",
      "tags": ["café"], "category": "Guía", "content_type": "guide",
      "language": "es", "language_name": "Español", "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": "…"
    }
  }
}
  • Inserta o actualiza según data.article.id (upsert); un evento article.unpublished solo incluye el id.
  • El tipo de evento es orientativo; consulta data.article.status para conocer el estado de la entrada. El orden de entrega no está garantizado.
  • Idiomas: language es el código del idioma del contenido (tr, en, de, fr, es; null si CoText no lo reconoce) y language_name, el nombre guardado. Las versiones en distintos idiomas de un mismo contenido comparten un translation_group_id (el ID del original del grupo); translations enumera las demás versiones ya publicadas, con sus direcciones (el article_id del original es igual a translation_group_id; úsalo para x-default). Construye con ello tus enlaces hreflang. Una versión publicada más tarde no reenvía las anteriores; la API de lectura siempre tiene la lista completa.
  • Imagen de portada: featured_image es { url, alt, width, height, mime, credit, ai } o null. url es una dirección pública permanente (muéstrala directamente o copia el archivo). Si credit no es null, muéstralo junto a la imagen; en las imágenes generadas con IA, ai es true.
  • Datos estructurados: structured_data es una lista de objetos JSON-LD de schema.org: un BlogPosting y, si el texto tiene una sección de preguntas frecuentes (un h2 seguido de preguntas h3 con respuestas en párrafos), un FAQPage. Coloca cada uno en el head de la página dentro de <script type="application/ld+json">.
  • Devuelve 2xx (opcionalmente con { "url": "https://…" } para indicar la dirección de la entrada). Devuelve 401 si la firma no es válida. 410 significa "endpoint eliminado" y no se reintenta; 408, 429 y 5xx se reintentan; las redirecciones no se siguen.

API de lectura

El propietario del espacio de trabajo crea una clave en Configuración > Claves de API (ctx_live_…). La clave se muestra una sola vez, al crearla; CoText solo guarda un hash. Envía el encabezado Authorization: Bearer ctx_live_… en cada solicitud.

La API solo devuelve contenido publicado en el canal Tu propio sitio web, en la versión enviada a ese canal: las ediciones hechas en CoText después de publicar que aún no se han aprobado y enviado no aparecen, y el contenido despublicado del canal sale de la lista.

  • GET /api/v1/articles: lista. Parámetros: limit (1–100, 20 por defecto), cursor (el next_cursor de la respuesta anterior), updated_since (ISO 8601, inclusivo), order (desc | asc, según updated_at), channel (ID del canal), language (código del idioma del contenido: tr, en, de, fr, es).
  • Cada elemento incluye language (código), language_name, translation_group_id y translations: sus otras versiones de idioma que sirve esta API, como { language, slug, url }, sea cual sea el filtro channel o language. Úsalas para los enlaces hreflang y un selector de idioma.
  • featured_image: la portada de la versión publicada, con el mismo formato que en el webhook ({ url, alt, width, height, mime, credit, ai }), o null.
  • structured_data: la misma lista JSON-LD de schema.org que en el webhook (BlogPosting y, si hay preguntas frecuentes, FAQPage).
  • GET /api/v1/articles/{slug}: un solo contenido; también funciona con el ID del contenido en lugar del slug.
  • GET /api/v1/openapi.json: la definición OpenAPI 3.1 (no requiere clave).
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": "es", "language_name": "Español", "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
}

Para sincronizar, usa order=asc&updated_since=<última sincronización> y recorre las páginas hasta que next_cursor esté vacío. Para enterarte del contenido eliminado o despublicado, escucha el webhook (article.unpublished) o descarga la lista completa de vez en cuando.

MCP: conecta tu asistente de IA

CoText es un servidor MCP (Model Context Protocol, Streamable HTTP): Claude, Cursor y otros clientes MCP pueden planificar, generar, editar, aprobar y programar contenido en tu espacio de trabajo. Todo pasa por el flujo de CoText: voz de marca, reglas editoriales aprendidas, créditos, versiones y aprobación ligada a la versión.

El propietario crea una clave con el acceso MCP: asistente de IA en Ajustes > Claves de API (content:write). El asistente trabaja con permisos de editor, a nombre de quien creó la clave; si esa persona deja de ser propietaria o editora, la clave deja de funcionar. Dirección: https://cotext.net/api/mcp. En Claude Code:

claude mcp add --transport http cotext https://cotext.net/api/mcp \
  --header "Authorization: Bearer ctx_live_…"
  • Herramientas: 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 generación va a la cola: generate_articles devuelve un lote al instante y el asistente lo sigue con get_batch. La aprobación pide tres confirmaciones y la revisión actual; la publicación solo envía la versión aprobada.
  • Cada escritura aparece en el registro de actividad como asistente de IA con el prefijo de la clave. Las ediciones de texto del asistente se guardan como una versión aparte y CoText no aprende reglas de marca de ellas. Revoca la clave para cortar el acceso al instante.
  • En claude.ai, ChatGPT y otras aplicaciones compatibles con conectores MCP, añade https://cotext.net/api/mcp como conector: la aplicación te envía a CoText, eliges uno o varios espacios de trabajo y permites el acceso; el asistente puede trabajar después en cada una de sus marcas (list_workspaces, brand_id) (OAuth 2.1 con PKCE; sin clave que copiar). Las conexiones aparecen en Ajustes > Claves de API y se pueden desconectar allí.

Errores y límites

  • Cuerpo del error: { "error": { "code": "…", "message": "…", "message_en": "…" } }. message está en el idioma de la solicitud (encabezado Accept-Language: inglés, turco, alemán, español, francés, árabe o japonés; en otro caso, inglés); message_en siempre está en inglés. Códigos: unauthorized, key_revoked, key_expired, insufficient_scope, api_access_disabled, invalid_parameter, not_found, rate_limited.
  • 600 solicitudes por hora y clave. Las respuestas incluyen X-RateLimit-Limit, X-RateLimit-Remaining y X-RateLimit-Reset; si superas el límite, recibes 429 y Retry-After.
  • Los endpoints aceptan solicitudes GET desde cualquier origen (CORS, sin cookies). Una clave en el navegador es visible para todos, así que úsala en el servidor siempre que puedas; solo lee contenido publicado y no puede cambiar nada. Si crees que se ha filtrado, revócala y crea una nueva.