Hiring Index API

Three read-only endpoints over an index of live job postings read straight from thirteen applicant tracking systems: search postings by a shared filter (title, keyword, location, salary, work arrangement, posting age), aggregate the same filter into salary percentiles, top employers and posting-age freshness, and fetch one posting by id. Metered by results on RapidAPI, with a first-party stdio MCP server and Python client alongside it.

Operations 3

POST /jobs/search Search job postings #
POST /jobs/insights Aggregates over a filtered slice #
GET /jobs/{id} One posting by id #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/hiring-index-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

hiring-index-openapi.yaml Raw ↑
# Спецификация Jobs API для листинга на RapidAPI.
#
# Переписана 10.09.2026 по живому API: каждый пример запроса ниже отправлен
# дословно через листинг с ключом подписчика и вернул 200; схемы и примеры
# ответов сняты с этих ответов (массивы урезаны, description укорочен, прочие
# значения не тронуты). Нормативный контракт — denis/jobs-backend/02-contract.md;
# где он расходится с живым API, здесь записано то, что API отвечает.
#
# Файл публикуется: никаких адресов origin, внутренних хостов и ключей.
openapi: 3.0.3

info:
  title: Hiring Index
  version: "1.0.0"
  description: |
    More than a million and a half live job postings, read straight from the
    applicant tracking systems companies actually hire on — Workday,
    SmartRecruiters, Greenhouse, Workable, Lever, Ashby, Recruitee, Teamtailor,
    Breezy, Personio, Hireology, PageUp and Zoho Recruit. Tens of thousands of company career boards behind one
    endpoint, and where a posting carries an apply link, it points at the
    employer's own ATS form, not at a re-listing.

    What this API does that a job board API does not.

    **Aggregates, not pagination.** `/jobs/insights` takes the same filter as a
    search and returns the whole picture of that slice — salary percentiles per
    currency and pay period, top employers, city, country, platform, seniority
    and employment-type splits, and posting age — computed server side. One call
    answers "what does this market look like", instead of thousands of calls and
    a spreadsheet on your side.

    **Dates you can audit.** Where the ATS publishes a date, `posted_at` is the
    employer's own publication date, and `posted_at_raw` keeps exactly what the
    vendor wrote. When a vendor gives only a relative phrase such as
    "Posted 30+ Days Ago", `posted_at` is left out rather than guessed.
    `first_seen_at`, on every row, is when this index first saw the posting.

    **Nothing is filled in for you.** A field the source did not state is absent
    from the object — never null, never an empty string, never a default.

servers:
  # The public RapidAPI listing host. NOT the origin: the origin is a private address
  # configured on the listing's target group and it must never appear in a published spec.
  # The paths below carry no /v1 prefix because the listing declares them that way; the
  # /v1 lives in the origin URL, where RapidAPI appends the path.
  - url: https://hiringindex.p.rapidapi.com
    description: RapidAPI listing (send X-RapidAPI-Key and X-RapidAPI-Host)

security:
  - rapidApiKey: []

tags:
  - name: search
    description: Paged job search over a filter
  - name: insights
    description: Aggregates over the same filter - the thing no one else in the category ships
  - name: job
    description: A single job posting by id

paths:
  # Пути ровно те, что заведены в листинге: search, insights, job. GET /healthz был
  # объявлен здесь как запасной вариант T-008 и в кабинет не заливался — на маркетплейсе
  # он отдаёт 404 "Endpoint does not exist" (замер джуна 12.09, T-068). Спека с эндпоинтом,
  # которого в листинге нет, — это пример, по которому клиент получает 404; убран 12.09.
  # Текст на /docs («There is no separate liveness path on the listing») этим остаётся правдой.
  /jobs/search:
    post:
      tags: [search]
      operationId: searchJobs
      summary: Search job postings
      description: |
        Returns a page of postings matching the filter. The request body is the
        filter object itself, at the top level — there is no wrapper. Send `{}` to
        match everything; a request with no body at all is rejected with `400`.

        All filters are optional; an absent key and an empty array mean the same
        thing — the filter is not applied. An unknown key is rejected with `422`
        and the list of valid keys, so a typo never returns a silently wrong slice.

        `job_titles` matches the posting title only. `keywords` matches the title
        or the description, and several keywords are matched as OR, not AND: a
        request for `["Kubernetes", "Terraform"]` returns postings mentioning
        either. A keyword found in more than about 130,000 postings returns
        `422 keyword_too_common` in milliseconds instead of a timeout; narrowing by
        city or country does not help, so use `job_titles` for the role or a rarer
        term.

        `page` and `limit` both run from 1 to 100, so one filter yields at most
        10,000 rows. When a filter matches more than that window, `total_pages`
        stops at 100 and `pages_truncated` says how many rows are reachable. One
        page past the end is an empty `200`, not an error.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SearchRequest' }
            examples:
              remoteDataEngineers:
                summary: Remote data engineering roles posted in the last week
                value:
                  job_titles: ["Data Engineer"]
                  remote_flag: ["true"]
                  days_ago: 7
                  limit: 20
              berlinYearlyPay:
                summary: Berlin roles whose advertised yearly pay reaches €60,000
                value:
                  cities: ["Berlin"]
                  salary: { min: 60000, period: ["year"], currency: ["EUR"], match: overlaps }
                  limit: 20
      responses:
        '200':
          description: Page of postings
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SearchResponse' }
              examples:
                berlinYearlyPay:
                  summary: Response to berlinYearlyPay, captured 2026-09-10 (two rows shown, description shortened)
                  value:
                    jobs:
                    - _id: YXNoYnl8YWxtZWRpYXwxOGM1YmNhOS01MmM5LTRkZWMtOGJmYi1jNmUyNzZkYWNlZGF8MA
                      source_platform: ashby
                      handle: almedia
                      source_job_id: 18c5bca9-52c9-4dec-8bfb-c6e276daceda
                      record_index: 0
                      title: Senior Growth Manager (Berlin)
                      company_name: Almedia
                      description: <p style="min-height:1.5em">This isn’t your regular job. Almedia is a place where those wh…
                      posted_at: '2026-09-09T14:43:52.483+00:00'
                      posted_at_raw: '2026-09-09T14:43:52.483+00:00'
                      posting_url: https://jobs.ashbyhq.com/almedia/18c5bca9-52c9-4dec-8bfb-c6e276daceda
                      apply_url: https://jobs.ashbyhq.com/almedia/18c5bca9-52c9-4dec-8bfb-c6e276daceda/application
                      location_raw: Berlin
                      city: Berlin
                      region: Berlin
                      country: Germany
                      country_code: DE
                      postal_code: '10117'
                      remote_flag: 'false'
                      employment_type: FullTime
                      department: Growth
                      team: Advertiser Growth
                      salary_text: €75K – €100K • Offers Equity • Offers Bonus
                      salary:
                        min: 75000
                        max: 100000
                        currency: EUR
                        period: 1 YEAR
                        source: vendor_field
                        confidence: stated
                      first_seen_at: '2026-09-10T02:50:23.085014+00:00'
                      fetched_at: '2026-09-10T02:50:23.085014+00:00'
                      content_hash: daca92c9fc8dc1ee05fc31644fb8e25c7b63503861d3a4475421e7113bb7048d
                    - _id: YXNoYnl8YWxtZWRpYXw5MGRiZjE3Yi1mNzA5LTRjZmMtOTc3NS04ZjY0MmQ5MzFkNTh8MA
                      source_platform: ashby
                      handle: almedia
                      source_job_id: 90dbf17b-f709-4cfc-9775-8f642d931d58
                      record_index: 0
                      title: Founding Engineer
                      company_name: Almedia
                      description: <p style="min-height:1.5em">This isn’t your regular job. Almedia is a place where those wh…
                      posted_at: '2026-09-09T13:25:49.087+00:00'
                      posted_at_raw: '2026-09-09T13:25:49.087+00:00'
                      posting_url: https://jobs.ashbyhq.com/almedia/90dbf17b-f709-4cfc-9775-8f642d931d58
                      apply_url: https://jobs.ashbyhq.com/almedia/90dbf17b-f709-4cfc-9775-8f642d931d58/application
                      location_raw: Berlin
                      city: Berlin
                      region: Berlin
                      country: Germany
                      country_code: DE
                      postal_code: '10117'
                      remote_flag: 'false'
                      employment_type: FullTime
                      department: Tech
                      team: Engineering
                      salary_text: €90K – €150K • Offers Equity
                      salary:
                        min: 90000
                        max: 150000
                        currency: EUR
                        period: 1 YEAR
                        source: vendor_field
                        confidence: stated
                      first_seen_at: '2026-09-03T23:12:31.801589+00:00'
                      fetched_at: '2026-09-10T02:50:23.085014+00:00'
                      content_hash: 960fae4b642c8094056a1835f710da4a3c9719889a3e4a603db520bfc1c3dc86
                    total_count: 207
                    company_count: 33
                    page: 1
                    limit: 20
                    total_pages: 11
                    meta:
                      request_id: be8374ea54b3756c61e55409f09e8075
                      took_ms: 62.35
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Busy' }

  /jobs/insights:
    post:
      tags: [insights]
      operationId: jobInsights
      summary: Aggregates over a filtered slice
      description: |
        Takes the same filter as `/jobs/search` — without `page` and `limit`,
        which it rejects — and returns aggregates over the whole matching set, not
        over one page.

        - `headline`: rows and distinct postings matched, named employers, how many
          rows carry a salary, a publication date and a description, and postings
          first seen in the last seven days.
        - `salary`: percentiles of the lower and the upper end of the advertised
          band, one entry per currency and pay period as the employer wrote them,
          so hourly, monthly and yearly pay are never averaged into one number. An
          entry needs at least 30 disclosed salaries: a narrow slice returns an
          empty array rather than a median of five rows.
        - `freshness` and `posted_at`: posting age in days, counted from the
          employer's publication date.
        - `top_companies` and the `*_split` lists: counts by employer, city,
          country code, ATS platform, board handle, seniority, employment type and
          remote flag. Split values are the vendor's own strings, so `Full-time`
          and `FullTime` arrive as separate rows.

        This is the call that replaces a data pipeline. Ask about Software
        Engineers in San Francisco and get the pay band, who is hiring and how old
        the listings are — in one round trip, billed as one call.

        **Grouped mode.** Add `group_by` together with a `cities` or
        `country_codes` list and the answer comes back as one block per city or
        country in `groups[]` — one call instead of one per location.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InsightsRequest' }
            examples:
              sfSoftwareEngineers:
                summary: What the Software Engineer market looks like in San Francisco
                value:
                  job_titles: ["Software Engineer"]
                  cities: ["San Francisco"]
              berlinDataEngineers:
                summary: Data Engineers in Berlin
                value:
                  job_titles: ["Data Engineer"]
                  cities: ["Berlin"]
              byCountry:
                summary: Data Engineers in Germany and the Netherlands, one block per country
                value:
                  job_titles: ["Data Engineer"]
                  country_codes: ["DE", "NL"]
                  group_by: country_code
                  percentiles: true
      responses:
        '202':
          description: |
            A filter that is not cached yet returns 202 with `Retry-After: 30`; call again
            after the delay to get the full aggregates (`meta.computed_at`). Each call,
            including the 202, counts as one request against your plan.

            This replaces the deadline `503` that `/jobs/insights` used to return: after
            T-072 that path no longer exists here. `503 Busy` still applies to both
            endpoints.
          headers:
            Retry-After:
              description: Seconds to wait before repeating the call. A hint, not a guarantee.
              schema: { type: integer, example: 30 }
          content:
            application/json:
              schema:
                type: object
                required: [status, message]
                properties:
                  status: { type: string, example: computing }
                  message: { type: string }
                  retry_after_seconds: { type: integer, example: 30 }
                  meta: { type: object }
              example:
                status: computing
                message: this filter has not been computed yet. The work has started; ask again in about 30 seconds and the answer will be here.
                retry_after_seconds: 30
                meta:
                  request_id: 9bb8feb8f661bb27d7e73f0e6a3f3512
                  took_ms: 1.65
        '200':
          description: |
            Aggregates. Without `group_by` the blocks sit at the top level; with it
            the response is `group_by` plus `groups[]`, one set of blocks per group.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InsightsResponse' }
              examples:
                sfSoftwareEngineers:
                  summary: Response to sfSoftwareEngineers, captured 2026-09-10 (lists trimmed to three)
                  value:
                    headline:
                      row_count: 1053
                      job_count: 1047
                      company_count: 300
                      with_salary: 643
                      with_posted_at: 1053
                      with_description: 1004
                      with_detail: 46
                      new_this_week: 59
                    salary:
                    - currency: USD
                      period: 1 YEAR
                      count: 635
                      min:
                        p25: 168000
                        p50: 200000
                        p75: 230000
                        p90: 266000
                      max:
                        p25: 250000
                        p50: 300000
                        p75: 390000
                        p90: 445000
                    posted_at:
                      count: 1053
                      days_since_posted:
                        p25: 36
                        p50: 98
                        p75: 225
                    freshness:
                      count: 1053
                      median_days_live: 98
                      pct_last_7_days: 5.6
                      pct_over_60_days: 63.1
                    top_companies:
                    - company_name: OpenAI
                      count: 124
                    - company_name: Crusoe
                      count: 38
                    - company_name: Harvey
                      count: 31
                    city_split:
                    - value: San Francisco
                      count: 1053
                    country_code_split:
                    - value: US
                      count: 991
                    - value: us
                      count: 46
                    employment_type_split:
                    - value: FullTime
                      count: 966
                    - value: Full-time
                      count: 62
                    - value: Intern
                      count: 13
                    remote_flag_split:
                    - value: 'false'
                      count: 477
                    - value: 'true'
                      count: 418
                    seniority_split:
                    - value: Mid-Senior Level
                      count: 37
                    - value: Mid-Senior level
                      count: 11
                    - value: Entry Level
                      count: 7
                    platform_split:
                    - value: ashby
                      count: 980
                    - value: smartrecruiters
                      count: 46
                    - value: workable
                      count: 24
                    handle_split:
                    - value: openai
                      count: 125
                    - value: crusoe
                      count: 38
                    - value: harvey
                      count: 31
                    meta:
                      request_id: 1e36fe5edd51b40581624be307ac5d45
                      took_ms: 714.67
                byCountry:
                  summary: Response to byCountry, captured 2026-09-10 (one group shown, lists trimmed to three)
                  value:
                    group_by: country_code
                    groups:
                    - country_code: nl
                      row_count: 95
                      with_salary: 34
                      new_this_week: 17
                      with_posted_at: 95
                      with_description: 66
                      with_detail: 28
                      top_companies:
                      - value: Sia
                        count: 16
                      - value: Metyis AG
                        count: 5
                      - value: Sopra Steria
                        count: 5
                      city_split:
                      - value: amsterdam
                        count: 25
                      - value: rotterdam
                        count: 14
                      - value: maastricht
                        count: 8
                      country_code_split:
                      - value: nl
                        count: 95
                      employment_type_split:
                      - value: fulltime_fixed_term
                        count: 32
                      - value: Full-time
                        count: 31
                      - value: fulltime_permanent
                        count: 25
                      remote_flag_split:
                      - value: 'false'
                        count: 88
                      - value: 'true'
                        count: 7
                      seniority_split:
                      - value: mid_level
                        count: 29
                      - value: Mid-Senior Level
                        count: 27
                      - value: experienced
                        count: 25
                      platform_split:
                      - value: recruitee
                        count: 61
                      - value: smartrecruiters
                        count: 28
                      - value: workable
                        count: 4
                      freshness:
                        count: 95
                        median_days_live: 62
                        pct_last_7_days: 17.9
                        pct_over_60_days: 50.5
                        p25: 14.5
                        p75: 226
                        p90: 517.6
                      salary:
                      - count: 32
                        currency: EUR
                        period: month
                        min:
                          p25: 3500
                          p50: 4000
                          p75: 5000
                          p90: 5500
                    meta:
                      request_id: 4e9c60b413cf0442261e8071b9eb49ed
                      took_ms: 1010.13
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Busy' }

  /jobs/{id}:
    get:
      tags: [job]
      operationId: getJob
      summary: One posting by id
      description: |
        The same object a search returns for that posting, including the
        description where the source publishes one.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string }
          description: Value of `_id` from a search response, used verbatim
          example: YXNoYnl8YWxtZWRpYXwxOGM1YmNhOS01MmM5LTRkZWMtOGJmYi1jNmUyNzZkYWNlZGF8MA
      responses:
        '200':
          description: The posting
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Job' }
              examples:
                almedia:
                  summary: Captured 2026-09-10 (description shortened)
                  value:
                    _id: YXNoYnl8YWxtZWRpYXwxOGM1YmNhOS01MmM5LTRkZWMtOGJmYi1jNmUyNzZkYWNlZGF8MA
                    source_platform: ashby
                    handle: almedia
                    source_job_id: 18c5bca9-52c9-4dec-8bfb-c6e276daceda
                    record_index: 0
                    title: Senior Growth Manager (Berlin)
                    company_name: Almedia
                    description: <p style="min-height:1.5em">This isn’t your regular job. Almedia is a place where those wh…
                    posted_at: '2026-09-09T14:43:52.483+00:00'
                    posted_at_raw: '2026-09-09T14:43:52.483+00:00'
                    posting_url: https://jobs.ashbyhq.com/almedia/18c5bca9-52c9-4dec-8bfb-c6e276daceda
                    apply_url: https://jobs.ashbyhq.com/almedia/18c5bca9-52c9-4dec-8bfb-c6e276daceda/application
                    location_raw: Berlin
                    city: Berlin
                    region: Berlin
                    country: Germany
                    country_code: DE
                    postal_code: '10117'
                    remote_flag: 'false'
                    employment_type: FullTime
                    department: Growth
                    team: Advertiser Growth
                    salary_text: €75K – €100K • Offers Equity • Offers Bonus
                    salary:
                      min: 75000
                      max: 100000
                      currency: EUR
                      period: 1 YEAR
                      source: vendor_field
                      confidence: stated
                    first_seen_at: '2026-09-10T02:50:23.085014+00:00'
                    fetched_at: '2026-09-10T02:50:23.085014+00:00'
                    content_hash: daca92c9fc8dc1ee05fc31644fb8e25c7b63503861d3a4475421e7113bb7048d
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404':
          description: No posting with that id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
              example:
                error: not_found
                message: that is not an id this API issued. An _id comes from a search response and is used verbatim.
                meta:
                  request_id: 86ae6ea2dab6b6f5f07e54e1af0b97e0
                  took_ms: 0.27
        '429': { $ref: '#/components/responses/RateLimited' }
        '503': { $ref: '#/components/responses/Busy' }

components:
  securitySchemes:
    rapidApiKey:
      type: apiKey
      in: header
      name: x-rapidapi-key

  responses:
    BadRequest:
      description: No body, or a body that is not a JSON object
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          example:
            error: invalid_request
            message: the request has no body. Send a JSON object; {} is the filter that matches everything.
            meta:
              request_id: 7930fd0adc27ed3a6cde6a53ce338d2c
              took_ms: 0.07
    Unauthorized:
      description: |
        Missing or invalid key. Answered by the marketplace before the request
        reaches this API, in its own shape — `message` only, no `error`.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/MarketplaceError' }
    Forbidden:
      description: |
        A valid key without a subscription to this API. Answered by the
        marketplace, `message` only.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/MarketplaceError' }
          example:
            message: You are not subscribed to this API.
    Unprocessable:
      description: |
        The filter is well-formed JSON but cannot be run. `error` is
        `invalid_request` (an unknown key or a malformed value — `message` names
        it and lists the valid keys) or `keyword_too_common` (a `keywords` term
        matches too many postings — `meta.estimated_matches` and `meta.limit` say
        by how much).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
          examples:
            unknownKey:
              summary: A key the API does not have
              value:
                error: invalid_request
                message: '''location_types'' is not a filter key. The keys are: cities, company_name, country_codes, days_ago, employment_type, handles, job_titles, keywords, limit, page, remote_flag, salary, seniority, source_platforms'
                meta:
                  request_id: de1bb62e86aa15134ec156b64dca09b8
                  took_ms: 0.16
            keywordTooCommon:
              summary: '{"keywords": ["experience"]}'
              value:
                error: keyword_too_common
                message: '''experience'' appears in about 64% of postings (~1,123,068 matches); keyword aggregates are computed for terms under 130,000 matches. Search the role with job_titles, or use a rarer keyword -- narrowing by city, country or employment type does not help: the term scan comes first.'
                meta:
                  term: experience
                  estimated_matches: 1123068
                  limit: 130000
                  request_id: 32c3863fafb59454773a444e7e782c7f
                  took_ms: 1.48
    RateLimited:
      description: Plan quota or rate limit exceeded. Answered by the marketplace; see its rate-limit headers.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/MarketplaceError' }
    Busy:
      description: Capacity exhausted for the moment (`busy`), or the filter did not finish inside the statement deadline (`timeout`). Retry after `retry_after_seconds`, or narrow the filter.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Meta:
      type: object
      description: Present on every response of this API.
      properties:
        request_id:
          type: string
          description: Quote it when you contact support.
          example: be8374ea54b3756c61e55409f09e8075
        took_ms:
          type: number
          description: Server time in milliseconds — tells our part from the network's.
          example: 62.35

    Error:
      type: object
      description: Every error this API itself returns. Branch on `error`, not on `message`.
      required: [error, message]
      properties:
        error:
          type: string
          description: Short code for the client to branch on.
          example: invalid_request
          enum: [invalid_request, keyword_too_common, not_found, busy, timeout]
        message:
          type: string
          description: Human-readable explanation; wording may change.
        retry_after_seconds:
          type: integer
          description: '`busy` and `timeout` only: how long to wait before retrying — 1 second for `busy`, 2 for `timeout`.'
        meta:
          type: object
          properties:
            request_id: { type: string }
            took_ms: { type: number }
            term:
              type: string
              description: '`keyword_too_common` only: the keyword that was refused.'
            estimated_matches:
              type: integer
              description: '`keyword_too_common` only: roughly how many postings mention it.'
            limit:
              type: integer
              description: '`keyword_too_common` only: the ceiling a keyword must stay under.'

    MarketplaceError:
      type: object
      description: Shape of 401, 403 and 429, which the marketplace answers before the request reaches this API.
      properties:
        message: { type: string }

    SalaryFilter:
      type: object
      description: |
        Filter on the advertised pay band; only postings that disclose a salary
        can match. Amounts are never converted between currencies: `min` and `max`
        are compared with the band in the currency the posting advertises. Send
        `currency` to keep the comparison in one currency — without it a 60,000
        bound matches 60,000 euros, dollars and rupees alike.
      properties:
        min:
          type: number
          example: 60000
        max:
          type: number
          example: 90000
        currency:
          type: array
          items: { type: string }
          description: |
            ISO 4217 codes, any case. Keeps only postings advertised in these
            currencies. Nothing is converted.
          example: ["EUR"]
        period:
          type: array
          items: { type: string }
          description: |
            Pay period, as an array: `year`, `month`, `week`, `day` or `hour`. Always
            send it: without it a yearly bound is compared with hourly and monthly
            rates as bare numbers. Vendor spellings read from a response (`1 YEAR`,
            `bi-week-salary`, `one-time`) are accepted too; anything else is rejected
            with 422 `invalid_request`, and the message lists what is accepted.
          example: ["year"]
        match:
          type: string
          enum: [contains, overlaps]
          default: contains
          description: |
            How a posting's band has to meet your bounds. `contains` keeps only
            postings whose whole band sits inside them; `overlaps` keeps every
            posting whose band touches them — what a question like "which roles
            can pay me this" means.

    JobFilter:
      type: object
      description: |
        The filter `/jobs/search` and `/jobs/insights` share. Sent as the request
        body itself. An absent key and an empty array both mean "not applied";
        an unknown key is rejected with `422`. String lists match any of their
        values (OR).
      properties:
        job_titles:
          type: array
          items: { type: string, minLength: 3 }
          description: |
    

# --- truncated at 32 KB (50 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/hiring-index/refs/heads/main/openapi/hiring-index-openapi.yaml