Base URL & authentication
Clair is served through RapidAPI. Send your key on every request asX-RapidAPI-Key, along with the host header shown on the Clair API’s
RapidAPI page:
Three ways to ask
POST /v1/match takes at most one query, and what you send decides how it reads:
order defaults to time in all three cases: this is a news API, and the
latest matching news is almost always the question. Ask for order=relevance
explicitly when you want the best matches regardless of when they ran.
The third row is the bulk read. There’s no separate firehose endpoint; a bulk
pull is just a match with nothing to match against, so it pages the same way and
costs the same per article.
Choosing min_score
With order=time, min_score is the most important parameter you send: it
doesn’t just filter a ranked list, it defines the result set. It’s cosine
similarity between your query and an article’s best-matching passage.
The scale is steep. Measured on the live corpus (~20k articles) for the query
"Foundation models, GPUs, AI labs and chips":
A 0.1 change is roughly a 10× change in result count. Start at the default,
then move in steps of 0.05. If you get
too_many_matches, raising min_score
is the fastest fix; if you get too little, 0.45 is usually enough. Below ~0.35
you’re asking for the whole corpus, and you should drop the query and page
chronologically instead; that’s free and exact.
Absolute scores aren’t comparable across different queries, only within one.
Edits and deduplication
Publishers routinely edit an article after publishing (a rewritten headline, a corrected figure, an added paragraph) under the same URL. Byte-level and title-similarity dedup both miss this, so a naive corpus ends up with the same story two to twenty times. Clair keeps every revision but, by default, returns one row per article: the first-published version, stamped with its original publication time. So a result set has no repeats, and the timestamp you get is when the story actually broke, which is the one an event study needs. Setinclude_edits: true to get the full revision history instead: every
version as its own row. Use it when the evolution of coverage is the thing you’re
studying; leave it off for clean, one-row-per-story results.
This is deduplication by article identity (same URL), not by meaning. Two
different outlets covering one event are different articles and both are
returned; independent corroboration is signal, not noise, and we don’t collapse
it.
Filtering by source
Scope a request to specific outlets withsources (keep only these) or
exclude_sources (drop these); the two are mutually exclusive. Both take
the exact name strings that GET /v1/sources returns,
so that endpoint is the picker: read it, then pass the names back. An unknown or
misspelled name is rejected with invalid_request, never silently treated as an
empty result.
This is source scoping, and it composes with meaning scoping: “articles that
mean X, but only from Reuters and Bloomberg” is a single request, which a
keyword API can’t express. Use it to watch a fixed set of wires, or to keep an
event’s corroboration count from being inflated by aggregators.
One honest caveat, only for sources (include) with order=time: a very
narrow source set over a broad query makes matches sparse in relevance order,
and the search can reach its internal walk limit before filling a page. Include a
handful of sources rather than one, or tighten the query, and it’s a non-issue.
Exclude filters and relevance order are never affected.
What “complete” means
This is the part worth reading before you build against it.order=time(the default) returns everything at or abovemin_score, newest-first, and paginates to the end. Every other news API sorts by date over a keyword filter; this sorts by date over a meaning filter.order=relevancereturns the toplimitarticles by score. It’s a snapshot by definition, and it has no cursor. Time filters apply before ranking, so a narrowsincereturns the best matches in that window, not an empty page.
order=time is complete to within ~99% recall (measured 99.6–100% against
an exhaustive scan of the corpus). Finding every match without reading every
article means an approximate nearest-neighbour search, and we would rather state
the number than claim an exactness we’d have to qualify later. If your query
matches more than we can collect in one pass, you get 422 too_many_matches
with how many we’d already found; never a quietly truncated page.
If you want an unbounded chronological walk, drop the query. That mode scores
nothing, so it’s exact and has no limit at all.
The embedding-space contract
Every vector you send must live in the pinned space, and you declare it withmodel:
model:"bge-m3"(the one accepted value at launch).vector: exactly 1024 floats, L2-normalized. Any other length is rejected with422.
text instead and the server embeds it
with the pinned model, chunking, pooling, and input type. See
How it works for why scores are reproducible.
Response shape
Every item carriestitle, a short source snippet (body), source_name,
source_url, and event_time. When you sent a query, it also carries score
and the passage that produced it (best_chunk); with no query both are null.
Full article bodies are never returned; you get the headline, the matched
snippet, and the source link, like a search engine.
Errors
Branch onerror, which is a stable code. message is for humans and may change.