Starta en integration
- Be en administratör att skapa din applikation i Admin → API och integrationer. Varje applikation har en separat miljö, innehållsomfattning, kanaler, språk, författare, giltighetstid och anropskvot.
- Lagra autentiseringsuppgiften på din server. Produktion använder ft_live_-autentiseringsuppgifter och sandlådan använder ft_test_. Skicka Authorization: Bearer <credential> med varje anrop.
- Läs /capabilities och /content/taxonomies för att hämta tillåtna ID:n för författare och ämnen. Använd schemat i OpenAPI för att förbereda ditt dokument.
- Skapa ett utkast, skicka det för granskning och lyssna efter publiceringshändelser. Din automatisering kan förbereda användbart innehåll medan en redaktör fortfarande ansvarar för publiceringen.
Bas-URL: https://www.formtruth.com/integrations/v1
Innehåll och publicering
Varje externt utkast kräver personalgodkännande av dess aktuella innehåll före publicering. En deklarerad mänsklig redigeringsmetod eller automatisk publiceringsinställning ger inte redaktionellt godkännande.
Inlägg, köpguider och FAQ:er använder samma dokumentformat och versionshistorik. Använd /content/posts, /content/guides eller /content/faqs med motsvarande behörighet. Applikationer kan bara komma åt innehåll de äger; att läsa ett privat utkast är något annat än att läsa den publicerade webbplatsen.
Dokument innehåller ett källspråk, en verklig författare, ämnen, källor samt lokaliserade titlar, sluggar, sammanfattningar och strukturerade innehållsblock. AI-assisterade eller automatiserade artiklar måste tydliggöra läsarens mål och det ursprungliga bidraget. Översättningsändringar får inte ändra produktfakta, siffror, bild-ID:n, länkar eller strukturella mått.
Varje skrivning behöver en Idempotency-Key. Uppdateringar, granskning, publicering och återkallelse behöver också If-Match med citerad ETag. Redigering av granskat innehåll ogiltigförklarar det berörda godkännandet. Ändring av källan gör berörda översättningar inaktuella tills de justerats och granskats igen.
Uppladdade bilder förblir icke godkända tills en redaktör har kontrollerat rättigheter och kvalitet. Publicerade versioner är oföränderliga ögonblicksbilder. Ett tillbakadragande tar bort publiceringen från offentligt innehåll, flöden och sökresultat.
API-referens
Sökvägarna nedan är relativa till bas-URL:en. Samma artikelrutter gäller för guider och FAQ:er om du ändrar innehållsfamiljen. Ladda ner OpenAPI för fullständiga begärandescheman, obligatoriska omfattningar och svar.
| Metod | Sökväg | Syfte |
|---|
GET | /capabilities | Dina aktuella behörigheter och begränsningar |
GET / POST | /content/posts | Lista dina artiklar / skapa ett utkast |
GET / PATCH | /content/posts/{id} | Läs utkastet och ETag / spara en version |
PUT | /content/posts/by-external-id/{externalId} | Skapa eller uppdatera ett utkast med ditt stabila externa ID |
POST | /content/posts/{id}/submit-review | Skicka för redaktionell granskning |
POST | /content/posts/{id}/publications | Publicera godkända språk eller schemalägg en publicering |
DELETE | /content/posts/{id}/publications/{releaseId} | Dra tillbaka en publicering med en motivering |
GET | /content/taxonomies | Aktiva ämnen och tillåtna författare |
POST | /content/imports | Lägg upp till 50 utkast i kön och granska resultat per objekt |
POST | /assets/uploads | Ladda upp en bild för granskning av rättigheter och kvalitet |
POST | /assets/{id}/complete | Läs den sparade uppladdningsstatusen |
GET | /jobs/{id} | Läs resultatet av din import |
GET / POST | /webhooks/subscriptions | Lista eller registrera dina HTTPS-händelsemottagare |
POST | /webhooks/subscriptions/{id} | Uppdatera, rotera nyckeln eller inaktivera leveransen |
Katalog, order och lager är reserverade för senare API-moduler. De är ännu inte tillgängliga via denna v1-gateway.
Fel och nya försök
Skicka okomprimerad UTF-8 JSON. Begärandekroppsgränser är 2 MiB för vanliga skrivningar, 8 MiB för importer och 18 MiB för kodade bilduppladdningar; kroppen måste ankomma inom tio sekunder. Ett 413 betyder att kroppen är för stor, 415 betyder ett format som inte stöds och 408 betyder att en uppladdning fick timeout.
Fel returnerar error.code, error.message, fältdetaljer när det är relevant, retryable och request_id. Spara X-Request-Id för supportärenden. Ett 401 betyder ett problem med autentiseringsuppgiften; 403 betyder otillräcklig behörighet; 412 betyder en ändrad version; 422 betyder ogiltigt eller inte färdigt innehåll; 428 betyder ett saknat förkrav.
För 429 ska du respektera Retry-After. Vid ett tillfälligt 503 eller nätverksavbrott ska du försöka igen med begränsad backoff och SAMMA nyckel och indata. En annan nyttolast kräver en ny nyckel. Vid 409 ska du stämma av den föregående åtgärden i stället för att upprepa den blint. Importer returnerar resultat per objekt via /jobs/{id}.
Signerade webhooks
Använd en offentlig HTTPS-mottagare på port 443. Händelser omfattar content.review_required, content.published, content.withdrawn, content.publication_failed och import.completed. Verifiera den råa begärandekroppen innan du tolkar den.
HMAC-SHA256(secret, timestamp + "." + event_id + "." + raw_body)
Läs X-Formtruth-Timestamp, X-Formtruth-Event-Id och X-Formtruth-Signature. Avvisa tidsstämplar som ligger utanför fem minuter. Signaturhuvudet kan innehålla flera kommaseparerade v1=-värden under 24-timmarsperioden för nyckelrotation; godkänn alla giltiga signaturer för nycklar du behåller.
Spara atomiskt händelse-ID:t för deduplicering, lägg arbetet i kö och returnera 2xx snabbt. Leveransen kan försöka igen, så en dubbletthändelse får inte publicera eller debitera två gånger. Håll signeringshemligheten åtskild från API-autentiseringsuppgiften.
Kodexempel
Förbered document.json med schemat KnowledgeDocument och verkliga tillåtna taxonomi-ID:n. Ange FORMTRUTH_API_TOKEN säkert på din server. Dessa exempel skapar ett utkast; publicering kräver fortfarande granskningsflödet.
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.