통합 시작
- 관리자에게 Admin → API 및 통합에서 애플리케이션을 생성해 달라고 요청하세요. 각 애플리케이션에는 별도의 환경, 콘텐츠 범위, 채널, 언어, 작성자, 만료 기간 및 요청 할당량이 적용됩니다.
- 자격 증명은 서버에 저장하세요. 프로덕션에서는 ft_live_ 자격 증명을 사용하고, 샌드박스에서는 ft_test_를 사용하세요. 각 요청에 Authorization: Bearer <credential>를 전송하세요.
- /capabilities 및 /content/taxonomies를 읽어 허용된 작성자 및 주제 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을 전송하세요. 요청 본문 제한은 일반 작성의 경우 2MiB, 가져오기의 경우 8MiB, 인코딩된 이미지 업로드의 경우 18MiB이며, 본문은 십 초 이내에 도착해야 합니다. 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}를 통해 항목별 결과를 반환합니다.
서명된 웹훅
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.