API Documentation
Everything the app does, over REST: quote a check, run it, stream results by polling, and drill into single claims with deep research.
Every endpoint, schema, and enum: interactive API reference →
Authentication
Base URL https://fact.engineering/api/v1. Create a token under API keys and send it as a Bearer header. read tokens can call GET endpoints; write tokens can do everything.
curl https://fact.engineering/api/v1/me \
-H "Authorization: Bearer fe_live_..."The check loop
1. Estimate (optional). Get the exact claims and credit price before committing. The extraction is cached on the returned documentId, so the quote is exactly what a subsequent run charges.
curl -X POST .../v1/checks/estimate \
-H "Authorization: Bearer fe_live_..." -H "Content-Type: application/json" \
-d '{ "text": "The Eiffel Tower is 210 metres tall.", "tiers": ["t1","t2","t3"] }'
// → { "documentId": "3b9f...", "claimCount": 1, "claims": [...],
// "credits": { "total": 146, ... }, "balance": 3302, "sufficient": true }2. Submit. Send the documentId (or inline text to skip the estimate). Tiers: t1 spell & grammar, t2 model fact-check, t3 live web research. An Idempotency-Key makes retries safe — a repeat returns the original check.
curl -X POST .../v1/checks \
-H "Authorization: Bearer fe_live_..." -H "Content-Type: application/json" \
-H "Idempotency-Key: 9f1c2b3a" \
-d '{
"documentId": "3b9f...",
"tiers": ["t1","t2","t3"],
"name": "Eiffel Tower fact sheet",
"author": "Jane Doe",
"sourceUrl": "https://example.com/eiffel-tower",
"callbackUrl": "https://example.com/webhooks/fact-engineering"
}'3. Poll the report. Results stream into the report as they land — claims appear immediately with their charStart/charEnd offsets into your text (for highlighting), votes fill in per model, and a consensus entry per tier marks a claim done. Poll every 2–3 s until check.status is terminal; you get a live UI for free.
curl .../v1/checks/CHECK_ID/report -H "Authorization: Bearer fe_live_..."
// claim lifecycle: no evaluations → panel voting
// evaluations, no consensus → judge synthesizing
// consensus present → verdict finalPOST /checks/{id}/cancel stops a run and refunds it. PATCH /checks/{id} edits name/author/sourceUrl later.
Deep research
Investigate one claim in depth: a proof/dissent argument map with live sources plus per-model votes. Flat priced. Pass free-form claim text, or a claimId from a report. Poll GET /research/{id} until terminal.
curl -X POST .../v1/research \
-H "Authorization: Bearer fe_live_..." -H "Content-Type: application/json" \
-d '{ "claimId": "b1e4..." }'Webhooks
With a callbackUrl, terminal states POST a thin signed event (check.completed / check.failed) — fetch the report for full results. Retries with backoff for ~40 minutes; respond 2xx to acknowledge.
X-FE-Signature: t=1719312000,v1=4f8e...c1
X-FE-Event: check.completed
{ "id": "evt_0a1b...", "type": "check.completed",
"data": { "checkId": "8c1d...", "status": "completed", ... } }Verify: HMAC-SHA256 of `${t}.${rawBody}` with your whsec_… secret must equal a v1 value (several are sent during a rotation — accept any match). Reject stale timestamps.
const [t, ...sigs] = header.split(",").map((p) => p.split("=")[1]);
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) reject();
const expected = crypto.createHmac("sha256", SECRET)
.update(`${t}.${rawBody}`).digest("hex");
if (!sigs.some((s) => timingSafeEqual(s, expected))) reject();Credits & settings
GET /credits — remaining balance. GET /webhook-secret — the signing secret (write scope); POST /webhook-secret/rotate issues a new one with a 24 h grace window for the old. GET /me — the organization the token acts as.
Errors
{ "error": { "code": "insufficient_credits", "message": "..." } }400 bad_request · 401 unauthorized · 402 insufficient_credits · 403 forbidden · 404 not_found · 422 unprocessable · 500 internal_error