Avvia un’integrazione
- Chiedi a un amministratore di creare la tua applicazione in Amministrazione → API e integrazioni. Ogni applicazione ha un ambiente, ambiti dei contenuti, canali, lingue, autori, scadenza e quota di richieste separati.
- Conserva la credenziale sul tuo server. La produzione utilizza credenziali ft_live_; l’ambiente sandbox utilizza ft_test_. Invia Authorization: Bearer <credential> con ogni richiesta.
- Leggi /capabilities e /content/taxonomies per ottenere gli ID degli autori e degli argomenti autorizzati. Utilizza lo schema in OpenAPI per preparare il documento.
- Crea una bozza, inviala alla revisione e ascolta gli eventi di pubblicazione. La tua automazione può preparare contenuti utili, mentre un editor resta responsabile della pubblicazione.
URL di base: https://www.formtruth.com/integrations/v1
Contenuti e pubblicazione
Ogni bozza esterna richiede l’approvazione del personale sul contenuto corrente prima della pubblicazione. Dichiarare un metodo di redazione umana o un’impostazione di pubblicazione automatizzata non equivale ad approvazione editoriale.
Articoli, guide per acquirenti e FAQ condividono un unico formato di documento e una cronologia delle versioni. Utilizza /content/posts, /content/guides o /content/faqs con l’autorizzazione corrispondente. Le applicazioni possono accedere solo ai contenuti di cui sono proprietarie; leggere una bozza privata è diverso dal leggere il sito pubblicato.
I documenti contengono una lingua di origine, un autore reale, argomenti, fonti e titoli, slug, riepiloghi e blocchi del corpo strutturati localizzati. Gli articoli assistiti dall’IA o automatizzati devono identificare l’obiettivo del lettore e il contributo originale. Le modifiche di traduzione non possono cambiare dati dei prodotti, numeri, ID delle immagini, link o dimensioni strutturali.
Ogni operazione di scrittura richiede un Idempotency-Key. Anche gli aggiornamenti, la revisione, la pubblicazione e il ritiro richiedono If-Match con l’ETag tra virgolette. La modifica di contenuti già revisionati invalida l’approvazione interessata. Se cambi la fonte, le traduzioni interessate diventano obsolete finché non vengono aggiornate e sottoposte di nuovo a revisione.
Le immagini caricate restano non approvate finché un editor non ne verifica i diritti e la qualità. Le pubblicazioni rilasciate sono istantanee immutabili. Il ritiro rimuove la pubblicazione dai contenuti pubblici, dai feed e dai risultati di ricerca.
Riferimento API
I percorsi riportati di seguito sono relativi all’URL di base. Gli stessi percorsi degli articoli si applicano alle guide e alle FAQ modificando la famiglia di contenuti. Scarica OpenAPI per gli schemi completi delle richieste, gli ambiti obbligatori e le risposte.
| Metodo | Percorso | Scopo |
|---|
GET | /capabilities | Le autorizzazioni e i limiti effettivi |
GET / POST | /content/posts | Elenca i tuoi articoli / crea una bozza |
GET / PATCH | /content/posts/{id} | Leggi la bozza e l’ETag / salva una revisione |
PUT | /content/posts/by-external-id/{externalId} | Crea o aggiorna una bozza usando il tuo ID esterno stabile |
POST | /content/posts/{id}/submit-review | Invia alla revisione editoriale |
POST | /content/posts/{id}/publications | Pubblica le lingue approvate o pianifica una pubblicazione |
DELETE | /content/posts/{id}/publications/{releaseId} | Ritira una pubblicazione indicando il motivo |
GET | /content/taxonomies | Argomenti attivi e autori autorizzati |
POST | /content/imports | Metti in coda fino a 50 bozze e controlla i risultati dei singoli elementi |
POST | /assets/uploads | Carica un’immagine per la verifica dei diritti e della qualità |
POST | /assets/{id}/complete | Leggi lo stato persistente del caricamento |
GET | /jobs/{id} | Leggi i risultati dell’importazione |
GET / POST | /webhooks/subscriptions | Elenca o registra i destinatari degli eventi HTTPS |
POST | /webhooks/subscriptions/{id} | Aggiorna, ruota la chiave o disabilita la consegna |
Catalogo, ordini e inventario sono riservati a moduli API futuri. Non sono ancora disponibili tramite questo gateway v1.
Errori e nuovi tentativi
Invia JSON UTF-8 non compresso. I limiti del corpo della richiesta sono 2 MiB per le normali operazioni di scrittura, 8 MiB per le importazioni e 18 MiB per i caricamenti di immagini codificate; il corpo deve essere ricevuto entro dieci secondi. Un 413 indica che il corpo è troppo grande, un 415 indica un formato non supportato e un 408 indica che il caricamento è scaduto.
Gli errori restituiscono error.code, error.message, i dettagli dei campi quando applicabili, retryable e request_id. Conserva X-Request-Id per l’assistenza. Un 401 indica un problema con la credenziale; 403 indica un problema di autorizzazione; 412 indica una versione cambiata; 422 indica contenuti non validi o non pronti; 428 indica una precondizione mancante.
Per 429, rispetta Retry-After. In caso di 503 temporaneo o di interruzione della rete, riprova con un backoff limitato utilizzando la STESSA chiave e gli stessi dati di input. Un payload diverso richiede una nuova chiave. In caso di 409, riconcilia l’operazione precedente invece di ripeterla alla cieca. Le importazioni restituiscono risultati per singolo elemento tramite /jobs/{id}.
Webhook firmati
Utilizza un ricevitore HTTPS pubblico sulla porta 443. Gli eventi includono content.review_required, content.published, content.withdrawn, content.publication_failed e import.completed. Verifica il corpo grezzo della richiesta prima di analizzarlo.
HMAC-SHA256(secret, timestamp + "." + event_id + "." + raw_body)
Leggi X-Formtruth-Timestamp, X-Formtruth-Event-Id e X-Formtruth-Signature. Rifiuta i timestamp al di fuori di cinque minuti. L’header della firma può contenere più valori v1= separati da virgole durante la finestra di rotazione della chiave di 24 ore; accetta qualsiasi firma valida per una chiave che conservi.
Salva atomicamente l’ID dell’evento per la deduplicazione, accoda il lavoro e restituisci rapidamente 2xx. La consegna può essere ritentata, quindi un evento duplicato non deve pubblicare né addebitare due volte. Conserva il segreto di firma separato dalla credenziale API.
Esempi di codice
Prepara document.json utilizzando lo schema KnowledgeDocument e gli ID reali delle tassonomie autorizzate. Imposta FORMTRUTH_API_TOKEN in modo sicuro sul tuo server. Questi esempi creano una bozza; la pubblicazione richiede comunque il flusso di revisione.
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.