Iniciar uma integração
- Peça a um administrador para criar a sua aplicação em Administração → API e integrações. Cada aplicação tem um ambiente, âmbitos de conteúdo, canais, idiomas, autores, validade e quota de pedidos próprios.
- Guarde a credencial no seu servidor. A produção utiliza credenciais ft_live_; a área de testes utiliza ft_test_. Envie Authorization: Bearer <credential> com cada pedido.
- Leia /capabilities e /content/taxonomies para obter os IDs de autores e tópicos autorizados. Utilize o esquema no OpenAPI para preparar o seu documento.
- Crie um rascunho, submeta-o para revisão e aguarde os eventos de publicação. A sua automatização pode preparar conteúdos úteis, enquanto um editor continua responsável pela publicação.
URL base: https://www.formtruth.com/integrations/v1
Conteúdos e publicação
Cada rascunho externo requer a aprovação da equipa do conteúdo atual antes da publicação. Declarar um método de autoria humana ou uma definição de publicação automática não concede aprovação editorial.
Artigos, guias de compra e FAQs partilham um formato de documento e um histórico de versões. Utilize /content/posts, /content/guides ou /content/faqs com a permissão correspondente. As aplicações só podem aceder a conteúdos que lhes pertençam; ler um rascunho privado é diferente de ler o site publicado.
Os documentos contêm um idioma de origem, um autor real, tópicos, fontes e títulos, slugs, resumos e blocos de conteúdo estruturados localizados. Os artigos assistidos por IA ou automatizados devem identificar o objetivo do leitor e o contributo original. As edições de tradução não podem alterar factos sobre produtos, números, IDs de imagens, ligações ou dimensões estruturais.
Cada operação de escrita requer uma Idempotency-Key. As atualizações, a revisão, a publicação e a retirada também requerem If-Match com a ETag entre aspas. A edição de conteúdo revisto invalida a aprovação afetada. Alterar a origem torna as traduções afetadas obsoletas até serem alinhadas e revistas novamente.
As imagens carregadas permanecem não aprovadas até um editor verificar os direitos e a qualidade. As publicações são instantâneos imutáveis. A retirada remove a publicação dos conteúdos públicos, dos feeds e da descoberta através da pesquisa.
Referência da API
Os caminhos abaixo são relativos ao URL base. As mesmas rotas de artigos aplicam-se a guias e FAQs, alterando a família de conteúdos. Descarregue o OpenAPI para consultar os esquemas completos dos pedidos, os âmbitos obrigatórios e as respostas.
| Método | Caminho | Objetivo |
|---|
GET | /capabilities | As suas permissões e limites efetivos |
GET / POST | /content/posts | Listar os seus artigos / criar um rascunho |
GET / PATCH | /content/posts/{id} | Ler o rascunho e o ETag / guardar uma revisão |
PUT | /content/posts/by-external-id/{externalId} | Criar ou atualizar um rascunho através do seu ID externo estável |
POST | /content/posts/{id}/submit-review | Submeter para revisão editorial |
POST | /content/posts/{id}/publications | Publicar idiomas aprovados ou agendar uma publicação |
DELETE | /content/posts/{id}/publications/{releaseId} | Retirar uma publicação com uma justificação |
GET | /content/taxonomies | Tópicos ativos e autores autorizados |
POST | /content/imports | Colocar em fila até 50 rascunhos e consultar os resultados dos itens |
POST | /assets/uploads | Carregar uma imagem para revisão de direitos e qualidade |
POST | /assets/{id}/complete | Ler o estado persistido do carregamento |
GET | /jobs/{id} | Ler os resultados da sua importação |
GET / POST | /webhooks/subscriptions | Listar ou registar os seus recetores de eventos HTTPS |
POST | /webhooks/subscriptions/{id} | Atualizar, rodar a chave ou desativar a entrega |
O catálogo, as encomendas e o inventário estão reservados para módulos de API posteriores. Ainda não estão disponíveis através deste gateway v1.
Erros e novas tentativas
Envie JSON UTF-8 sem compressão. Os limites do corpo do pedido são 2 MiB para operações de escrita comuns, 8 MiB para importações e 18 MiB para carregamentos de imagens codificadas; o corpo tem de chegar no prazo de dez segundos. Um 413 significa que o corpo é demasiado grande, 415 significa um formato não suportado e 408 significa que o carregamento excedeu o tempo limite.
Os erros devolvem error.code, error.message, detalhes dos campos quando aplicável, retryable e request_id. Guarde X-Request-Id para assistência. Um 401 significa um problema com a credencial; 403 significa falta de permissão; 412 significa uma versão alterada; 422 significa conteúdo inválido ou não preparado; 428 significa uma pré-condição em falta.
Para 429, respeite Retry-After. Em caso de um 503 temporário ou de uma interrupção de rede, repita com um recuo limitado e a MESMA chave e entrada. Um payload diferente requer uma chave nova. Em 409, reconcilie a operação anterior em vez de a repetir sem verificação. As importações devolvem resultados por item através de /jobs/{id}.
Webhooks assinados
Utilize um recetor HTTPS público na porta 443. Os eventos incluem content.review_required, content.published, content.withdrawn, content.publication_failed e import.completed. Verifique o corpo bruto do pedido antes de o analisar.
HMAC-SHA256(secret, timestamp + "." + event_id + "." + raw_body)
Leia X-Formtruth-Timestamp, X-Formtruth-Event-Id e X-Formtruth-Signature. Rejeite carimbos temporais com mais de cinco minutos. O cabeçalho da assinatura pode conter vários valores v1= separados por vírgulas durante a janela de rotação da chave de 24 horas; aceite qualquer assinatura válida correspondente a uma chave que tenha guardado.
Guarde atomicamente o ID do evento para deduplicação, coloque o trabalho em fila e devolva 2xx rapidamente. A entrega pode ser repetida, por isso um evento duplicado não pode publicar nem cobrar duas vezes. Mantenha o segredo de assinatura separado da credencial da API.
Exemplos de código
Prepare document.json utilizando o esquema KnowledgeDocument e IDs reais de taxonomias autorizadas. Defina FORMTRUTH_API_TOKEN de forma segura no seu servidor. Estes exemplos criam um rascunho; a publicação continua a exigir o fluxo de revisão.
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.