---
title: "Search people"
method: POST
path: "/v1/people/search"
tags: ["Search"]
---

# Search people

`POST /v1/people/search`

## Request

- Base URL: `https://api.recruitmcp.com`
- URL: `https://api.recruitmcp.com/v1/people/search`
- Auth: one of:
  - HTTP bearer
  - OAuth 2 (authorization code, authorization URL `https://recruitmcp.com/oauth/authorize`, token URL `https://recruitmcp.com/oauth/token`, scopes: `sourcing:spend`)

Profiles for a filter set. limit is how many profiles to return, from 1 to 100, and defaults to 25. cursor continues a previous search: send the next_cursor from the last response. Filters map one to one to what the index supports; an unknown or unsupported filter is refused with unsupported_filter, and an unknown body field with invalid_request, never ignored.

company_headcount is A through I: A self-employed, B 1-10, C 11-50, D 51-200, E 201-500, F 501-1,000, G 1,001-5,000, H 5,001-10,000, I 10,001+. years_of_experience_ids and years_at_current_company_ids are 1 through 5: 1 is under a year, 2 is 1 to 2 years, 3 is 3 to 5 years, 4 is 6 to 10 years, 5 is more than 10 years. seniority_level_ids run from 100 to 320. function_ids run from 1 to 26.

current_companies and past_companies take a company URL or company id from POST /v1/companies:resolve. A bare name is not resolved and does not match.

data holds at most limit profiles. next_cursor is null when nothing remains. A suppressed person is never returned and never charged: search counts them in omitted.suppressed and keeps filling limit. exclude_seen defaults to false on REST and MCP, so people already in the seen register come back with already_seen true.

One credit per profile in data, once per workspace. Profiles fetched and not returned are not charged. A later call with cursor returns those cached profiles without asking the index again, and charges them when they are returned. An empty result costs nothing. A profile already billed in this workspace is free.

An authorized call runs and charges. confirm is deprecated and ignored. dry_run=true previews and charges nothing; dry_run is true, requires_confirm is false, and estimate.credits_max equals limit. The estimate includes balance_credits, sufficient, and warning. sufficient is false when the visible balance is below credits_max, and warning names 402 insufficient_credits. Above the key's approval_threshold_credits the call returns 409 approval_required with an estimate and a confirm_token.

Each search has a time budget of about 20 seconds. When it ends before limit is reached, the response is 200 with partial: true: data holds the profiles that arrived, only those are charged, and next_cursor continues the rest. Several geo_ids are fetched in parallel and interleaved; a place that did not answer in time is continued by next_cursor. Only when nothing arrived is the response 504 gateway_timeout, with nothing charged. The pages asked for keep loading, so the same request after Retry-After usually reads them from cache. Fewer geo_ids per request also finish faster. A known location drops profiles outside that place; they are counted in omitted.outside_geo and not charged. seniority 310 is CXO, and a CXO-only search drops other titles into omitted.seniority without charging them. profile_id is the search id. alternate_id is the other profile id for the same person, and it is null until that person has been enriched. total_elements is an estimate from the index before the location post-filter. omitted.outside_geo counts profiles dropped from pages already read and is not subtracted from total_elements.

Scope: `sourcing:spend`.

## Query parameters

- `dry_run` boolean

## Headers

- `Idempotency-Key` string
- `RecruitMCP-Version` '2026-10-13'
- `RecruitMCP-Customer` string

## Request body

- PeopleSearchRequest
  - `filters` PeopleFilters, required: People-search filters. Each one maps to a filter the profile index applies; anything else is rejected with unsupported_filter.
    - `current_job_titles` string[]: Current job titles. Up to 70.
    - `exclude_current_job_titles` string[]: Current job titles to exclude. Up to 70.
    - `past_job_titles` string[]: Titles held before the current role. Up to 70.
    - `exclude_past_job_titles` string[]: Past titles to exclude. Up to 70.
    - `geo_ids` string[]: Numeric location ids from GET /v1/lookups/locations or /v1/lookups/areas. Up to 70.
    - `exclude_geo_ids` string[]: Location ids to exclude. Up to 70.
    - `current_companies` string[]: Current employers. Pass a company URL or company id from POST /v1/companies:resolve. A bare name is sent as written and does not match a company. Up to 50.
    - `exclude_current_companies` string[]: Current employers to exclude, as company URLs or ids from companies:resolve. Up to 50.
    - `past_companies` string[]: Former employers. Pass a company URL or company id from POST /v1/companies:resolve. A bare name is not resolved. Up to 50.
    - `exclude_past_companies` string[]: Former employers to exclude, as company URLs or ids. Up to 50.
    - `schools` string[]: Schools attended, by name. Up to 50.
    - `exclude_schools` string[]: Schools to exclude. Up to 50.
    - `industry_ids` string[]: Industry ids from GET /v1/lookups/industries.
    - `exclude_industry_ids` string[]: Industry ids to exclude.
    - `function_ids` string[]: Job function ids from GET /v1/lookups/functions.
    - `exclude_function_ids` string[]: Job function ids to exclude.
    - `seniority_level_ids` string[]: Seniority level ids from GET /v1/lookups/seniorities.
    - `exclude_seniority_level_ids` string[]: Seniority level ids to exclude.
    - `years_of_experience_ids` string[]: Experience ranges from GET /v1/lookups/experience-ranges.
    - `years_at_current_company_ids` string[]: Experience ranges for time at the current company.
    - `company_headcount` string[]: Current-company headcount ranges from GET /v1/lookups/headcounts.
    - `profile_languages` string[]: Profile language codes from GET /v1/lookups/languages.
    - `first_names` string[]: First names. Up to 50.
    - `last_names` string[]: Last names. Up to 50.
    - `recently_changed_jobs` boolean: Only people who changed jobs in the last 90 days.
    - `search` string: Keywords. Quote phrases and combine with OR, AND, NOT. At most 1,000 characters.
  - `limit` integer: Profiles to return. 1 to 100. Defaults to 25. A JSON number. Query parameters accept numeric strings; a JSON body does not.
  - `cursor` string: From the previous response next_cursor. Omit to start at the beginning.
  - `exclude_seen` boolean: Drop people already in the seen register. Default false on REST and MCP: they are returned with already_seen true.
  - `confirm` boolean: Deprecated. Accepted and ignored. An authorized search runs without it.
  - `confirm_token` string: From approval_required. Required only when the estimate is above the key threshold.

## Response `200`

Live response is a PeopleSearchPage. With ?dry_run=true, a PeopleSearchPreview; nothing is charged or sent.

- union
  - PeopleSearchPage
    - `object` 'people_page'
    - `query_key` string, required: Stable id of this filter set. A lead is charged once per workspace.
    - `total_elements` integer, nullable: Estimate from the index before the location post-filter. Profiles dropped for location are counted in omitted.outside_geo and are not subtracted here.
    - `data` PersonStub[], required
      - `profile_id` string, required: Search id for this person, an opaque string. GET /v1/people/{profile_id} also accepts the alternate id.
      - `alternate_id` string, nullable: The other profile id for the same person. Null until the person has been enriched and both ids are known.
      - `full_name` string, nullable
      - `location` string, nullable
      - `profile_url` string, nullable
      - `photo` string, nullable: Profile photo on this API, /v1/media/{token}. The token expires after 7 days; a later response signs a new one. Null when there is no photo.
      - `current_positions` object[]
        - `title` string, nullable
        - `company_name` string, nullable
        - `company_id` string, nullable
        - `company_url` string, nullable
        - `current` boolean
        - `description` string, nullable: The role description as written on the profile.
        - `started_on` object, nullable: Start of this role, for example { year: 2020, month: 1, text: "2020-01" }. Keys can be missing.
          - `year` integer
          - `month` integer
          - `text` string
        - `tenure_at_position` object, nullable: Time in this role, for example { numYears: 2, numMonths: 7 }. A key whose value is 0 is left out, so { numYears: 4 } means 4 years and 0 months. Null when neither is known.
          - `numYears` integer
          - `numMonths` integer
        - `tenure_at_company` object, nullable: Time at this company, same shape as tenure_at_position: a key whose value is 0 is left out.
          - `numYears` integer
          - `numMonths` integer
      - `identity_key` string, nullable
      - `already_seen` boolean: True when the person is in your seen register.
    - `next_cursor` string, nullable, required: Pass this back as cursor to continue. Null when nothing remains.
    - `omitted` object
      - `suppressed` integer: Always removed.
      - `already_seen` integer: Removed because exclude_seen was true.
      - `unpaid` integer: Not returned, because the balance ran out. Not charged. Continue with next_cursor.
      - `cap` integer: Not returned, because the key or customer daily credit cap was reached. Not charged. Continue with next_cursor after resets_at.
      - `outside_geo` integer: Removed because the location is outside a requested place. Not charged.
      - `seniority` integer: Removed because the title does not match seniority 310 (CXO). Not charged.
    - `applied_filters` AppliedFilter[], required: Each filter as sent to the person index, plus suppression and exclude_seen.
      - `field` string, required
      - `effect` 'hard_filter' | 'search_order' | 'exclusion' | 'result_filter', required
      - `detail` string, required
    - `ignored_fields` IgnoredField[], required: Always empty: unsupported filters are refused with unsupported_filter.
      - `field` string, required
      - `reason` string, required
    - `cost` Cost, required
      - `credits` integer, required: Credits charged by this call. Whole credits. 0 on an idempotent replay, where the body also has replayed: true and the header Idempotent-Replayed: true.
      - `cached_items` integer: Items read from cache. On a replay, the value of the first response.
      - `live_items` integer: Items fetched live by this call. 0 on a replay.
      - `upstream_calls` integer: Index calls made by this call. 0 on a replay.
    - `partial` boolean: Present and true when the time budget ended before limit was reached. data holds the profiles that arrived, only those are charged, and next_cursor continues the rest, which keep loading in the background.
  - PeopleSearchPreview
    - `dry_run` true, required: True. This response spent nothing and changed nothing.
    - `estimate` Estimate, required
      - `credits_min` integer, required: Lowest expected spend. Whole credits.
      - `credits_max` integer, required: Ceiling. Spend never passes it. Whole credits.
      - `expected_pages` integer
      - `expected_profile_fetches` integer
      - `skipped_pages_from_checkpoints` integer: Pages already paid for in an earlier search. Not charged again.
      - `cached_items` integer: Items served from cache, free.
      - `balance_credits` integer, nullable: Account balance. A customer-bound key gets that customer's remaining daily cap, or null when the customer has no cap.
      - `sufficient` boolean: True when the visible balance covers credits_max, or when that balance is hidden. False when a live call can return 402 insufficient_credits.
      - `warning` string, nullable: Null when sufficient is true. Otherwise the live call can return 402 insufficient_credits.
      - `requires_confirm` boolean, required: False on a search, profile, company, or contact preview. True when the call still needs an explicit confirm.
    - `applied_filters` AppliedFilter[], required
      - `field` string, required
      - `effect` 'hard_filter' | 'search_order' | 'exclusion' | 'result_filter', required
      - `detail` string, required
    - `ignored_fields` IgnoredField[], required
      - `field` string, required
      - `reason` string, required
    - `cost` Cost, required
      - `credits` integer, required: Credits charged by this call. Whole credits. 0 on an idempotent replay, where the body also has replayed: true and the header Idempotent-Replayed: true.
      - `cached_items` integer: Items read from cache. On a replay, the value of the first response.
      - `live_items` integer: Items fetched live by this call. 0 on a replay.
      - `upstream_calls` integer: Index calls made by this call. 0 on a replay.

## Other responses

- `400`: `idempotency_key_required` (do not retry): This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview. `invalid_request` (do not retry): A field is missing or has a value this endpoint does not accept. `unsupported_filter` (do not retry): skills is not a supported filter. `unsupported_version` (do not retry): RecruitMCP-Version is not a supported version.
- `401`: `unauthorized` (do not retry): Missing bearer API key.
- `402`: `daily_credit_cap_reached` (do not retry): This key's daily credit cap is reached. `free_tier_limit` (do not retry): This workspace has used its free upstream allowance. `insufficient_credits` (do not retry): Not enough credits for this operation.
- `403`: `forbidden` (do not retry): This customer is disabled. `insufficient_scope` (do not retry): This key does not include the scope this endpoint requires.
- `409`: `approval_required` (do not retry): This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it. `confirm_token_used` (do not retry): This confirm_token was already used. Ask for a new preview to get another. `idempotency_in_progress` (retry): A request with this Idempotency-Key is still running. Retry after it finishes. `idempotency_key_reused` (do not retry): Idempotency-Key was already used with a different request.
- `429`: `rate_limited` (retry): Per-key rate limit exceeded. `upstream_busy` (retry): The profile source is busy. Retry after the delay.
- `502`: `upstream_busy` (retry): The profile source is busy. Retry shortly. `upstream_error` (retry): The profile source returned a response that could not be used. Retry later.
- `503`: `upstream_busy` (retry): The profile source is paused.
- `504`: `gateway_timeout` (retry): The search did not finish in time. Nothing was charged. The index is still loading these results: retry the same request after Retry-After and it reads them from cache. With 4 geo_ids, fewer per request (or one per request) also finishes faster.

## Changes

- **2026-10-20** `ad27b35513a6`: 9 warning
  - added the new `approval_forced` enum value to the `error/code` response property for the response status `400`
  - added the new `approval_forced` enum value to the `error/code` response property for the response status `401`
  - added the new `approval_forced` enum value to the `error/code` response property for the response status `402`
  - added the new `approval_forced` enum value to the `error/code` response property for the response status `403`
  - …5 more

[Operation history](https://skmtc.dev/recruitmcp/apis/recruitmcp-api/changes/v1/people/search/post.md)

---

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