เริ่มการเชื่อมต่อ
- ขอให้ผู้ดูแลระบบสร้างแอปพลิเคชันของคุณใน Admin → API & integrations แอปพลิเคชันแต่ละรายการจะมีสภาพแวดล้อม ขอบเขตเนื้อหา ช่องทาง ภาษา ผู้เขียน วันหมดอายุ และโควตาคำขอแยกจากกัน
- จัดเก็บข้อมูลรับรองไว้บนเซิร์ฟเวอร์ของคุณ ระบบ Production ใช้ข้อมูลรับรอง ft_live_ ส่วน Sandbox ใช้ ft_test_ ส่ง Authorization: Bearer <credential> ไปกับทุกคำขอ
- อ่าน /capabilities และ /content/taxonomies เพื่อรับรหัสผู้เขียนและหัวข้อที่ได้รับอนุญาต ใช้สคีมาใน OpenAPI เพื่อเตรียมเอกสารของคุณ
- สร้างฉบับร่าง ส่งเพื่อตรวจทาน และรอฟังเหตุการณ์การเผยแพร่ ระบบอัตโนมัติของคุณสามารถเตรียมเนื้อหาที่เป็นประโยชน์ได้ ขณะที่บรรณาธิการยังคงรับผิดชอบการเผยแพร่
URL พื้นฐาน: https://www.formtruth.com/integrations/v1
เนื้อหาและการเผยแพร่
แบบร่างภายนอกทุกฉบับต้องได้รับการอนุมัติจากเจ้าหน้าที่สำหรับเนื้อหาปัจจุบันก่อนเผยแพร่ การประกาศวิธีการเขียนโดยมนุษย์หรือการตั้งค่าการเผยแพร่อัตโนมัติไม่ได้ให้การอนุมัติทางบรรณาธิการ
โพสต์ คู่มือผู้ซื้อ และคำถามที่พบบ่อยใช้รูปแบบเอกสารและประวัติเวอร์ชันเดียวกัน ใช้ /content/posts, /content/guides หรือ /content/faqs พร้อมสิทธิ์ที่ตรงกัน แอปพลิเคชันเข้าถึงได้เฉพาะเนื้อหาที่ตนเป็นเจ้าของ การอ่านฉบับร่างส่วนตัวแตกต่างจากการอ่านเว็บไซต์ที่เผยแพร่แล้ว
เอกสารประกอบด้วยภาษาต้นฉบับ ผู้เขียนจริง หัวข้อ แหล่งที่มา รวมถึงชื่อเรื่อง สลัก URL สรุป และบล็อกเนื้อหาแบบมีโครงสร้างในภาษาต่าง ๆ บทความที่สร้างโดยมี AI ช่วยหรือสร้างโดยอัตโนมัติต้องระบุเป้าหมายของผู้อ่านและส่วนที่สร้างขึ้นใหม่ การแก้ไขคำแปลต้องไม่เปลี่ยนข้อเท็จจริงของผลิตภัณฑ์ ตัวเลข รหัสรูปภาพ ลิงก์ หรือมิติของโครงสร้าง
การเขียนแต่ละครั้งต้องมี 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} | สร้างหรืออัปเดตแบบร่างด้วย ID ภายนอกที่คงที่ของคุณ |
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 MiB สำหรับการเขียนทั่วไป 8 MiB สำหรับการนำเข้า และ 18 MiB สำหรับการอัปโหลดภาพที่เข้ารหัส เนื้อหาต้องมาถึงภายในสิบวินาที 413 หมายความว่าเนื้อหาใหญ่เกินไป 415 หมายความว่ารูปแบบไม่รองรับ และ 408 หมายความว่าการอัปโหลดหมดเวลา
ข้อผิดพลาดจะแสดง error.code, error.message รายละเอียดฟิลด์เมื่อมี และข้อมูลว่าลองใหม่ได้หรือไม่ รวมถึง 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.