একটি ইন্টিগ্রেশন শুরু করুন
- Admin → API & integrations-এ আপনার অ্যাপ্লিকেশন তৈরি করতে একজন প্রশাসককে বলুন। প্রতিটি অ্যাপ্লিকেশনের আলাদা পরিবেশ, কনটেন্ট স্কোপ, চ্যানেল, ভাষা, লেখক, মেয়াদোত্তীর্ণতার সময় ও অনুরোধের কোটা থাকে।
- ক্রেডেনশিয়ালটি আপনার সার্ভারে সংরক্ষণ করুন। প্রোডাকশনে ft_live_ ক্রেডেনশিয়াল এবং স্যান্ডবক্সে ft_test_ ব্যবহার করা হয়। প্রতিটি অনুরোধের সঙ্গে Authorization: Bearer <credential> পাঠান।
- অনুমোদিত লেখক ও বিষয়ের ID পেতে /capabilities এবং /content/taxonomies পড়ুন। আপনার ডকুমেন্ট প্রস্তুত করতে OpenAPI-তে থাকা স্কিমা ব্যবহার করুন।
- একটি খসড়া তৈরি করুন, পর্যালোচনার জন্য জমা দিন এবং প্রকাশনা-সংক্রান্ত ইভেন্ট শুনুন। আপনার অটোমেশন কার্যকর কনটেন্ট প্রস্তুত করতে পারে, তবে প্রকাশনার দায়িত্ব একজন সম্পাদকের কাছেই থাকবে।
বেস URL: https://www.formtruth.com/integrations/v1
কনটেন্ট ও প্রকাশনা
প্রতিটি বহিরাগত খসড়া প্রকাশের আগে তার বর্তমান বিষয়বস্তুর স্টাফ অনুমোদন প্রয়োজন। একটি ঘোষিত মানব লেখন পদ্ধতি বা স্বয়ংক্রিয় প্রকাশনা সেটিং সম্পাদকীয় অনুমোদন প্রদান করে না।
পোস্ট, ক্রেতা-নির্দেশিকা ও FAQ একই ডকুমেন্ট ফরম্যাট এবং সংস্করণ ইতিহাস ব্যবহার করে। সংশ্লিষ্ট অনুমতি অনুযায়ী /content/posts, /content/guides অথবা /content/faqs ব্যবহার করুন। অ্যাপ্লিকেশন কেবল নিজেদের মালিকানাধীন কনটেন্টে অ্যাক্সেস করতে পারে; একটি ব্যক্তিগত খসড়া পড়া এবং প্রকাশিত ওয়েবসাইট পড়া এক বিষয় নয়।
ডকুমেন্টে একটি উৎস ভাষা, প্রকৃত লেখক, বিষয়, উৎস এবং স্থানীয়কৃত শিরোনাম, স্লাগ, সারাংশ ও কাঠামোবদ্ধ বডি ব্লক থাকে। AI-সহায়তাপ্রাপ্ত বা স্বয়ংক্রিয় নিবন্ধে পাঠকের লক্ষ্য ও মৌলিক অবদান উল্লেখ করতে হবে। অনুবাদ সম্পাদনা পণ্যের তথ্য, সংখ্যা, ছবি ID, লিংক বা কাঠামোগত মাত্রা পরিবর্তন করতে পারে না।
প্রতিটি লেখার জন্য একটি Idempotency-Key প্রয়োজন। আপডেট, পর্যালোচনা, প্রকাশনা এবং প্রত্যাহারের জন্যও উদ্ধৃত ETag সহ If-Match প্রয়োজন। পর্যালোচিত বিষয়বস্তু সম্পাদনা করলে প্রভাবিত অনুমোদন বাতিল হয়ে যায়। উৎস পরিবর্তন করলে প্রভাবিত অনুবাদগুলি সারিবদ্ধ এবং পুনরায় পর্যালোচনা না হওয়া পর্যন্ত পুরনো হয়ে যায়।
আপলোড করা ছবি কোনো সম্পাদক অধিকার ও মান যাচাই না করা পর্যন্ত অননুমোদিত থাকে। প্রকাশিত রিলিজগুলো অপরিবর্তনীয় স্ন্যাপশট। প্রত্যাহার করলে প্রকাশ্য কনটেন্ট, ফিড ও সার্চে প্রদর্শন থেকে রিলিজটি সরিয়ে নেওয়া হয়।
API রেফারেন্স
নিচের পাথগুলো বেস URL-এর আপেক্ষিক। কনটেন্ট ফ্যামিলি পরিবর্তন করে guides ও FAQ-এর ক্ষেত্রেও একই নিবন্ধের রুট প্রযোজ্য। সম্পূর্ণ রিকোয়েস্ট স্কিমা, প্রয়োজনীয় স্কোপ ও প্রতিক্রিয়ার জন্য 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 গেটওয়ের মাধ্যমে উপলভ্য নয়।
ত্রুটি ও পুনরায় চেষ্টা
অসংকুচিত UTF-8 JSON পাঠান। অনুরোধ বডি সীমা সাধারণ লেখার জন্য 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
443 পোর্টে একটি পাবলিক HTTPS রিসিভার ব্যবহার করুন। ইভেন্টের মধ্যে রয়েছে 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 পড়ুন। পাঁচ মিনিটের বেশি পুরোনো বা ভবিষ্যতের টাইমস্ট্যাম্প প্রত্যাখ্যান করুন। 24 ঘণ্টার কী রোটেশন সময়কালে স্বাক্ষর হেডারে কমা দিয়ে পৃথক একাধিক v1= মান থাকতে পারে; আপনার কাছে সংরক্ষিত কীগুলোর যেকোনো একটি দিয়ে বৈধ স্বাক্ষর গ্রহণ করুন।
ডিডুপ্লিকেশনের জন্য ইভেন্ট ID পারমাণবিকভাবে সংরক্ষণ করুন, কাজটি কিউতে রাখুন এবং দ্রুত 2xx রিটার্ন করুন। ডেলিভারি পুনরায় চেষ্টা করা হতে পারে, তাই একই ইভেন্ট যেন দুইবার প্রকাশনা বা চার্জ না করে। সাইনিং সিক্রেট API ক্রেডেনশিয়াল থেকে আলাদা রাখুন।
কোডের উদাহরণ
KnowledgeDocument স্কিমা এবং প্রকৃত অনুমোদিত ট্যাক্সোনমি ID ব্যবহার করে document.json প্রস্তুত করুন। আপনার সার্ভারে 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.