Bắt đầu tích hợp
- Yêu cầu quản trị viên tạo ứng dụng của bạn trong Quản trị → API & tích hợp. Mỗi ứng dụng có môi trường, phạm vi nội dung, kênh, ngôn ngữ, tác giả, thời hạn và hạn ngạch yêu cầu riêng.
- Lưu thông tin xác thực trên máy chủ của bạn. Môi trường production sử dụng thông tin xác thực ft_live_; sandbox sử dụng ft_test_. Gửi Authorization: Bearer <credential> trong mỗi yêu cầu.
- Đọc /capabilities và /content/taxonomies để lấy ID tác giả và chủ đề được phép. Sử dụng lược đồ trong OpenAPI để chuẩn bị tài liệu.
- Tạo bản nháp, gửi để xét duyệt và theo dõi các sự kiện xuất bản. Tự động hóa của bạn có thể chuẩn bị nội dung hữu ích trong khi biên tập viên vẫn chịu trách nhiệm phát hành.
URL cơ sở: https://www.formtruth.com/integrations/v1
Nội dung & xuất bản
Mỗi bản nháp bên ngoài yêu cầu nhân viên phê duyệt nội dung hiện tại trước khi phát hành. Phương thức viết do con người được khai báo hoặc cài đặt xuất bản tự động không cấp quyền phê duyệt biên tập.
Bài viết, hướng dẫn dành cho người mua và câu hỏi thường gặp dùng chung một định dạng tài liệu và lịch sử phiên bản. Sử dụng /content/posts, /content/guides hoặc /content/faqs cùng quyền tương ứng. Ứng dụng chỉ có thể truy cập nội dung do mình sở hữu; đọc một bản nháp riêng tư khác với đọc nội dung đã xuất bản trên website.
Tài liệu chứa ngôn ngữ nguồn, tác giả thực, chủ đề, nguồn, cùng tiêu đề, đường dẫn, bản tóm tắt và các khối nội dung có cấu trúc đã được bản địa hóa. Bài viết có sự hỗ trợ của AI hoặc được tự động hóa phải xác định mục tiêu người đọc và đóng góp nguyên bản. Các chỉnh sửa bản dịch không được thay đổi thông tin sản phẩm, số liệu, ID hình ảnh, liên kết hoặc kích thước cấu trúc.
Mỗi lần ghi cần một Idempotency-Key. Cập nhật, xem xét, xuất bản và rút lại cũng cần If-Match với ETag được trích dẫn. Chỉnh sửa nội dung đã xem xét sẽ vô hiệu hóa phê duyệt bị ảnh hưởng. Thay đổi nguồn khiến các bản dịch bị ảnh hưởng trở nên lỗi thời cho đến khi được căn chỉnh và xem xét lại.
Hình ảnh đã tải lên vẫn chưa được phê duyệt cho đến khi biên tập viên kiểm tra quyền sử dụng và chất lượng. Các lần phát hành đã xuất bản là những ảnh chụp bất biến. Việc thu hồi sẽ xóa lần phát hành khỏi nội dung công khai, nguồn cấp dữ liệu và kết quả khám phá tìm kiếm.
Tài liệu tham khảo API
Các đường dẫn dưới đây là đường dẫn tương đối so với URL cơ sở. Các tuyến bài viết tương tự cũng áp dụng cho hướng dẫn và câu hỏi thường gặp bằng cách thay đổi nhóm nội dung. Tải OpenAPI để xem đầy đủ lược đồ yêu cầu, phạm vi bắt buộc và phản hồi.
| Phương pháp | Đường dẫn | Mục đích |
|---|
GET | /capabilities | Quyền và giới hạn hiệu lực của bạn |
GET / POST | /content/posts | Liệt kê bài viết / tạo bản nháp |
GET / PATCH | /content/posts/{id} | Đọc bản nháp và ETag / lưu bản sửa đổi |
PUT | /content/posts/by-external-id/{externalId} | Tạo hoặc cập nhật bản nháp theo ID bên ngoài ổn định của bạn |
POST | /content/posts/{id}/submit-review | Gửi để biên tập viên xét duyệt |
POST | /content/posts/{id}/publications | Xuất bản các ngôn ngữ đã được phê duyệt hoặc lên lịch phát hành |
DELETE | /content/posts/{id}/publications/{releaseId} | Thu hồi một lần phát hành kèm lý do |
GET | /content/taxonomies | Chủ đề đang hoạt động và tác giả được phép |
POST | /content/imports | Xếp hàng tối đa 50 bản nháp và kiểm tra kết quả từng mục |
POST | /assets/uploads | Tải lên hình ảnh để kiểm tra quyền sử dụng và chất lượng |
POST | /assets/{id}/complete | Đọc trạng thái tải lên đã lưu |
GET | /jobs/{id} | Đọc kết quả nhập của bạn |
GET / POST | /webhooks/subscriptions | Liệt kê hoặc đăng ký các bộ nhận sự kiện HTTPS của bạn |
POST | /webhooks/subscriptions/{id} | Cập nhật, thay khóa hoặc tắt việc gửi |
Danh mục, đơn hàng và hàng tồn kho được dành cho các mô-đun API về sau. Hiện chưa thể truy cập các chức năng này qua cổng v1.
Lỗi & thử lại
Gửi JSON UTF-8 không nén. Giới hạn nội dung yêu cầu là 2 MiB cho ghi thông thường, 8 MiB cho nhập và 18 MiB cho tải lên hình ảnh được mã hóa; nội dung phải đến trong vòng mười giây. 413 có nghĩa là nội dung quá lớn, 415 có nghĩa là định dạng không được hỗ trợ và 408 có nghĩa là tải lên đã hết thời gian.
Lỗi trả về error.code, error.message, thông tin chi tiết về trường khi phù hợp, retryable và request_id. Giữ lại X-Request-Id để bộ phận hỗ trợ xử lý. 401 có nghĩa là vấn đề về thông tin xác thực; 403 có nghĩa là vấn đề về quyền; 412 có nghĩa là phiên bản đã thay đổi; 422 có nghĩa là nội dung không hợp lệ hoặc chưa sẵn sàng; 428 có nghĩa là thiếu điều kiện tiên quyết.
Với 429, hãy tuân thủ Retry-After. Khi gặp 503 tạm thời hoặc gián đoạn mạng, hãy thử lại với thời gian chờ tăng dần có giới hạn và CÙNG một khóa cùng dữ liệu đầu vào. Payload khác cần một khóa mới. Khi gặp 409, hãy đối soát thao tác trước đó thay vì lặp lại một cách máy móc. Kết quả nhập được trả về theo từng mục qua /jobs/{id}.
Webhook có chữ ký
Sử dụng bộ nhận HTTPS công khai trên cổng 443. Các sự kiện gồm content.review_required, content.published, content.withdrawn, content.publication_failed và import.completed. Xác minh phần thân yêu cầu thô trước khi phân tích cú pháp.
HMAC-SHA256(secret, timestamp + "." + event_id + "." + raw_body)
Đọc X-Formtruth-Timestamp, X-Formtruth-Event-Id và X-Formtruth-Signature. Từ chối các dấu thời gian nằm ngoài năm phút. Header chữ ký có thể chứa nhiều giá trị v1= được phân tách bằng dấu phẩy trong khoảng thời gian xoay khóa kéo dài 24 giờ; chấp nhận mọi chữ ký hợp lệ cho khóa bạn còn lưu giữ.
Lưu nguyên tử ID sự kiện để loại trùng lặp, đưa công việc vào hàng đợi và nhanh chóng trả về 2xx. Việc gửi có thể được thử lại, vì vậy một sự kiện trùng lặp không được phép xuất bản hoặc tính phí hai lần. Giữ bí mật ký tách biệt với thông tin xác thực API.
Ví dụ mã
Chuẩn bị document.json bằng lược đồ KnowledgeDocument và các ID phân loại thực được cho phép. Thiết lập FORMTRUTH_API_TOKEN an toàn trên máy chủ của bạn. Các ví dụ này tạo một bản nháp; việc xuất bản vẫn cần quy trình xét duyệt.
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.