Iniciar uma integração
- Peça a um administrador para criar sua aplicação em Admin → API e integrações. Cada aplicação tem um ambiente, escopos de conteúdo, canais, idiomas, autores, validade e cota de requisições próprios.
- Armazene a credencial no seu servidor. Em produção, use credenciais ft_live_; no ambiente de testes, use ft_test_. Envie Authorization: Bearer <credential> em cada requisição.
- Leia /capabilities e /content/taxonomies para obter os IDs de autores e tópicos permitidos. Use o esquema no OpenAPI para preparar seu documento.
- Crie um rascunho, envie-o para revisão e acompanhe os eventos de publicação. Sua automação pode preparar conteúdo útil enquanto um editor continua responsável pela publicação.
URL base: https://www.formtruth.com/integrations/v1
Conteúdo e publicação
Todo rascunho externo requer aprovação da equipe de seu conteúdo atual antes da publicação. Um método de autoria humana declarado ou configuração de publicação automatizada não concede aprovação editorial.
Publicações, guias para compradores e perguntas frequentes compartilham um único formato de documento e histórico de versões. Use /content/posts, /content/guides ou /content/faqs com a permissão correspondente. As aplicações só podem acessar o conteúdo que possuem; 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. Artigos assistidos por IA ou automatizados devem identificar o objetivo do leitor e a contribuição original. Edições de tradução não podem alterar fatos sobre produtos, números, IDs de imagens, links ou dimensões estruturais.
Cada gravação precisa de uma Idempotency-Key. Atualizações, revisão, publicação e retirada também precisam de If-Match com o ETag entre aspas. Editar conteúdo revisado invalida a aprovação afetada. Alterar a fonte torna as traduções afetadas desatualizadas até serem alinhadas e revisadas novamente.
As imagens enviadas permanecem sem aprovação até que um editor verifique os direitos e a qualidade. As publicações lançadas são instantâneos imutáveis. A retirada remove a publicação do conteúdo público, dos feeds e da descoberta em buscas.
Referência da API
Os caminhos abaixo são relativos à URL base. As mesmas rotas de artigos se aplicam a guias e perguntas frequentes, alterando a família de conteúdo. Baixe o OpenAPI para consultar os esquemas completos de requisição, os escopos obrigatórios e as respostas.
| Método | Caminho | Finalidade |
|---|
GET | /capabilities | Suas permissões e limites efetivos |
GET / POST | /content/posts | Listar seus artigos / criar um rascunho |
GET / PATCH | /content/posts/{id} | Ler o rascunho e o ETag / salvar uma revisão |
PUT | /content/posts/by-external-id/{externalId} | Criar ou atualizar um rascunho pelo seu ID externo estável |
POST | /content/posts/{id}/submit-review | Enviar 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 um motivo |
GET | /content/taxonomies | Tópicos ativos e autores permitidos |
POST | /content/imports | Enfileirar até 50 rascunhos e consultar os resultados dos itens |
POST | /assets/uploads | Enviar uma imagem para análise de direitos e qualidade |
POST | /assets/{id}/complete | Ler o status persistido do envio |
GET | /jobs/{id} | Ler os resultados da sua importação |
GET / POST | /webhooks/subscriptions | Listar ou registrar seus receptores de eventos HTTPS |
POST | /webhooks/subscriptions/{id} | Atualizar, alternar a chave ou desativar a entrega |
Catálogo, pedidos e estoque estão reservados para módulos de API posteriores. Eles ainda não estão disponíveis por meio deste gateway v1.
Erros e novas tentativas
Envie JSON UTF-8 não compactado. Os limites do corpo da solicitação são 2 MiB para gravações comuns, 8 MiB para importações e 18 MiB para uploads de imagens codificadas; o corpo deve chegar em dez segundos. Um 413 significa que o corpo é muito grande, 415 significa um formato não suportado e 408 significa que o upload expirou.
Os erros retornam error.code, error.message, detalhes dos campos quando aplicável, retryable e request_id. Mantenha X-Request-Id para o suporte. Um 401 significa um problema de credencial; 403 significa permissão; 412 significa uma versão alterada; 422 significa conteúdo inválido ou não pronto; 428 significa uma pré-condição ausente.
Para 429, respeite Retry-After. Em caso de 503 temporário ou interrupção da rede, tente novamente com backoff limitado e a MESMA chave e entrada. Uma carga diferente precisa de uma chave nova. Em 409, concilie a operação anterior em vez de repeti-la às cegas. As importações retornam resultados por item por meio de /jobs/{id}.
Webhooks assinados
Use um receptor 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 da requisição antes de analisá-lo.
HMAC-SHA256(secret, timestamp + "." + event_id + "." + raw_body)
Leia X-Formtruth-Timestamp, X-Formtruth-Event-Id e X-Formtruth-Signature. Rejeite registros de data e hora fora de cinco minutos. O cabeçalho de assinatura pode conter vários valores v1= separados por vírgulas durante a janela de rotação de chave de 24 horas; aceite qualquer assinatura válida para sua chave mantida.
Salve atomicamente o ID do evento para deduplicação, enfileire o trabalho e retorne 2xx prontamente. A entrega pode ser repetida, portanto um evento duplicado não deve publicar nem cobrar duas vezes. Mantenha o segredo de assinatura separado da credencial da API.
Exemplos de código
Prepare document.json usando o esquema KnowledgeDocument e IDs reais de taxonomias permitidas. Defina FORMTRUTH_API_TOKEN com segurança no seu servidor. Estes exemplos criam um rascunho; a publicação ainda exige 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.