Iniciar una integración
- Pide a un administrador que cree tu aplicación en Administrador → API e integraciones. Cada aplicación tiene un entorno, ámbitos de contenido, canales, idiomas, autores, caducidad y cuota de solicitudes independientes.
- Guarda la credencial en tu servidor. En producción se utilizan credenciales ft_live_; en el entorno de pruebas, ft_test_. Envía Authorization: Bearer <credential> con cada solicitud.
- Lee /capabilities y /content/taxonomies para obtener los ID de autores y temas permitidos. Usa el esquema de OpenAPI para preparar tu documento.
- Crea un borrador, envíalo a revisión y escucha los eventos de publicación. Tu automatización puede preparar contenido útil mientras un editor sigue siendo responsable de la publicación.
URL base: https://www.formtruth.com/integrations/v1
Contenido y publicación
Todo borrador externo requiere la aprobación del personal sobre su contenido actual antes de publicarse. Declarar un método de autoría humana o una configuración de publicación automatizada no concede aprobación editorial.
Las publicaciones, las guías de compra y las preguntas frecuentes comparten un formato de documento y un historial de versiones. Usa /content/posts, /content/guides o /content/faqs con el permiso correspondiente. Las aplicaciones solo pueden acceder al contenido que poseen; leer un borrador privado no es lo mismo que leer el sitio web publicado.
Los documentos contienen un idioma de origen, un autor real, temas, fuentes y títulos, slugs, resúmenes y bloques de contenido estructurado localizados. Los artículos asistidos por IA o automatizados deben identificar el objetivo de sus lectores y su aportación original. Las ediciones de traducción no pueden cambiar los datos del producto, los números, los ID de imagen, los enlaces ni las dimensiones estructurales.
Cada escritura necesita una Idempotency-Key. Las actualizaciones, la revisión, la publicación y la retirada también necesitan If-Match con el ETag entre comillas. Editar contenido revisado invalida la aprobación afectada. Cambiar la fuente hace que las traducciones afectadas queden obsoletas hasta que se alineen y se revisen de nuevo.
Las imágenes subidas permanecen sin aprobar hasta que un editor comprueba los derechos y la calidad. Las publicaciones realizadas son instantáneas inmutables. La retirada elimina la publicación del contenido público, los feeds y la visibilidad en las búsquedas.
Referencia de la API
Las rutas siguientes son relativas a la URL base. Las mismas rutas de artículos se aplican a las guías y las preguntas frecuentes cambiando la familia de contenido. Descarga OpenAPI para consultar los esquemas completos de las solicitudes, los ámbitos obligatorios y las respuestas.
| Método | Ruta | Propósito |
|---|
GET | /capabilities | Tus permisos y límites efectivos |
GET / POST | /content/posts | Enumerar tus artículos / crear un borrador |
GET / PATCH | /content/posts/{id} | Leer el borrador y el ETag / guardar una revisión |
PUT | /content/posts/by-external-id/{externalId} | Crear o actualizar un borrador mediante su ID externo estable |
POST | /content/posts/{id}/submit-review | Enviar a revisión editorial |
POST | /content/posts/{id}/publications | Publicar idiomas aprobados o programar una publicación |
DELETE | /content/posts/{id}/publications/{releaseId} | Retirar una publicación con un motivo |
GET | /content/taxonomies | Temas activos y autores autorizados |
POST | /content/imports | Poner en cola hasta 50 borradores y consultar los resultados de cada elemento |
POST | /assets/uploads | Subir una imagen para la revisión de derechos y calidad |
POST | /assets/{id}/complete | Leer el estado persistente de la carga |
GET | /jobs/{id} | Leer los resultados de tu importación |
GET / POST | /webhooks/subscriptions | Enumerar o registrar tus receptores de eventos HTTPS |
POST | /webhooks/subscriptions/{id} | Actualizar, rotar la clave o desactivar la entrega |
El catálogo, los pedidos y el inventario están reservados para módulos de API posteriores. Todavía no están disponibles a través de esta puerta de enlace v1.
Errores y reintentos
Envía JSON UTF-8 sin comprimir. Los límites del cuerpo de la solicitud son de 2 MiB para escrituras ordinarias, 8 MiB para importaciones y 18 MiB para cargas de imágenes codificadas; el cuerpo debe llegar en un plazo de diez segundos. Un 413 significa que el cuerpo es demasiado grande, 415 que el formato no es compatible y 408 que se ha agotado el tiempo de espera de la carga.
Los errores devuelven error.code, error.message, detalles del campo cuando corresponde, retryable y request_id. Conserva X-Request-Id para el soporte. Un 401 indica un problema con la credencial; 403 indica falta de permisos; 412 indica una versión modificada; 422 indica contenido no válido o no preparado; 428 indica una condición previa ausente.
Para 429, respeta Retry-After. Ante un 503 temporal o una interrupción de red, vuelve a intentarlo con un retroceso limitado y la MISMA clave y entrada. Una carga diferente requiere una clave nueva. Ante 409, concilia la operación anterior en lugar de repetirla a ciegas. Las importaciones devuelven resultados por elemento a través de /jobs/{id}.
Webhooks firmados
Usa un receptor HTTPS público en el puerto 443. Los eventos incluyen content.review_required, content.published, content.withdrawn, content.publication_failed e import.completed. Verifica el cuerpo sin procesar de la solicitud antes de analizarlo.
HMAC-SHA256(secret, timestamp + "." + event_id + "." + raw_body)
Lee X-Formtruth-Timestamp, X-Formtruth-Event-Id y X-Formtruth-Signature. Rechaza las marcas de tiempo que queden fuera de cinco minutos. El encabezado de firma puede contener varios valores v1= separados por comas durante el periodo de rotación de claves de 24 horas; acepta cualquier firma válida correspondiente a una clave que conserves.
Guarda atómicamente el ID del evento para la deduplicación, pon el trabajo en cola y devuelve 2xx rápidamente. La entrega puede reintentarse, por lo que un evento duplicado no debe publicar ni cobrar dos veces. Mantén el secreto de firma separado de la credencial de la API.
Ejemplos de código
Prepara document.json usando el esquema KnowledgeDocument y los ID reales de taxonomía permitidos. Configura FORMTRUTH_API_TOKEN de forma segura en tu servidor. Estos ejemplos crean un borrador; la publicación sigue requiriendo el flujo de revisión.
import { readFile } from "node:fs/promises"
import { randomUUID } from "node:crypto"
const base = process.env.FORMTRUTH_URL || "https://www.formtruth.com"
const token = process.env.FORMTRUTH_API_TOKEN
if (!token) throw new Error("Set FORMTRUTH_API_TOKEN in a secure environment")
const document = JSON.parse(await readFile("document.json", "utf8"))
// Persist the key before calling. Reuse it only for an identical retry.
const key = process.env.FORMTRUTH_REQUEST_KEY || randomUUID()
console.log("Request key:", key)
const response = await fetch(base + "/integrations/v1/content/posts", {
method: "POST",
headers: { Authorization: "Bearer " + token, "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify({ channel: "b2b", external_id: document.locales[document.source_locale].slug, document }),
})
const result = await response.json()
if (!response.ok) throw new Error(JSON.stringify(result.error))
console.log("Draft:", result.entry.id, "ETag:", response.headers.get("etag"))
// Creating a draft does not publish it. Submit for editorial review separately.
import json, os, uuid, urllib.request, urllib.error
base = os.environ.get("FORMTRUTH_URL", "https://www.formtruth.com")
token = os.environ["FORMTRUTH_API_TOKEN"]
with open("document.json", encoding="utf-8") as source:
document = json.load(source)
# Persist this key before sending; reuse for an identical retry.
key = os.environ.get("FORMTRUTH_REQUEST_KEY", str(uuid.uuid4()))
print("Request key:", key)
payload = {"channel": "b2b", "external_id": document["locales"][document["source_locale"]]["slug"], "document": document}
request = urllib.request.Request(base + "/integrations/v1/content/posts",
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"), method="POST",
headers={"Authorization": "Bearer " + token, "Content-Type": "application/json", "Idempotency-Key": key})
try:
with urllib.request.urlopen(request, timeout=45) as response:
result = json.load(response)
print("Draft:", result["entry"]["id"], "ETag:", response.headers.get("ETag"))
except urllib.error.HTTPError as error:
raise RuntimeError(error.read().decode("utf-8")) from error
# Creating a draft does not publish it. Submit for editorial review separately.
import { createHmac, timingSafeEqual } from "node:crypto"
export function verifyWebhook(rawBody, headers, secret, now = Date.now()) {
const timestamp = headers.get("x-formtruth-timestamp") || ""
const eventId = headers.get("x-formtruth-event-id") || ""
const signatures = (headers.get("x-formtruth-signature") || "").split(",")
if (!/^[0-9]{10}$/.test(timestamp) || !eventId || Math.abs(now - Number(timestamp) * 1000) > 300000) return false
const expected = createHmac("sha256", secret).update(timestamp + "." + eventId + ".").update(rawBody).digest()
return signatures.some(value => {
const part = value.trim()
if (!/^v1=[a-f0-9]{64}$/.test(part)) return false
const actual = Buffer.from(part.slice(3), "hex")
return actual.length === expected.length && timingSafeEqual(actual, expected)
})
}
// Verify the raw body BEFORE JSON parsing. Atomically record eventId in durable
// storage to deduplicate retries; enqueue accepted work, then return HTTP 2xx.
// During rotation both v1 signatures are sent for 24 hours.
import hashlib, hmac, re, time
def verify_webhook(raw_body, headers, secret, now=None):
timestamp = headers.get("x-formtruth-timestamp", "")
event_id = headers.get("x-formtruth-event-id", "")
signatures = headers.get("x-formtruth-signature", "").split(",")
current = time.time() if now is None else now
if not re.fullmatch(r"[0-9]{10}", timestamp) or not event_id or abs(current - int(timestamp)) > 300:
return False
signed = (timestamp + "." + event_id + ".").encode("utf-8") + raw_body
expected = hmac.new(secret.encode("utf-8"), signed, hashlib.sha256).hexdigest()
return any(re.fullmatch(r"v1=[a-f0-9]{64}", part.strip()) and hmac.compare_digest(part.strip()[3:], expected) for part in signatures)
# Normalize header names to lowercase. Verify raw bytes before parsing JSON.
# Atomically persist event_id for deduplication, enqueue work, then return 2xx.