התחלת אינטגרציה
- בקשו ממנהל מערכת ליצור את היישום שלכם ב־Admin → API & integrations. לכל יישום יש סביבה נפרדת, היקפי תוכן, ערוצים, שפות, מחברים, תאריך תפוגה ומכסת בקשות.
- שמרו את פרטי האימות בשרת שלכם. בסביבת ייצור משתמשים בפרטי ft_live_; בסביבת sandbox משתמשים בפרטי ft_test_. שלחו Authorization: Bearer <credential> בכל בקשה.
- קראו את /capabilities ואת /content/taxonomies כדי לקבל את מזהי המחברים והנושאים המותרים. השתמשו בסכמה שב־OpenAPI כדי להכין את המסמך.
- צרו טיוטה, שלחו אותה לבדיקה והאזינו לאירועי פרסום. האוטומציה שלכם יכולה להכין תוכן שימושי, בעוד שהעורך נשאר אחראי לפרסום.
כתובת בסיס: https://www.formtruth.com/integrations/v1
תוכן ופרסום
כל טיוטה חיצונית דורשת אישור צוות של התוכן הנוכחי שלה לפני פרסום. שיטת כתיבה אנושית מוצהרת או הגדרת פרסום אוטומטי אינן מעניקות אישור עריכה.
פוסטים, מדריכי קנייה ושאלות נפוצות משתמשים באותו פורמט מסמך ובהיסטוריית גרסאות אחת. השתמשו ב־/content/posts, ב־/content/guides או ב־/content/faqs עם ההרשאה המתאימה. יישומים יכולים לגשת רק לתוכן שבבעלותם; קריאת טיוטה פרטית שונה מקריאת האתר שפורסם.
המסמכים כוללים שפת מקור, מחבר אמיתי, נושאים, מקורות וכותרות, נתיבי כתובת, תקצירים ובלוקי גוף מובנים שעברו לוקליזציה. מאמרים שנוצרו בסיוע AI או באופן אוטומטי חייבים לזהות את מטרת הקורא ואת התרומה המקורית שלהם. עריכות תרגום אינן יכולות לשנות עובדות על המוצר, מספרים, מזהי תמונות, קישורים או ממדים מבניים.
כל כתיבה דורשת Idempotency-Key. עדכונים, סקירה, פרסום ומשיכה דורשים גם If-Match עם ה-ETag במרכאות. עריכת תוכן שנסקר מבטלת את האישור הרלוונטי. שינוי המקור הופך תרגומים רלוונטיים ללא עדכניים עד להתאמה וסקירה מחודשת.
תמונות שהועלו נשארות ללא אישור עד שעורך בודק את הזכויות והאיכות. פרסומים שפורסמו הם תמונות מצב בלתי ניתנות לשינוי. משיכת פרסום מסירה אותו מהתוכן הציבורי, מהעדכונים ומגילוי בחיפוש.
מדריך API
הנתיבים שלהלן הם יחסיים לכתובת הבסיס. אותם נתיבי מאמר חלים גם על מדריכים ושאלות נפוצות, באמצעות החלפת משפחת התוכן. הורידו את 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 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}.
Webhook חתום
השתמשו במקבל 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.