> ## Documentation Index
> Fetch the complete documentation index at: https://clair.im/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Introduction

> Base URL, authentication, the embedding-space contract, completeness guarantees, and error codes for the v1 data API.

The Clair data API is one read endpoint over one corpus, plus coverage discovery:

| Method | Path          | Purpose                                                                                                         |
| ------ | ------------- | --------------------------------------------------------------------------------------------------------------- |
| `POST` | `/v1/match`   | Describe what you care about and get the news that means it. The query is optional; omit it to page the corpus. |
| `GET`  | `/v1/sources` | The outlets we actively serve, by name, the picker for the source filters.                                      |

## Base URL & authentication

Clair is served through [RapidAPI](https://rapidapi.com/clairapis-clairapis-default/api/clair-news-api). Send your key on every
request as `X-RapidAPI-Key`, along with the host header shown on the Clair API's
RapidAPI page:

```sh theme={null}
X-RapidAPI-Key: YOUR_RAPIDAPI_KEY
X-RapidAPI-Host: clair-news-api.p.rapidapi.com
```

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:

| You send | You get                                                           |
| -------- | ----------------------------------------------------------------- |
| `text`   | We embed it with bge-m3 server-side. Plain curl is enough.        |
| `vector` | Your own 1024-dim bge-m3 vector, matched as-is. Nothing to trust. |
| neither  | No query: the corpus itself, newest-first, keyset-paginated.      |

`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"`:

| `min_score`         | articles returned | what you get                                          |
| ------------------- | ----------------- | ----------------------------------------------------- |
| `0.6`               | 11                | near-duplicates of the query                          |
| **`0.5`** (default) | **330**           | on-topic coverage                                     |
| `0.4`               | 3,419             | loosely related; the filter is fading                 |
| `0.3`               | 13,000+           | \~60% of everything; the filter has stopped filtering |

**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`](/docs/api-reference/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](/docs/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.

| Status | `error`                      | Meaning                                                                                                                                                  |
| ------ | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`  | `unauthorized`               | Missing or invalid API key.                                                                                                                              |
| `422`  | `invalid_request`            | A field failed validation, e.g. `vector` is not 1024 floats, `limit` exceeds 100, or a name in `sources`/`exclude_sources` isn't a known source.         |
| `422`  | `too_many_matches`           | `order=time` matched more articles than one pass can collect. Raise `min_score`, or narrow `since`/`until`. `matched_at_least` says how many we'd found. |
| `422`  | `order_requires_query`       | `order=relevance` with nothing to rank.                                                                                                                  |
| `422`  | `cursor_requires_time_order` | `cursor` sent with `order=relevance`, which has no pages.                                                                                                |
| `400`  | `bad_cursor`                 | Cursor is malformed or not one of ours.                                                                                                                  |
| `429`  | -                            | Plan rate limit exceeded (enforced by RapidAPI).                                                                                                         |
| `503`  | `text_mode_unavailable`      | Text mode is off or unreachable. Send a `vector` instead.                                                                                                |
