---
title: "Find live reqs matching an ICP"
method: POST
path: "/v1/reqs/qualified"
---

# Find live reqs matching an ICP

`POST /v1/reqs/qualified`

## Request

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

The live reqs matching a full ICP definition -- "senior Go roles at
51-200-person fintechs in the EU, agencies excluded" -- as a page you pull,
rather than a webhook you register and wait on.

`criteria` is the same closed vocabulary a watch takes
(`POST /v1/watches`), evaluated here in SQL instead of on the dispatcher's
tick: three namespaced groups (`company`, `req`, `event`), AND across
groups and fields, OR within a field. A field outside the vocabulary is a
422, and so is one this projection cannot answer -- `event.function` (a
Python-derived taxonomy bucket, not a column; use `req.function`) and
`event.event_type` (a ledger transition, where a row here is a live
posting; use `/v1/events`). Never a silently-ignored filter: a dropped
criterion answers a wider question than the caller asked.

A NULL fact never matches (fail-closed), so a criterion on a sparse facet
is honestly narrow rather than wrong. The one exception is
`req.q_excludes`, which vetoes and therefore does NOT fire on an absent
title -- an unknown fact cannot prove an exclusion either.

POST rather than GET because criteria is a nested object; a URL-encoded
AND-of-ORs would be an invented serialization, which is the open grammar
the closed vocabulary exists to refuse.

Rows carry the normalized req layer no other route serves -- skills,
seniority, job family, declared **and** inferred compensation (with
`salary_estimate_samples`, so an estimate can be judged rather than
trusted), the eligibility block, apply actionability, lifecycle and dedup
-- plus `req_key`, the ledger's own identity for the req, so a page joins
to `/v1/events`.

`salary_min_usd` and `salary_max_usd` are the pay band: either alone is
half-open, both together bracket one comparable yearly-USD figure per row.
That figure is the employer's declared salary when there is one (the
midpoint of a declared range) and our inferred median otherwise, and the
inferred one qualifies only above a sample floor -- so a band is never
satisfied by a thinly-evidenced guess, and never half by the employer's
number and half by ours. A req with no salary at all matches no band
(fail-closed), and an inverted band is a 422 rather than a confidently
empty page.

`exclude_agencies` (default true) removes staffing agencies, aggregators
and job boards, so a row names the employer. `live_only` (default true)
keeps expired and superseded postings out. Keyset-paginated: pass the
previous page's `next_cursor`, which is monotone, so it doubles as
"everything new since my last call". `limit` is bounded -- this endpoint
meters the billed `source_row` atom off its own serialized rows, so an
out-of-range page is a 422 rather than a silent truncation.

`include_pulse` (default false) adds `companies`: the hiring pulse of every
company on the page, keyed by `company_id` and shaped like the pulse
`/v1/reqs/search` embeds, so a page of reqs needs no follow-up call per
company. It is not billed separately; the page still bills per row.

Requires the `read` scope.

## Headers

- `Idempotency-Key` string, nullable

## Request body

- QualifiedReqsRequest
  - `criteria` QualifiedCriteria, required: The watch criteria vocabulary, minus the fields a live-req page cannot answer. At least one field must be constrained.
    - `event` QualifiedCriteriaEvent: The `event` criteria group as `POST /v1/reqs/qualified` answers it. Not accepted here: event.function is a Python-derived taxonomy bucket, not a stored column on this projection; filter on req.function instead; event.event_type describes a ledger transition, and a qualified req is a live posting rather than a transition; use GET /v1/events for event types.
      - `company_id` integer[]
      - `board` string[]
      - `source_type` string[]
      - `ats_vendor` string[]
      - `country` string[]
    - `req` CriteriaReq: The `req` criteria group: AND across fields, OR within a field.
      - `seniority_level` string[]
      - `function` string[]
      - `remote_type` string[]
      - `skills` string[]
      - `q` string[]
      - `location` string[]
      - `q_excludes` string[]
    - `company` CriteriaCompany: The `company` criteria group: AND across fields, OR within a field.
      - `industry` string[]
      - `company_kind` string[]
      - `country` string[]
      - `headcount_band` string[]
      - `industry_terms` string[]
  - `exclude_agencies` boolean
  - `live_only` boolean
  - `salary_min_usd` number, nullable
  - `salary_max_usd` number, nullable
  - `since` string, date-time, nullable
  - `cursor` integer, nullable
  - `limit` integer
  - `include_pulse` boolean: Also return `companies`: the hiring pulse of each company on the page, once per company.

## Response `200`

Successful Response

- QualifiedReqsResponse
  - `reqs` QualifiedReqOut[], required
    - `req_key` string, required
    - `url` string, required
    - `posted_at` string, date-time, nullable, required
    - `fetched_at` string, date-time, nullable, required
    - `board` string, nullable, required
    - `source_type` string, nullable, required
    - `ats_vendor` string, nullable, required
    - `external_reference_id` string, nullable, required
    - `title` string, nullable, required
    - `standard_title` string, nullable, required
    - `seniority_level` string, nullable, required
    - `job_family` string, nullable, required
    - `skills` union[], nullable, required
      - union
        - object
        - string
    - `country` string, nullable, required
    - `location` string, nullable, required
    - `remote_type` string, nullable, required
    - `remote_type_confidence` number, nullable, required
    - `employment_type` string, nullable, required
    - `salary_min` integer, nullable, required
    - `salary_max` integer, nullable, required
    - `salary_currency` string, nullable, required
    - `salary_period` string, nullable, required
    - `yearly_salary_usd` number, nullable, required
    - `salary_estimate_min` integer, nullable, required
    - `salary_estimate_max` integer, nullable, required
    - `salary_estimate_median` integer, nullable, required
    - `salary_estimate_samples` integer, nullable, required
    - `eligibility_allowed_regions` string[], nullable, required
    - `eligibility_excluded_regions` string[], nullable, required
    - `eligibility_allowed_countries` string[], nullable, required
    - `eligibility_excluded_countries` string[], nullable, required
    - `eligibility_remote_global` boolean, nullable, required
    - `eligibility_work_auth_required` boolean, nullable, required
    - `apply_url` string, nullable, required
    - `apply_type` string, nullable, required
    - `apply_precision` string, nullable, required
    - `leaves_site` boolean, nullable, required
    - `lifecycle_status` string, nullable, required
    - `simhash` integer, nullable, required
    - `company_id` integer, required
    - `company_name` string, nullable, required
    - `company_domain` string, nullable, required
    - `company_country` string, nullable, required
    - `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).
    - `posted_at_precision` 'exact' | 'date' | 'unknown', required: How well the source knew `posted_at`: `exact` (a moment, carrying an intra-day component), `date` (a calendar day, at midnight UTC), or `unknown` (the source published no date and `posted_at` is null). Derived from `posted_at`, never stored. Note: the ingest still substitutes its own clock when a source publishes no date, so `unknown` is not yet reachable and those rows read `exact`.
  - `next_cursor` integer, nullable, required
  - `as_of` string, date-time, required
  - `companies` object, nullable: Only with `include_pulse`: each company on the page, keyed by `company_id`, with the same hiring pulse `/v1/reqs/search` embeds.

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