ClaimTrace API reference (v1)
Base URL: https://claimtrace.dev. Authentication: header X-API-Key: afk_…. The v1 transport contract is stable: no field is removed or changes meaning without a major version.
Endpoints
| Method | Path | Purpose |
|---|---|---|
POST | /v1/signup | Self-serve key (email → afk_… key sent by email, never in the response, 50 trial credits) |
POST | /v1/fetch | Blocking call — full FetchResultV1 JSON |
GET | /v1/fetch/stream | SSE stream — meta → entity… → done |
GET | /v1/requests/{request_id} | Re-read a result (24 h TTL, isolated per key) |
GET | /v1/usage | Your tier, remaining credits, requests today |
GET | /v1/stats | Public service counters of the day (no key required) |
GET | /v1/health | Service status + schema version |
Request — POST /v1/fetch
{
"thematique": "restaurants menus du jour",
"lieu": "Neuchâtel",
"prompt": "the actual daily menu, with prices if published",
"depth": "standard",
"max_entities": 8,
"day": "2026-08-08"
}
| Field | Type | Constraints |
|---|---|---|
thematique | string | required, 2–200 chars — what kind of entities |
prompt | string | required, 2–4000 chars — the fact you want established |
lieu | string | optional, ≤ 200 chars — geographic scope |
depth | string | fast · standard (default) · deep (Pack M+) |
max_entities | int | 1–30, default 8 |
day | string | optional, YYYY-MM-DD — for dated data (e.g. daily menus) |
deadline_s | number | optional, 1–900 — global wall-clock budget of the read: at the deadline, unfinished entities are returned with attributes.interrupt_reason = "deadline" (never "not published") and the response arrives no later than deadline_s + 5 s |
cache_mode | auto · bypass | bypass re-reads everything cold (reproducibility measurement). Honoured only for a *-scoped key; a free key is silently mapped to auto — read usage.credits_charged to know whether the run was cold |
Every accepted parameter
Generated from the v1 request model (FetchRequest) — any other field is silently ignored (send only the fields above).
<!-- gen:params (scripts/claimtrace_api_reference.py) -->
| Parameter | Type | Default | Bounds | Behaviour |
|---|---|---|---|---|
thematique | string | required | ≥ 2 chars, ≤ 200 chars | what kind of entities |
lieu | string | — | ≤ 200 chars | geographic scope |
prompt | string | required | ≥ 2 chars, ≤ 4000 chars | the fact you want established |
sources | list of web_search · places · website · social · files · images | — | — | restricts the source families the planner may use; default: the planner decides |
max_entities | int | 8 | ≥ 1, ≤ 30 | capped by the server limit; credits are reserved before the read |
depth | fast · standard · deep | standard | — | deep requires Pack M or L (otherwise 403) |
output_hint | object | — | — | {"entity_fields": [...]} adds up to 14 expected fields to the plan; never changes the response contract |
language | string | fr | ≤ 5 chars | language of the search and of day names |
country | string | — | ≥ 2 chars, ≤ 2 chars | ISO 3166-1 alpha-2 — search locale |
currency | string | — | ≥ 3 chars, ≤ 3 chars | accepted, not used in v1: a price carries the currency proven by its source (ClaimV1.currency) |
day | string | — | ≤ 10 chars | YYYY-MM-DD — for dated data (e.g. daily menus) |
radius_km | number | — | > 0, ≤ 25 | radius around lat/lng |
lat | number | — | ≥ -90, ≤ 90 | point of an area listing (with lng, radius_km) |
lng | number | — | ≥ -180, ≤ 180 | see lat |
background | bool | false | — | reserved — honoured only for an internal key, ignored otherwise |
known_contacts | list of object | — | — | reserved — honoured only for a trusted internal caller, ignored otherwise |
cache_mode | auto · bypass | auto | — | bypass is honoured only for an internal key; any other key is mapped to auto (read usage.credits_charged) |
deadline_s | number | — | > 0, ≤ 900 | global wall-clock budget; unfinished entities come back with attributes.interrupt_reason = "deadline" |
skip_image_curation | bool | false | — | set by the server — the value sent is ignored |
profile | menu_daily | — | — | menu_daily: daily-menu profile (no image curation) |
entity_name | string | — | ≤ 120 chars | named-entity lookup instead of an area listing |
<!-- /gen:params -->
Response — FetchResultV1
| Field | Type | Meaning |
|---|---|---|
schema_version | "1" | Contract version |
request_id | string | Re-read handle for /v1/requests/{id} (24 h) |
query | QueryEchoV1 | Echo: thematique, lieu, day, depth, vertical |
entities | EntityV1[] | The resolved entities |
verification | VerificationV1 | truth_rules_version, verified_live, checked_at, notes |
usage | UsageV1 | credits_charged, cache_hits, cost_class |
warnings | string[] | Degradations, stated rather than hidden — e.g. phase_failed:<phase>:<Error> when a step (discovery, planning) failed: an empty answer is never silent |
attribution | string[] | e.g. © OpenStreetMap contributors (ODbL) |
EntityV1
| Field | Type | Meaning | |
|---|---|---|---|
id | string | Stable entity id within the response | |
title | string | Entity name | |
summary | string \ | null | Short description when available |
location | GeoV1 \ | null | address, city, country, lat, lng |
contact | ContactV1 \ | null | phone, email, website |
attributes | object | Vertical-specific attributes | |
claims | ClaimV1[] | The verified facts — see below | |
images | ImageV1[] | url, role, caption | |
confidence | float \ | null | Entity-level confidence, 0–1 |
not_found | NotFoundV1 \ | null | Present when the requested data is not published |
ClaimV1 — the heart of the product
| Field | Type | Meaning | ||||
|---|---|---|---|---|---|---|
field | string | Which fact, e.g. menu_du_jour | ||||
value | any | The fact itself — null is allowed and assumed | ||||
currency | "CHF" \ | "EUR" \ | "USD" \ | "GBP" \ | null | ISO 4217 code of a monetary value, proven by the source verbatim or the field name — never guessed, never converted. null means the source does not prove it |
match_level | "exact" \ | "equivalent" \ | "comparable" \ | null | Product vertical only, on price claims: exact = the requested reference itself, equivalent = successor/equivalent from the same manufacturer, comparable = similar product from another range. Closed set | |
provenance.source_url | string \ | null | The page or document the fact was read from | |||
provenance.fetched_at | string \ | null | When it was read (ISO 8601 UTC) | |||
provenance.verbatim | string \ | null | Short proof excerpt, literally from the source | |||
provenance.page_kind | string \ | null | e.g. menu_du_jour, carte | |||
provenance.domain_owned_by_entity | bool \ | null | Whether the source belongs to the entity | |||
confidence | float \ | null | 0–1 | |||
verified_live | bool \ | null | Tri-state: live-confirmed / contradicted / not checked |
NotFoundV1 — absence is an answer
| Field | Type | Meaning | |
|---|---|---|---|
reason | enum, closed set | Exactly one of: not_published (the source was read, the requested data is not published there), wrong_page_kind (the source publishes another kind of content — e.g. a permanent menu card, not a daily menu), not_verifiable (the data could not be extracted with a verifiable verbatim proof — or the read did not happen: attributes.interrupt_reason is then deadline, cost_budget, llm_unavailable or error; social source behind a login wall; declared website that does not name the entity), source_unreachable (the source could not be read). This vocabulary is part of the contract: you can write an exhaustive switch on it. Any new value implies a new schema_version — never a silent addition | |
detail | string \ | null | Human-readable explanation |
source_checked | string \ | null | The source that was actually read |
What ClaimTrace reads — and does not
| Read | Status |
|---|---|
| HTML page of the entity's own site (including content shipped in an embedded JSON such as Inertia / Next, without running JS) | read |
| JavaScript-rendered page (empty shell without a browser) | rendered headless |
| Linked PDF, PDF inside an iframe, multi-page PDF (≤ 6 pages) | read; native text, otherwise vision page by page (scanned PDF, undecodable font) |
| Menu image published by the site (board, poster) | read with vision |
| Google Sites page | read as an HTML page |
| Facebook / Instagram | never downloaded (login wall): only dated public-index snippets count; otherwise not_verifiable |
| Google Business (profile, posts) | never a source — internal discovery only (Google Maps Platform terms) |
| Directories and aggregators (listas.ch, TripAdvisor, TheFork, ordering platforms…) | never a source, never an entity's "website" |
A fact served from a PDF is re-validated against the PDF's native text (verified_live); a scanned PDF or an image stays verified_live: null (honest tri-state).
Reproducibility — the replay harness
Same input → same output? Check it yourself with nothing but curl and jq:
CLAIMTRACE_API_KEY=afk_… ./claimtrace-replay.sh -n 3 --stream requests.jsonl
requests.jsonl: one POST /v1/fetch request per line ({"id":"…","body":{…}}). Outputs: summary.tsv (per run: duration, cold/cache, entities, claims, not_found by reason, interruptions, credits, facts hash, time to first entity in stream mode), variance.tsv (per request: k/N runs with identical facts, entity-set composition, latencies) and divergences.txt (every fact that differs, with the runs serving it). The facts hash ignores order, request_id and timestamps: two runs serving the same facts share the same hash.
What v1 never returns
Fields whose only source is Google Places — ratings, review counts, price levels, Google-hosted images — are excluded from the payload for legal reasons (Google Maps Platform terms of service prohibit exporting or re-sharing Places content). Claims come exclusively from sources owned by the entity, enforced by a domain-ownership whitelist.
Errors
| Code | Meaning |
|---|---|
401 | Missing or invalid API key |
402 | Credits exhausted — top up or wait for the monthly refill |
403 | Scope missing for this key, or depth: "deep" without Pack M or L — checked before anything runs, never charged |
404 | On /v1/requests/{id}: unknown or expired result for this key |
409 | On /v1/signup: an active key already exists for this email |
422 | Validation error — a field violates the constraints above |
429 | Rate limit — response carries Retry-After |
405 | Wrong HTTP method on a documented route (e.g. GET /v1/fetch) |
Refusals are never charged.