Commencer une intégration
- Demandez à un administrateur de créer votre application dans Admin → API et intégrations. Chaque application possède un environnement, des champs de contenu, des canaux, des langues, des auteurs, une expiration et un quota de requêtes distincts.
- Stockez l’identifiant d’accès sur votre serveur. La production utilise des identifiants ft_live_ ; l’environnement bac à sable utilise ft_test_. Envoyez Authorization: Bearer <credential> avec chaque requête.
- Lisez /capabilities et /content/taxonomies pour obtenir les identifiants d’auteurs et de thématiques autorisés. Utilisez le schéma d’OpenAPI pour préparer votre document.
- Créez un brouillon, soumettez-le à la révision et écoutez les événements de publication. Votre automatisation peut préparer du contenu utile tandis qu’un éditeur reste responsable de la publication.
URL de base: https://www.formtruth.com/integrations/v1
Contenu et publication
Chaque brouillon externe nécessite l'approbation du personnel pour son contenu actuel avant publication. Une méthode de rédaction humaine déclarée ou un paramètre de publication automatisée n'accorde pas l'approbation éditoriale.
Les articles, guides d’achat et FAQ partagent un même format de document et un même historique des versions. Utilisez /content/posts, /content/guides ou /content/faqs avec l’autorisation correspondante. Les applications ne peuvent accéder qu’au contenu dont elles sont propriétaires ; la lecture d’un brouillon privé diffère de la lecture du site publié.
Les documents contiennent une langue source, un véritable auteur, des thématiques, des sources ainsi que des titres, identifiants, résumés et blocs de contenu structurés localisés. Les articles assistés par IA ou automatisés doivent préciser l’objectif de lecture et leur contribution originale. Les modifications de traduction ne peuvent pas changer les caractéristiques des produits, les nombres, les identifiants d’image, les liens ni les dimensions structurelles.
Chaque écriture nécessite une Idempotency-Key. Les mises à jour, révisions, publications et retraits nécessitent également If-Match avec l'ETag entre guillemets. Modifier du contenu révisé invalide l'approbation concernée. Changer la source rend les traductions concernées obsolètes jusqu'à leur alignement et révision.
Les images téléversées restent non approuvées jusqu’à ce qu’un éditeur vérifie les droits et la qualité. Les publications mises en ligne sont des instantanés immuables. Le retrait supprime la publication du contenu public, des flux et des résultats de recherche.
Référence de l’API
Les chemins ci-dessous sont relatifs à l’URL de base. Les mêmes routes d’articles s’appliquent aux guides et aux FAQ en changeant la famille de contenu. Téléchargez OpenAPI pour consulter les schémas complets des requêtes, les champs d’autorisation requis et les réponses.
| Méthode | Chemin | Objectif |
|---|
GET | /capabilities | Vos autorisations et limites effectives |
GET / POST | /content/posts | Lister vos articles / créer un brouillon |
GET / PATCH | /content/posts/{id} | Lire le brouillon et l’ETag / enregistrer une révision |
PUT | /content/posts/by-external-id/{externalId} | Créer ou mettre à jour un brouillon par votre identifiant externe stable |
POST | /content/posts/{id}/submit-review | Soumettre à la révision éditoriale |
POST | /content/posts/{id}/publications | Publier les langues approuvées ou programmer une publication |
DELETE | /content/posts/{id}/publications/{releaseId} | Retirer une publication avec un motif |
GET | /content/taxonomies | Thématiques actives et auteurs autorisés |
POST | /content/imports | Mettre en file jusqu’à 50 brouillons et consulter les résultats par élément |
POST | /assets/uploads | Téléverser une image pour vérification des droits et de la qualité |
POST | /assets/{id}/complete | Lire l’état enregistré du téléversement |
GET | /jobs/{id} | Lire les résultats de votre importation |
GET / POST | /webhooks/subscriptions | Lister ou enregistrer vos récepteurs d'événements HTTPS |
POST | /webhooks/subscriptions/{id} | Mettre à jour, faire pivoter la clé ou désactiver la distribution |
Le catalogue, les commandes et les stocks sont réservés à de futurs modules de l’API. Ils ne sont pas encore disponibles via cette passerelle v1.
Erreurs et nouvelles tentatives
Envoyez du JSON UTF-8 non compressé. Les limites du corps de requête sont 2 Mio pour les écritures ordinaires, 8 Mio pour les importations et 18 Mio pour les téléchargements d'images encodées ; le corps doit arriver dans les dix secondes. Un 413 signifie que le corps est trop volumineux, 415 signifie un format non pris en charge et 408 signifie un téléchargement expiré.
Les erreurs renvoient error.code, error.message, les détails des champs lorsque cela s’applique, retryable et request_id. Conservez X-Request-Id pour le support. Un 401 indique un problème d’identifiant d’accès ; 403 indique un problème d’autorisation ; 412 indique une version modifiée ; 422 indique un contenu invalide ou non prêt ; 428 indique une condition préalable manquante.
Pour 429, respectez Retry-After. En cas de 503 temporaire ou d’interruption réseau, réessayez avec un délai progressif plafonné et la MÊME clé ainsi que les MÊMES données d’entrée. Une charge utile différente nécessite une nouvelle clé. En cas de 409, rapprochez l’opération précédente au lieu de la répéter sans vérification. Les importations renvoient les résultats par élément via /jobs/{id}.
Webhooks signés
Utilisez un récepteur HTTPS public sur le port 443. Les événements comprennent content.review_required, content.published, content.withdrawn, content.publication_failed et import.completed. Vérifiez le corps brut de la requête avant de l’analyser.
HMAC-SHA256(secret, timestamp + "." + event_id + "." + raw_body)
Lisez X-Formtruth-Timestamp, X-Formtruth-Event-Id et X-Formtruth-Signature. Rejetez les horodatages datant de plus de cinq minutes. L’en-tête de signature peut contenir plusieurs valeurs v1= séparées par des virgules pendant la fenêtre de rotation de la clé de 24 heures ; acceptez toute signature valide correspondant à une clé que vous avez conservée.
Enregistrez atomiquement l’identifiant de l’événement pour la déduplication, mettez le traitement en file et renvoyez rapidement 2xx. La distribution peut être réessayée : un événement en double ne doit donc pas publier ni facturer deux fois. Conservez le secret de signature séparément de l’identifiant d’accès à l’API.
Exemples de code
Préparez document.json à l’aide du schéma KnowledgeDocument et d’identifiants réels de taxonomie autorisés. Définissez FORMTRUTH_API_TOKEN de manière sécurisée sur votre serveur. Ces exemples créent un brouillon ; la publication nécessite toujours le processus de révision.
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.