بدء عملية تكامل
- اطلب من أحد المسؤولين إنشاء تطبيقك في المسؤول → واجهة برمجة التطبيقات والتكاملات. لكل تطبيق بيئة ونطاقات محتوى وقنوات ولغات ومؤلفون وانتهاء صلاحية وحصة طلبات منفصلة.
- خزّن بيانات الاعتماد على خادمك. تستخدم بيئة الإنتاج بيانات اعتماد ft_live_؛ وتستخدم البيئة التجريبية ft_test_. أرسل Authorization: Bearer <credential> مع كل طلب.
- اقرأ /capabilities و/content/taxonomies للحصول على معرّفات المؤلفين والمواضيع المسموح بها. استخدم المخطط في OpenAPI لإعداد مستندك.
- أنشئ مسودة، وأرسلها للمراجعة، واستمع إلى أحداث النشر. يمكن لأتمتتك إعداد محتوى مفيد، بينما يظل المحرّر مسؤولًا عن الإصدار.
عنوان URL الأساسي: https://www.formtruth.com/integrations/v1
المحتوى والنشر
تتطلب كل مسودة خارجية موافقة الموظفين على محتواها الحالي قبل الإصدار. لا تمنح طريقة التأليف البشري المعلنة أو إعداد النشر الآلي الموافقة التحريرية.
تستخدم المنشورات وأدلة المشترين والأسئلة الشائعة تنسيق مستند واحدًا وسجل إصدارات واحدًا. استخدم /content/posts أو /content/guides أو /content/faqs مع الإذن المقابل. لا يمكن للتطبيقات الوصول إلا إلى المحتوى الذي تملكه؛ وقراءة مسودة خاصة تختلف عن قراءة الموقع المنشور.
تحتوي المستندات على لغة مصدر، ومؤلف حقيقي، ومواضيع، ومصادر، وعناوين وسمات URL وملخصات وكتل نص منظّمة محلية. يجب أن تحدد المقالات المدعومة بالذكاء الاصطناعي أو المؤتمتة هدف قارئها ومساهمتها الأصلية. لا يمكن لتعديلات الترجمة تغيير حقائق المنتجات أو الأرقام أو معرّفات الصور أو الروابط أو الأبعاد البنيوية.
تحتاج كل عملية كتابة إلى Idempotency-Key. تحتاج التحديثات والمراجعة والنشر والسحب أيضًا إلى If-Match مع ETag المقتبس. يؤدي تحرير المحتوى المُراجع إلى إبطال الموافقة المتأثرة. يؤدي تغيير المصدر إلى جعل الترجمات المتأثرة قديمة حتى يتم مواءمتها ومراجعتها مرة أخرى.
تبقى الصور المحمّلة غير معتمدة إلى أن يتحقق محرّر من الحقوق والجودة. الإصدارات المنشورة لقطات ثابتة غير قابلة للتغيير. يؤدي السحب إلى إزالة الإصدار من المحتوى العام وخلاصات المحتوى ونتائج البحث.
مرجع واجهة برمجة التطبيقات
المسارات أدناه نسبية إلى عنوان 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} | تحديث المفتاح أو تدويره أو تعطيل التسليم |
يُحتفظ بالكتالوج والطلبات والمخزون لوحدات واجهة برمجة التطبيقات اللاحقة. وهي غير متاحة بعد عبر بوابة v1.
الأخطاء وإعادة المحاولات
أرسل JSON بترميز UTF-8 غير مضغوط. حدود نص الطلب هي 2 ميبي بايت للكتابات العادية، و8 ميبي بايت للاستيرادات و18 ميبي بايت لتحميلات الصور المشفرة؛ يجب أن يصل النص خلال عشر ثوانٍ. يعني 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 بسرعة. يمكن إعادة محاولة التسليم، لذلك يجب ألا يؤدي الحدث المكرر إلى النشر أو تحصيل الرسوم مرتين. احتفظ بسر التوقيع منفصلًا عن بيانات اعتماد واجهة برمجة التطبيقات.
أمثلة برمجية
أعدّ 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.