インテグレーションを開始
- 管理者に依頼して、Admin → APIとインテグレーションでアプリケーションを作成してもらってください。各アプリケーションには、個別の環境、コンテンツスコープ、チャネル、言語、著者、有効期限、リクエスト割り当てがあります。
- 認証情報はサーバーに保存してください。本番環境ではft_live_認証情報、サンドボックスではft_test_を使用します。各リクエストにAuthorization: Bearer <credential>を付けて送信してください。
- /capabilitiesと/content/taxonomiesを読み取り、許可された著者IDとトピックIDを取得します。OpenAPIのスキーマを使ってドキュメントを準備してください。
- 下書きを作成して審査に提出し、公開イベントを受信します。編集者が公開に責任を持つ一方で、自動化によって有用なコンテンツを準備できます。
ベースURL: https://www.formtruth.com/integrations/v1
コンテンツと公開
外部から作成された下書きはすべて、公開前に現在の内容についてスタッフの承認が必要です。人による執筆方法または自動公開設定を申告しても、編集上の承認が付与されるわけではありません。
記事、バイヤーガイド、FAQは、ひとつのドキュメント形式とバージョン履歴を共有します。対応する権限で/content/posts、/content/guides、または/content/faqsを使用してください。アプリケーションがアクセスできるのは所有するコンテンツだけです。非公開の下書きの読み取りと、公開済みウェブサイトの読み取りは異なります。
ドキュメントには、ソース言語、実在する著者、トピック、ソース、ローカライズされたタイトル、スラッグ、概要、構造化された本文ブロックが含まれます。AI支援または自動生成の記事では、読者の目的と独自の貢献を明示する必要があります。翻訳の編集によって、製品情報、数値、画像ID、リンク、構造上の寸法を変更することはできません。
すべての書き込みにはIdempotency-Keyが必要です。更新、レビュー、公開、取り下げには、引用符付きETagを指定したIf-Matchも必要です。レビュー済みのコンテンツを編集すると、影響を受ける承認は無効になります。ソースを変更すると、影響を受ける翻訳は、再度整合させてレビューされるまで古い状態になります。
アップロードした画像は、編集者が権利と品質を確認するまで未承認のままです。公開済みのリリースは変更できないスナップショットです。取り下げると、公開コンテンツ、フィード、検索結果からリリースが削除されます。
APIリファレンス
以下のパスはベースURLからの相対パスです。コンテンツファミリーを変更すれば、同じ記事用ルートをガイドと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では、以前の操作を照合してから、無条件に繰り返さないでください。インポート結果は/content/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スキーマを使用してdocument.jsonを準備し、実際に許可されたタクソノミーIDを設定してください。サーバー上で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.