Skip to content

API documentation

Everything the app does, over REST. Quote a check, run it, poll for results as they land, and investigate single claims with deep research.

Every endpoint, schema, and enum is in the 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. We cache the extraction on the returned documentId, so a later run charges exactly the quoted amount.

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 repeated request 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 appear in the report as they land. Claims show up first, with charStart/charEnd offsets into your text for highlighting. Votes fill in per model, and a consensus entry per tier marks a claim as done. Poll every 2-3 seconds until check.status is terminal, and you have a live UI.

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. The result is a proof-and-dissent argument map with live sources, plus per-model votes. It is flat priced. Pass free-form claim text, or a claimId from a report. Poll GET /research/{id} until the status is terminal.

curl -X POST .../v1/research \
  -H "Authorization: Bearer fe_live_..." -H "Content-Type: application/json" \
  -d '{ "claimId": "b1e4..." }'

Webhooks

If you set a callbackUrl, we POST a small signed event (check.completed or check.failed) when the check reaches a terminal state. Fetch the report for the full results. We retry with backoff for about 40 minutes. Respond with 2xx to acknowledge. Every retry of an event carries the same id (also sent as X-FE-Delivery), so de-duplicate on it. The URL must be a public https endpoint, and redirects are not followed.

X-FE-Signature: t=1719312000,v1=4f8e...c1
X-FE-Event: check.completed

{ "id": "evt_0a1b...", "type": "check.completed",
  "data": { "checkId": "8c1d...", "status": "completed", ... } }

To verify, compute the HMAC-SHA256 of `${t}.${rawBody}` with your whsec_... secret. It must equal one of the v1 values. We send several during a rotation, so 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 returns the remaining balance. GET /webhook-secret returns the signing secret (write scope). POST /webhook-secret/rotate issues a new one and keeps the old one valid for 24 hours. GET /me returns 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.