Почати інтеграцію
- Попросіть адміністратора створити ваш застосунок у розділі «Адміністрування» → «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.