Започване на интеграция
- Помолете администратор да създаде приложението ви в 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.
Грешки и повторни опити
Изпращайте некомпресиран UTF-8 JSON. Лимитите за тялото на заявката са 2 MiB за обикновени записвания, 8 MiB за импорти и 18 MiB за качвания на кодирани изображения; тялото трябва да пристигне в рамките на десет секунди. 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.