---
title: "Search open roles across companies"
method: GET
path: "/v1/jobs/search"
---

# Search open roles across companies

`GET /v1/jobs/search`

## Request

- Base URL: `https://api.reqbeat.com`
- URL: `https://api.reqbeat.com/v1/jobs/search`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

Flat, role-granular job search: the individual open roles across
companies matching `function` / `geo` (country) / `since`
(first-seen lower bound), one row per logical req. LinkedIn-excluded:
ATS tenants plus public job boards and aggregators; each row's
`board` names its source. `since` and `first_seen` are our first
observation of the req, not the employer's posting date.
Freshness-floored, PK-ordered and keyset-paginated via the opaque
`next_cursor` (the page's last `company_id:req_key`). The
company-granular reverse view is `/v1/reqs/search`; full multi-board
dedup for one company is `/v1/companies/{company_id}/open-reqs`.

`industry`, `industry_terms` and `headcount_band` cut the corpus down to
your ICP's companies. Repeat a parameter to OR its values; different
parameters AND. They are filters only -- no company facet is added to a
served row by passing one -- and a company whose fact we do not hold is
never matched, so a cut is honestly narrow rather than quietly padded.

`exclude_employer_types` drops roles whose posting is classified as a given
employer type (e.g. `staffing`), keeping unclassified ones unless
'unknown' is listed. Opt-in: omit it and the page is unchanged.

`desc_q` keeps only roles whose description contains the keyword or
phrase (words match adjacent, in order). It filters; it never ranks, and it
combines with `q`, `function` and the other filters by AND.

`q` is free-text title search, ranked: it expands semantically to nearby
job titles and matches them against the posting's own title, and each row
carries the `relevance` (0-1) it scored. `sort` picks the ordering --
'relevance' (the default whenever `q` is given) or 'recency'. Omit both and
the page keeps its shipped stable order exactly. `q` is INDEPENDENT of
`function`: pass both to search titles within one function.

An unresolvable `function` -- anything that is neither a function name
nor a function taxonomy id -- is rejected with 422 instead of returning a
silently-empty result.
`limit` is bounded: an oversized page is rejected with 422 rather than
truncated -- page through the full set with `next_cursor` instead of
raising `limit`. The shared public demo key gets a tighter page instead --
it bills nothing, so no ledger-derived gate can bound it, and a full-size
page would make a world-readable credential a bulk-export window onto the
fresh index. `sort='relevance'` without a `q`, and a `next_cursor`
replayed under a different `sort`, are 422s for the same reason: an order
other than the one asked for reads as a measurement.

Requires the `read` scope.

## Query parameters

- `function` string, nullable: Function to search for: its name as served in `function_name`, e.g. 'Software & IT', or its taxonomy id as served in `function`, e.g. '54f81306-3000-5c4a-a088-dca4350b19ff'. A name covers every function id that serves it; an id matches that one id. Anything else is rejected with 422, which lists the names, rather than returning a silently-empty result.
- `geo` string, nullable: Country name to restrict the search to, e.g. 'United States'. Matched against the geocoder's pinned country list.
- `since` string, date-time, nullable: ISO-8601 timestamp lower bound on `first_seen` -- our first observation of the job, not the employer's posting date -- e.g. '2026-01-01T00:00:00Z'. Omit for no lower bound.
- `q` string, nullable: Free-text title search, e.g. 'senior backend engineer'. Expanded semantically to nearby job titles and matched against the posting's own title, with each row's `relevance` (0-1) reporting how well it matched. Independent of `function`: pass both to search titles WITHIN a function.
- `desc_q` string, nullable: Keyword or phrase the job DESCRIPTION must contain, e.g. 'clay tables'. Words match as one adjacent phrase. A filter, not a ranking: it does not change `relevance` or the default order. A req whose description we have not resolved never matches, and a value with no searchable word is rejected with 422.
- `sort` string, nullable: 'relevance' (the default when `q` is given) or 'recency' (newest `first_seen` first). Omit without a `q` for the default stable order. 'relevance' without a `q` is rejected with 422 — there is no query to be relevant to.
- `source_type` 'ats_tenant' | 'board', nullable: Keep only reqs read from one kind of source: 'ats_tenant' (the employer's own applicant tracking system) or 'board' (a job board or aggregator). Matches each req's `source_type`. Omit to search both, which is the default; any other value is rejected with 422.
- `exclude_agencies` boolean: Drop reqs whose company we have positively classified as a middleman — a staffing agency, job aggregator, subcontractor network or job board. A company whose kind we have not determined is KEPT, not dropped: this is a veto on proven intermediation, never a filter down to proven direct employers, so an unclassified company can never disappear from your results for want of a classification.
- `exclude_employer_types` string[], nullable: Drop reqs whose own posting is classified as one of these employer types: 'direct', 'staffing', 'consultancy' or 'unknown'. Repeat to drop several, e.g. `exclude_employer_types=staffing`. Reads the posting, not the company (see `exclude_agencies`). A req we have not classified is KEPT unless 'unknown' is listed. Any other value is rejected with 422.
- `industry` string[], nullable: Company industry, matched exactly (case-insensitively), e.g. 'software development'. Repeat for an OR-set. The stored vocabulary is granular -- 'software development', 'computer software' and 'it services and it consulting' are three separate labels -- so prefer `industry_terms` unless you know the exact label you want. A company whose industry we do not hold is never matched.
- `headcount_band` string[], nullable: Company size band: one of '1-10', '11-50', '51-200', '201-500', '501-1000', '1001-5000', '5001+'. Repeat for an OR-set. A band outside that ladder is rejected with 422 rather than answered emptily; a company whose headcount we do not hold is never matched.
- `industry_terms` string[], nullable: Substring of the company industry, matched case-insensitively, e.g. 'health' to reach both 'hospitals and health care' and 'health, wellness and fitness'. Repeat for an OR-set. The forgiving counterpart to `industry`, and usually the one to reach for first.
- `cursor` string, nullable: Keyset pagination cursor: pass the previous page's `next_cursor` to get the next page. Omit for the first page.
- `limit` integer: Maximum jobs per page. An out-of-range value is rejected with 422 — page through the full set with `next_cursor` instead.

## Headers

- `Idempotency-Key` string, nullable

## Response `200`

Successful Response

- JobsSearchResponse
  - `jobs` JobOut[], required
    - `req_key` string, required
    - `company_id` integer, required
    - `company_name` string, nullable
    - `company_domain` string, nullable
    - `title` string, nullable, required
    - `function` string, nullable, required
    - `country` string, nullable, required
    - `first_seen` string, date-time, required: Our first observation of this req (not the employer's posting date). The `since` parameter filters on this value.
    - `source_posted_at` string, date-time, nullable: The posting date the source itself published for this req. Null when the source gave no date, or none has been recorded yet -- `first_seen` is then the only date we hold, and it is our observation time.
    - `board` string, required
    - `source_type` 'ats_tenant' | 'board', nullable: What kind of source this req was most recently read from: 'ats_tenant' (the employer's own applicant tracking system) or 'board' (a job board or aggregator). Most served reqs are 'board'; pass `source_type=ats_tenant` to keep only the ones read from the employer's own system.
    - `relevance` number, nullable
    - `country_code` string, nullable, required: The posting country's ISO-3166-1 alpha-2 code, uppercase (`DE`, `US`), derived from `country` and never stored. `null` when `country` is absent or is not a country this corpus names — never a guess. Note this is an output only: the `geo`/`country` filter still takes the name, because a bare two-letter code is ambiguous on input (`CA` is Canada to ISO and California to this column).
    - `function_name` string, nullable, required: The job function's human-readable name (`Software & IT`, `Healthcare`), derived from `function` and never stored. The `?function=` filter accepts this name as well as the opaque taxonomy id in `function`. `null` when the req carries no function attribution (`unspecified`, 10.2% of served reqs), or when it was classified from its skills alone, so its id is outside the named title-family vocabulary (16.7%) — never a guess and never the raw id. Named on 73.1% of served reqs (309,251 of 422,826, measured 2026-10-07).
  - `next_cursor` string, nullable, required
  - `as_of` string, date-time, required

## Other responses

- `401`: No API key was sent.
- `402`: The key is over its plan's rolling call quota, or the account is past its spend cap. `upgrade_url` and `purchase_url` are the ways out.
- `403`: The presented key is invalid or revoked, or (see `x-required-scope` on this operation) it does not carry the scope this operation requires.
- `422`: Validation Error
- `429`: Too many calls in the current 60-second window. The limit is per API key, set by the key's plan, and shared by every metered REST route and MCP tool; the shared demo key is limited per IP address instead. Wait `Retry-After` seconds and retry. The `RateLimit-*` headers are absent when `reason` is `key_throttled`.

---

[API](https://skmtc.dev/reqbeat/apis/reqbeat-api.md) · [All operations](https://skmtc.dev/reqbeat/apis/reqbeat-api/llms.txt) · [OpenAPI document](https://skmtc.dev/reqbeat/apis/reqbeat-api/revisions/4758e5957e8c?raw)
