> ## 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

> Search indexed jobs and internships with normalized fields, cursor pagination, source provenance, and explicit freshness metadata.

The Clair Job Search API exposes one bounded, market-wide search over active job
postings:

| Method | Path       | Purpose                                                                          |
| ------ | ---------- | -------------------------------------------------------------------------------- |
| `GET`  | `/v1/jobs` | Search normalized jobs and internships already present in Clair's crawled index. |

Clair reads a validated registry of employer boards in the background. Public
requests query Clair's database only. They do not contact an employer site,
talk to an ATS, or fetch a caller-supplied URL.

That separation keeps request latency bounded by infrastructure Clair controls.
It also gives every result the same filters, pagination, freshness signals, and
error contract.

## Base URL and authentication

The Jobs API is distributed through
[RapidAPI](https://rapidapi.com/clairapis-clairapis-default/api/clair-job-search-api).
Send your key and the Jobs API host on every request:

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

RapidAPI owns key issuance, plan quotas, rate limiting, and billing. There is no
separate Clair account.

<Note>
  Use the exact host shown on the RapidAPI listing. The News API is a separate
  listing with a separate host, even when you use the same RapidAPI key.
</Note>

## What you can search

`q` is a case-insensitive substring filter over the posting title. It
determines which postings match; `sort` still controls their order. Use the
structured filters below for company, department, skills, compensation, and
other normalized fields.

Structured filters let you narrow the result set without parsing source text:

* Company, free-form location, and ISO 3166-1 alpha-2 country.
* Normalized `workplace_type`: `remote`, `hybrid`, or `onsite`.
* Derived `seniority` and career `track`.
* Department or team.
* Skills requiring either every requested value with `skills_all` or at least
  one with `skills_any`.
* Compensation range, currency, and interval.
* Publisher time with `posted_after` or Clair discovery time with
  `first_seen_after`.
* ATS provider and board identifier for advanced provenance filtering.

See [Search jobs and internships](/docs/jobs-api/search) for the complete parameter
reference.

## Jobs and internships share one resource

An internship is a posting, so it uses the same endpoint and schema as every
other role. Set:

```sh theme={null}
seniority=intern
```

The title classifier recognizes internships, co-ops, apprenticeships,
working-student roles, and placements. You can combine the filter with
`q`, `country`, `workplace_type`, skills, or discovery time:

```sh theme={null}
curl -G https://clair-job-search-api.p.rapidapi.com/v1/jobs \
  -H "X-RapidAPI-Key: YOUR_RAPIDAPI_KEY" \
  -H "X-RapidAPI-Host: clair-job-search-api.p.rapidapi.com" \
  --data-urlencode "seniority=intern" \
  --data-urlencode "workplace_type=remote" \
  --data-urlencode "skills_any=python,pytorch"
```

## The normalized job schema

Every result includes:

* A stable Clair `id`.
* `title`, `company`, `department`, and `team`.
* The primary `location`, all published `locations`, derived `country`, and
  normalized `workplace_type`.
* Source-provided `employment_type`.
* Validated HTTPS `posting_url` and `apply_url`; every returned posting has at
  least one, while either field can be null when the source exposes only the
  other.
* Publisher timestamps, Clair discovery and verification timestamps.
* Normalized compensation with its extraction source.
* Derived `seniority`, `track`, `title_family`, and canonical `skills`.
* `description_available`, plus an optional 2,000-character plain-text
  `description_excerpt`.
* Source provider, board, and external posting ID.

Nullable values are intentional. Clair returns `null` when a source did not
state a value or it could not be derived safely. It does not invent an
employment type, country, salary, publication date, or company name. In
particular, a source board identifier is never substituted for a missing
company.

An indexed record with neither a valid HTTPS posting link nor a valid HTTPS
application link is omitted from search until a later crawl repairs it. It is
not presented as a closed job merely because one source response was
incomplete.

`employment_type` reflects what the source adapter could normalize. Some
publishers expose free-form values, so do not treat it as a closed enum.

## Compensation semantics

Salary bounds use overlap matching:

* `salary_min=150000` keeps a posting whose stated maximum is at least 150,000.
* `salary_max=200000` keeps a posting whose stated minimum is at most 200,000.

`currency` is required whenever you send a salary bound. If you omit
`salary_interval`, bounds default to annual compensation. You can explicitly
filter `year`, `hour`, `month`, `week`, or `day`.

The nested `compensation.source` explains provenance:

| Value         | Meaning                                                                        |
| ------------- | ------------------------------------------------------------------------------ |
| `ats`         | The ATS published compensation as a structured field.                          |
| `description` | Clair extracted a stated figure from the employer-written posting description. |
| `null`        | The source of the available compensation could not be classified.              |

Clair does not convert between currencies or silently annualize hourly pay.

## Provenance and time

The timestamps answer different questions:

| Field               | Meaning                                                        |
| ------------------- | -------------------------------------------------------------- |
| `posted_at`         | When the publisher says the role was posted. May be null.      |
| `updated_at`        | When the publisher says it updated the role. May be null.      |
| `first_seen_at`     | When Clair first discovered the posting.                       |
| `last_verified_at`  | Most recent successful crawl that still contained the posting. |
| `meta.generated_at` | When Clair generated this API response.                        |

Use `posted_after` for publication-date research. Use `first_seen_after` for
alerts or incremental discovery, because it remains available when a publisher
omits `posted_at`.

Each job also carries:

```json theme={null}
{
  "source": {
    "provider": "greenhouse",
    "board": "examplelabs",
    "external_id": "4512338005"
  }
}
```

Pair this source identity with `posting_url`, `apply_url`, and
`last_verified_at` when auditing a result.

## Index freshness

Every successful response includes an `index` object inside `meta`:

```json theme={null}
{
  "status": "healthy",
  "as_of": "2026-07-28T12:00:00Z",
  "active_boards": 9000,
  "stale_boards": 0
}
```

`as_of` is the most recent successful board crawl recorded. It is not a claim
that every board was crawled at the same instant. `stale_boards` is the number
of enabled boards that have missed two of their own configured crawl intervals,
have never completed, or most recently failed.

| Status        | Meaning                                   |
| ------------- | ----------------------------------------- |
| `healthy`     | No enabled board is stale.                |
| `degraded`    | At most 5% of enabled boards are stale.   |
| `stale`       | More than 5% of enabled boards are stale. |
| `unavailable` | Clair could not establish freshness.      |

Use per-job `last_verified_at` when the exact freshness of one posting matters.

## Cursor pagination

Pages are ordered newest first with a stable ID tiebreaker. `sort=posted_at`
uses the publisher time and falls back to `first_seen_at` when the publisher
supplied no date. `sort=first_seen_at` is useful for ingestion and alerting.

If `meta.has_more` is true, pass `meta.next_cursor` unchanged:

```sh theme={null}
curl -G https://clair-job-search-api.p.rapidapi.com/v1/jobs \
  -H "X-RapidAPI-Key: YOUR_RAPIDAPI_KEY" \
  -H "X-RapidAPI-Host: clair-job-search-api.p.rapidapi.com" \
  --data-urlencode "country=DE" \
  --data-urlencode "cursor=CURSOR_FROM_PREVIOUS_RESPONSE"
```

A cursor is bound to the filters and sort that created it. Changing either
while reusing the cursor returns `bad_cursor`. Stop when `has_more` is false and
`next_cursor` is null.

Exact totals require an additional distinct-count query, so they are opt-in.
Set `include_total=true` to populate `meta.total`. Otherwise `total` is null and
`total_is_exact` is false.

## Deduplication

One requisition can appear on several career sites owned by the same ATS
tenant. Clair retains source rows for crawling, then applies provider-aware
identity to search results. Workday uses tenant plus external requisition ID.
Reviewed Recruitee aliases also require the same exact link, while a
SmartRecruiters copy must agree on board, exact link, title, company, and
location. Clair does not use fuzzy title similarity that could merge distinct
openings. Optional `meta.total` counts canonical postings rather than storage
rows.

## Description excerpts

Descriptions are excluded by default. Set `include_description=true` to return
`description_excerpt`, a plain-text preview capped at 2,000 characters.

The field is a preview, not a complete or archival copy. Follow `posting_url`
for the publisher's current description.

## Errors

Branch on the stable `error` code. `detail` is human-readable and may change.
Validation failures return an array of details; typed failures return a string.

| Status | `error`                  | Meaning                                                                      |
| ------ | ------------------------ | ---------------------------------------------------------------------------- |
| `400`  | `bad_request`            | One or more parameters failed validation.                                    |
| `400`  | `bad_cursor`             | The cursor is malformed or belongs to different filters or sort order.       |
| `400`  | `unknown_skill`          | A value in `skills_all` or `skills_any` is outside the supported vocabulary. |
| `401`  | `unauthorized`           | The request is missing valid RapidAPI authorization.                         |
| `429`  | Gateway-managed          | The RapidAPI plan rate limit was exceeded.                                   |
| `500`  | `internal`               | An unexpected internal error occurred.                                       |
| `503`  | `dependency_unavailable` | The API platform is not ready.                                               |
| `503`  | `product_unavailable`    | The Jobs product failed its readiness check.                                 |
| `503`  | `overloaded`             | This endpoint reached its per-machine concurrency limit.                     |
| `504`  | `timeout`                | Search exceeded its processing deadline.                                     |

Origin responses include `X-Request-ID`. Include it when reporting a problem.
Honor `Retry-After` on `503` responses and retry with backoff.
