Начать интеграцию
- Попросите администратора создать ваше приложение в Admin → API и интеграции. У каждого приложения отдельные среда, области контента, каналы, языки, авторы, срок действия и квота запросов.
- Храните учётные данные на своём сервере. В рабочей среде используются учётные данные ft_live_, в песочнице — ft_test_. Отправляйте Authorization: Bearer <credential> с каждым запросом.
- Прочитайте /capabilities и /content/taxonomies, чтобы получить разрешённые идентификаторы авторов и тем. Подготовьте документ по схеме из OpenAPI.
- Создайте черновик, отправьте его на проверку и отслеживайте события публикации. Ваша автоматизация может подготовить полезный контент, а редактор по-прежнему отвечает за выпуск.
Базовый URL: https://www.formtruth.com/integrations/v1
Контент и публикация
Каждый внешний черновик требует одобрения персоналом его текущего содержания перед публикацией. Заявленный метод авторства человеком или автоматическая настройка публикации не предоставляют редакционного одобрения.
Посты, руководства для покупателей и часто задаваемые вопросы используют единый формат документа и историю версий. Используйте /content/posts, /content/guides или /content/faqs с соответствующим разрешением. Приложения могут получать доступ только к принадлежащему им контенту; чтение закрытого черновика отличается от чтения опубликованного сайта.
Документы содержат исходный язык, реального автора, темы, источники, а также локализованные заголовки, слаги, резюме и структурированные блоки основного текста. Статьи, созданные с помощью ИИ или автоматизированных средств, должны определять цель для читателя и исходный вклад. При переводе нельзя изменять сведения о товарах, числа, идентификаторы изображений, ссылки или структурные размеры.
Каждая запись требует Idempotency-Key. Обновления, проверка, публикация и отзыв также требуют If-Match с цитируемым ETag. Редактирование проверенного контента аннулирует затронутое одобрение. Изменение источника делает затронутые переводы устаревшими до согласования и повторной проверки.
Загруженные изображения остаются неодобренными, пока редактор не проверит права и качество. Опубликованные выпуски представляют собой неизменяемые снимки. Отзыв убирает выпуск из общедоступного контента, лент и поисковой выдачи.
Справочник API
Приведённые ниже пути являются относительными к базовому URL. Те же маршруты статей применяются к руководствам и часто задаваемым вопросам при изменении типа контента. Загрузите OpenAPI для получения полных схем запросов, обязательных областей доступа и ответов.
| Метод | Путь | Назначение |
|---|
GET | /capabilities | Ваши действующие разрешения и лимиты |
GET / POST | /content/posts | Список ваших статей / создание черновика |
GET / PATCH | /content/posts/{id} | Чтение черновика и ETag / сохранение редакции |
PUT | /content/posts/by-external-id/{externalId} | Создать или обновить черновик по вашему стабильному внешнему идентификатору |
POST | /content/posts/{id}/submit-review | Отправка на редакторскую проверку |
POST | /content/posts/{id}/publications | Публикация одобренных языковых версий или планирование выпуска |
DELETE | /content/posts/{id}/publications/{releaseId} | Отзыв выпуска с указанием причины |
GET | /content/taxonomies | Активные темы и разрешённые авторы |
POST | /content/imports | Поставьте в очередь до 50 черновиков и проверьте результаты по каждому элементу |
POST | /assets/uploads | Загрузка изображения для проверки прав и качества |
POST | /assets/{id}/complete | Чтение сохранённого статуса загрузки |
GET | /jobs/{id} | Чтение результатов импорта |
GET / POST | /webhooks/subscriptions | Перечислить или зарегистрировать ваши приёмники событий HTTPS |
POST | /webhooks/subscriptions/{id} | Обновление, замена ключа или отключение доставки |
Каталог, заказы и запасы зарезервированы для будущих модулей API. Они пока недоступны через этот шлюз v1.
Ошибки и повторные попытки
Отправляйте несжатый JSON в кодировке UTF-8. Ограничения тела запроса составляют 2 МиБ для обычных записей, 8 МиБ для импорта и 18 МиБ для загрузки закодированных изображений; тело должно поступить в течение десяти секунд. 413 означает, что тело слишком большое, 415 означает неподдерживаемый формат, а 408 означает истечение времени загрузки.
Ошибки возвращают error.code, error.message, сведения о поле, если применимо, retryable и request_id. Сохраняйте X-Request-Id для обращений в службу поддержки. 401 означает проблему с учётными данными; 403 — отсутствие разрешения; 412 — изменение версии; 422 — недействительный или неготовый контент; 428 — невыполненное предварительное условие.
Для 429 соблюдайте Retry-After. При временном 503 или сетевом сбое повторяйте запрос с ограниченной задержкой и тем же ключом и входными данными. Для другой полезной нагрузки нужен новый ключ. При 409 согласуйте состояние предыдущей операции вместо бездумного повторения. Импорт возвращает результаты по каждому элементу через /jobs/{id}.
Подписанные вебхуки
Используйте общедоступный приёмник HTTPS на порту 443. События включают content.review_required, content.published, content.withdrawn, content.publication_failed и import.completed. Проверяйте исходное тело запроса до его разбора.
HMAC-SHA256(secret, timestamp + "." + event_id + "." + raw_body)
Читайте X-Formtruth-Timestamp, X-Formtruth-Event-Id и X-Formtruth-Signature. Отклоняйте временные метки, отличающиеся более чем на пять минут. Заголовок подписи может содержать несколько разделённых запятыми значений v1= в течение окна ротации ключа продолжительностью 24 часов; принимайте любую действительную подпись для сохранённого у вас ключа.
Атомарно сохраняйте идентификатор события для устранения дубликатов, ставьте задачу в очередь и быстро возвращайте 2xx. Доставка может повторяться, поэтому дублирующееся событие не должно повторно публиковать контент или списывать оплату. Храните секрет подписи отдельно от учётных данных API.
Примеры кода
Подготовьте document.json по схеме KnowledgeDocument и используйте действительные разрешённые идентификаторы таксономии. Безопасно задайте FORMTRUTH_API_TOKEN на своём сервере. Эти примеры создают черновик; для публикации по-прежнему требуется пройти процесс проверки.
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.