Skip to main content
The Clair data API is one read endpoint over one corpus, plus coverage discovery:

Base URL & authentication

Clair is served through RapidAPI. Send your key on every request as X-RapidAPI-Key, along with the host header shown on the Clair API’s RapidAPI page:
RapidAPI owns key issuance, plan quotas, rate-limiting, and billing. There is no separate Clair account or key to manage.

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. Set include_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 with sources (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 above min_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=relevance returns the top limit articles by score. It’s a snapshot by definition, and it has no cursor. Time filters apply before ranking, so a narrow since returns 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 with model:
  • model: "bge-m3" (the one accepted value at launch).
  • vector: exactly 1024 floats, L2-normalized. Any other length is rejected with 422.
Prefer not to manage any of that? Send 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 carries title, 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 on error, which is a stable code. message is for humans and may change.