Mulai integrasi
- Minta administrator membuat aplikasi Anda di Admin → API & integrasi. Setiap aplikasi memiliki lingkungan, cakupan konten, kanal, bahasa, penulis, masa berlaku, dan kuota permintaan yang terpisah.
- Simpan kredensial di server Anda. Produksi menggunakan kredensial ft_live_; sandbox menggunakan ft_test_. Kirim Authorization: Bearer <credential> bersama setiap permintaan.
- Baca /capabilities dan /content/taxonomies untuk memperoleh ID penulis dan topik yang diizinkan. Gunakan skema di OpenAPI untuk menyiapkan dokumen Anda.
- Buat draf, ajukan untuk ditinjau, dan pantau event publikasi. Otomatisasi Anda dapat menyiapkan konten yang bermanfaat sementara editor tetap bertanggung jawab atas rilis.
URL dasar: https://www.formtruth.com/integrations/v1
Konten & publikasi
Setiap draf eksternal memerlukan persetujuan staf atas kontennya saat ini sebelum dirilis. Metode penulisan oleh manusia yang dinyatakan atau pengaturan publikasi otomatis tidak memberikan persetujuan editorial.
Postingan, panduan pembeli, dan FAQ menggunakan satu format dokumen dan riwayat versi. Gunakan /content/posts, /content/guides, atau /content/faqs dengan izin yang sesuai. Aplikasi hanya dapat mengakses konten yang dimilikinya; membaca draf pribadi berbeda dari membaca situs web yang telah dipublikasikan.
Dokumen berisi bahasa sumber, penulis nyata, topik, sumber, serta judul, slug, ringkasan, dan blok isi terstruktur yang dilokalkan. Artikel yang dibantu AI atau diotomatisasi harus menjelaskan tujuan pembacanya dan kontribusi orisinalnya. Penyuntingan terjemahan tidak boleh mengubah fakta produk, angka, ID gambar, tautan, atau dimensi struktural.
Setiap penulisan memerlukan Idempotency-Key. Pembaruan, peninjauan, publikasi, dan penarikan juga memerlukan If-Match dengan ETag yang diapit tanda kutip. Mengedit konten yang telah ditinjau akan membatalkan persetujuan yang terdampak. Mengubah sumber akan membuat terjemahan yang terdampak menjadi usang hingga diselaraskan dan ditinjau kembali.
Gambar yang diunggah tetap belum disetujui sampai editor memeriksa hak dan kualitasnya. Rilis yang telah dipublikasikan merupakan snapshot yang tidak dapat diubah. Penarikan menghapus rilis dari konten publik, feed, dan penemuan melalui pencarian.
Referensi API
Path di bawah ini relatif terhadap URL dasar. Rute artikel yang sama berlaku untuk panduan dan FAQ dengan mengubah keluarga kontennya. Unduh OpenAPI untuk skema permintaan lengkap, cakupan yang diperlukan, dan respons.
| Metode | Path | Tujuan |
|---|
GET | /capabilities | Izin dan batas Anda yang berlaku |
GET / POST | /content/posts | Daftar artikel Anda / buat draf |
GET / PATCH | /content/posts/{id} | Baca draf dan ETag / simpan revisi |
PUT | /content/posts/by-external-id/{externalId} | Buat atau perbarui draf menggunakan ID eksternal stabil Anda |
POST | /content/posts/{id}/submit-review | Ajukan untuk tinjauan editorial |
POST | /content/posts/{id}/publications | Publikasikan bahasa yang disetujui atau jadwalkan rilis |
DELETE | /content/posts/{id}/publications/{releaseId} | Tarik rilis dengan alasan |
GET | /content/taxonomies | Topik aktif dan penulis yang diizinkan |
POST | /content/imports | Antrekan hingga 50 draf dan periksa hasil tiap item |
POST | /assets/uploads | Unggah gambar untuk tinjauan hak dan kualitas |
POST | /assets/{id}/complete | Baca status unggahan yang tersimpan |
GET | /jobs/{id} | Baca hasil impor Anda |
GET / POST | /webhooks/subscriptions | Daftarkan atau registrasikan penerima event HTTPS Anda |
POST | /webhooks/subscriptions/{id} | Perbarui, putar kunci, atau nonaktifkan pengiriman |
Katalog, pesanan, dan inventaris disediakan untuk modul API mendatang. Semua itu belum tersedia melalui gateway v1 ini.
Error & percobaan ulang
Kirim JSON UTF-8 tanpa kompresi. Batas isi permintaan adalah 2 MiB untuk penulisan biasa, 8 MiB untuk impor, dan 18 MiB untuk unggahan gambar yang dikodekan; isi permintaan harus tiba dalam waktu sepuluh detik. 413 berarti isi terlalu besar, 415 berarti format tidak didukung, dan 408 berarti waktu unggah habis.
Error mengembalikan error.code, error.message, detail field jika berlaku, retryable, dan request_id. Simpan X-Request-Id untuk dukungan. 401 berarti masalah kredensial; 403 berarti izin; 412 berarti versi berubah; 422 berarti konten tidak valid atau belum siap; 428 berarti prasyarat tidak ada.
Untuk 429, patuhi Retry-After. Untuk 503 sementara atau gangguan jaringan, lakukan percobaan ulang dengan backoff terbatas serta key dan input yang SAMA. Payload yang berbeda memerlukan key baru. Saat terjadi 409, rekonsiliasikan operasi sebelumnya alih-alih mengulanginya secara membabi buta. Impor mengembalikan hasil per item melalui /jobs/{id}.
Webhook bertanda tangan
Gunakan penerima HTTPS publik pada port 443. Event mencakup content.review_required, content.published, content.withdrawn, content.publication_failed, dan import.completed. Verifikasi isi permintaan mentah sebelum menguraikannya.
HMAC-SHA256(secret, timestamp + "." + event_id + "." + raw_body)
Baca X-Formtruth-Timestamp, X-Formtruth-Event-Id, dan X-Formtruth-Signature. Tolak timestamp di luar lima menit. Header tanda tangan dapat berisi beberapa nilai v1= yang dipisahkan koma selama periode rotasi kunci 24 jam; terima tanda tangan valid apa pun untuk kunci yang masih Anda simpan.
Simpan ID event secara atomik untuk deduplikasi, antrekan pekerjaan, dan segera kembalikan 2xx. Pengiriman dapat dicoba ulang, sehingga event duplikat tidak boleh memublikasikan atau menagih dua kali. Simpan rahasia penandatanganan secara terpisah dari kredensial API.
Contoh kode
Siapkan document.json menggunakan skema KnowledgeDocument dan ID taksonomi nyata yang diizinkan. Atur FORMTRUTH_API_TOKEN dengan aman di server Anda. Contoh ini membuat draf; publikasi tetap memerlukan alur tinjauan.
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.