Skip to content

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 final

POST /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