# API reference

Search, Profiles, Contacts, Lists, and Account each start with one request, then filters, pagination, and recipes. The reference for that area follows. Each endpoint has a Try it panel.

[OpenAPI document](https://recruitmcp.com/openapi.yaml) [Import to Postman](https://app.postman.com/import?url=https%3A%2F%2Frecruitmcp.com%2Fopenapi.yaml) [MCP setup](https://recruitmcp.com/docs/mcp) [Quickstart](https://recruitmcp.com/docs/quickstart)

## If you want to…

- Turn a place name into a location id [GET /v1/lookups/locations](https://recruitmcp.com/docs/api#lookupLocations)
- Find location ids around a city [GET /v1/lookups/areas](https://recruitmcp.com/docs/api#lookupArea)
- Resolve a company name [POST /v1/companies:resolve](https://recruitmcp.com/docs/api#resolveCompanies)
- Search people and see the cost first [POST /v1/people/search](https://recruitmcp.com/docs/api#searchPeople)
- Enrich a profile [POST /v1/people:enrich](https://recruitmcp.com/docs/api#enrichPeople)
- Match one person by name and company [POST /v1/people:match](https://recruitmcp.com/docs/api#matchPerson)
- Look up a contact [POST /v1/people/contacts:fetch](https://recruitmcp.com/docs/api#lookupContacts)
- Save a shortlist [POST /v1/lists](https://recruitmcp.com/docs/api#createList)
- Set candidate status [POST /v1/seen](https://recruitmcp.com/docs/api#upsertSeen)
- Outreach runs from your agent on your own connected account [Outreach runs from your agent on your own connected account](https://recruitmcp.com/pricing)
- Read the credit balance [GET /v1/billing/balance](https://recruitmcp.com/docs/api#getBalance)
- Buy a credit pack [POST /v1/billing/checkout](https://recruitmcp.com/docs/api#createCheckout)
- Start a monthly plan [POST /v1/billing/subscriptions](https://recruitmcp.com/docs/api#createSubscription)
- Read this API as plain text [/llms.txt](https://recruitmcp.com/llms.txt)

## Common headers and errors

These apply to every authenticated call. They are not repeated on each endpoint. A preview spends nothing. Search, profile, company, and contact calls run when the key is authorized. On REST, omitting dry_run is the confirmation. On MCP it is confirm=true. The guide lists cause and fix for every status at [Errors](https://recruitmcp.com/docs#errors).

### Preview (dry_run)

Preview mode. dry_run=true as a query parameter, or dry_run set to true in the JSON body, is a preview. Either one spends nothing and changes nothing. The response includes dry_run true, an estimate, and cost.credits 0. Idempotency-Key is not required while dry_run is true. A live call omits the query parameter and omits dry_run from the body. On REST, omitting dry_run is the confirmation. On MCP it is confirm=true.

```
POST /v1/people/search?dry_run=true

{
  "filters": {
    "current_job_titles": [
      "Backend Engineer"
    ],
    "geo_ids": [
      "90009496"
    ]
  },
  "limit": 10
}
```

### Headers

- `RecruitMCP-Version` pins one request. With no header, the call uses the version stored on the key.
- `RecruitMCP-Customer` is `cus_…` when you act inside a customer. A key bound to a customer ignores it.
- `Idempotency-Key` is required unless `dry_run=true`. Same key and body replays for 24 hours. A different body is 409 idempotency_key_reused.

### Errors

- 400 unsupported_version. The pin is not in the supported list. details.supported names the dates that remain.
- 401 unauthorized. The Authorization header is missing, or the key is unknown or revoked.
- 403 insufficient_scope. The key lacks the scope named on the operation.
- 403 forbidden. A customer-bound key called an account route, or the customer is disabled.
- 429 rate_limited. Wait for Retry-After. RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset are on this response and on successes.

## Search

Search people, then turn a place, a company, or a filter label into the id search expects.

### First request

Search returns profile stubs for a filter set. The first request is a preview: Backend Engineer in the London area, limit 10. It spends nothing.

```
POST /v1/people/search?dry_run=true

{
  "filters": {
    "current_job_titles": [
      "Backend Engineer"
    ],
    "geo_ids": [
      "90009496"
    ]
  },
  "limit": 10
}
```

### Filters

geo_ids come from GET /v1/lookups/locations or GET /v1/lookups/areas. seniority_level_ids come from GET /v1/lookups/seniorities. current_companies and past_companies take a company URL from POST /v1/companies:resolve. A bare company name is not resolved and does not match. current_job_titles is the title text. The search operation lists the other codes (headcount, years, and function).

```
GET /v1/lookups/locations?q=Amsterdam
```

```
GET /v1/lookups/locations?q=London&prefer_area=true
```

```
GET /v1/lookups/seniorities?q=Senior
```

```
POST /v1/companies:resolve?dry_run=true

{
  "names": [
    "Paperkiln"
  ]
}
```

### Pagination

limit is how many profiles to return, from 1 to 100. The default is 25. The first request below uses 10. Pass next_cursor back as cursor to continue. Omit cursor to start at the beginning. next_cursor is null when nothing remains.

### Recipes

#### Senior backend engineers in Amsterdam

Title Backend Engineer, Amsterdam's location id, and the seniority id for Senior.

```
POST /v1/people/search?dry_run=true

{
  "filters": {
    "current_job_titles": [
      "Backend Engineer"
    ],
    "geo_ids": [
      "102011674"
    ],
    "seniority_level_ids": [
      "120"
    ]
  },
  "limit": 10
}
```

#### Search by employer and title

Title Backend Engineer and the company_id resolve returns for a company (here the fictional Paperkiln).

```
POST /v1/people/search?dry_run=true

{
  "filters": {
    "current_job_titles": [
      "Backend Engineer"
    ],
    "current_companies": [
      "51207"
    ]
  },
  "limit": 10
}
```

### Reference

### POST `/v1/people/search`

#### Purpose

Search people

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.

#### Scopes

sourcing:spend

#### Cost, free cases, and preview

Credit cost. Up to limit × 1 credit per profile returned. Default limit is 25. A profile already billed in this workspace is free. An empty result is free. dry_run spends nothing, and its credits_max equals limit.

Preview mode. dry_run=true as a query parameter, or dry_run set to true in the JSON body, is a preview. Either one spends nothing and changes nothing. The response includes dry_run true, an estimate, and cost.credits 0. Idempotency-Key is not required while dry_run is true. A live call omits the query parameter and omits dry_run from the body. On REST, omitting dry_run is the confirmation. On MCP it is confirm=true. [Preview (dry_run)](https://recruitmcp.com/docs/api#preview) shows one request. [Execution, preview and approval](https://recruitmcp.com/docs#execution) is the rule for this call.

#### Parameters

- filters

  body · PeopleFilters · required People-search filters. Each one maps to a filter the profile index applies; anything else is rejected with unsupported_filter.

- filters.current_job_titles

  body · array of string · optional Current job titles. Up to 70.

- filters.exclude_current_job_titles

  body · array of string · optional Current job titles to exclude. Up to 70.

- filters.past_job_titles

  body · array of string · optional Titles held before the current role. Up to 70.

- filters.exclude_past_job_titles

  body · array of string · optional Past titles to exclude. Up to 70.

- filters.geo_ids

  body · array of string · optional Numeric location ids from GET /v1/lookups/locations or /v1/lookups/areas. Up to 70.

- filters.exclude_geo_ids

  body · array of string · optional Location ids to exclude. Up to 70.

- filters.current_companies

  body · array of string · optional 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.

- filters.exclude_current_companies

  body · array of string · optional Current employers to exclude, as company URLs or ids from companies:resolve. Up to 50.

- filters.past_companies

  body · array of string · optional Former employers. Pass a company URL or company id from POST /v1/companies:resolve. A bare name is not resolved. Up to 50.

- filters.exclude_past_companies

  body · array of string · optional Former employers to exclude, as company URLs or ids. Up to 50.

- filters.schools

  body · array of string · optional Schools attended, by name. Up to 50.

- filters.exclude_schools

  body · array of string · optional Schools to exclude. Up to 50.

- filters.industry_ids

  body · array of string · optional Industry ids from GET /v1/lookups/industries.

- filters.exclude_industry_ids

  body · array of string · optional Industry ids to exclude.

- filters.function_ids

  body · array of string · optional Job function ids from GET /v1/lookups/functions.

- filters.exclude_function_ids

  body · array of string · optional Job function ids to exclude.

- filters.seniority_level_ids

  body · array of string · optional Seniority level ids from GET /v1/lookups/seniorities.

- filters.exclude_seniority_level_ids

  body · array of string · optional Seniority level ids to exclude.

- filters.years_of_experience_ids

  body · array of "1" | "2" | "3" | "4" | "5" · optional Experience ranges from GET /v1/lookups/experience-ranges.

- filters.years_at_current_company_ids

  body · array of "1" | "2" | "3" | "4" | "5" · optional Experience ranges for time at the current company.

- filters.company_headcount

  body · array of "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" · optional Current-company headcount ranges from GET /v1/lookups/headcounts.

- filters.profile_languages

  body · array of string · optional Profile language codes from GET /v1/lookups/languages.

- filters.first_names

  body · array of string · optional First names. Up to 50.

- filters.last_names

  body · array of string · optional Last names. Up to 50.

- filters.recently_changed_jobs

  body · boolean · optional Only people who changed jobs in the last 90 days.

- filters.search

  body · string · optional Keywords. Quote phrases and combine with OR, AND, NOT. At most 1,000 characters.

- limit

  body · integer · optional Profiles to return. 1 to 100. Defaults to 25. A JSON number. Query parameters accept numeric strings; a JSON body does not.

- cursor

  body · string · optional From the previous response next_cursor. Omit to start at the beginning.

- exclude_seen

  body · boolean · optional Drop people already in the seen register. Default false on REST and MCP: they are returned with already_seen true.

- confirm_token

  body · string · optional From approval_required. Required only when the estimate is above the key threshold.

#### Minimal request

Example request. Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/people/search?dry_run=true' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"filters":{"current_job_titles":["Backend Engineer"],"geo_ids":["90009496"],"past_companies":["51207"],"profile_languages":["en"]},"limit":10}'
```

#### Success

HTTP 200. Live response. Example response.

```
{
  "object": "people_page",
  "query_key": "qk_backend_london",
  "total_elements": 128,
  "data": [
    {
      "profile_id": "lead_8f2a1c_1",
      "full_name": "Ada Peeters",
      "location": "London, England, United Kingdom",
      "alternate_id": null,
      "photo": null,
      "profile_url": "https://profiles.example.com/ada-peeters",
      "current_positions": [
        {
          "title": "Backend Engineer",
          "company_name": "Acme Robotics",
          "company_id": null,
          "company_url": "https://companies.example.com/acme-robotics",
          "current": true,
          "description": "Builds the fleet API in Go.",
          "started_on": {
            "year": 2021,
            "month": 3,
            "text": "2021-03"
          },
          "tenure_at_position": {
            "numYears": 5,
            "numMonths": 7
          },
          "tenure_at_company": {
            "numYears": 6
          }
        }
      ],
      "identity_key": "ada peeters|acme robotics",
      "already_seen": false
    },
    {
      "profile_id": "lead_3c71e0_2",
      "full_name": "Jonas Vermeulen",
      "location": "London, England, United Kingdom",
      "alternate_id": null,
      "photo": null,
      "profile_url": "https://profiles.example.com/jonas-vermeulen",
      "current_positions": [
        {
          "title": "Platform Engineer",
          "company_name": "Paperkiln",
          "company_id": null,
          "company_url": "https://companies.example.com/paperkiln",
          "current": true,
          "description": null,
          "started_on": {
            "year": 2025,
            "month": 11,
            "text": "2025-11"
          },
          "tenure_at_position": {
            "numMonths": 11
          },
          "tenure_at_company": {
            "numMonths": 11
          }
        }
      ],
      "identity_key": "jonas vermeulen|paperkiln",
      "already_seen": false
    },
    {
      "profile_id": "lead_5d09b4_3",
      "full_name": "Lina Dubois",
      "location": "London, England, United Kingdom",
      "alternate_id": null,
      "photo": null,
      "profile_url": "https://profiles.example.com/lina-dubois",
      "current_positions": [
        {
          "title": "Backend Engineer",
          "company_name": "Acme Robotics",
          "company_id": null,
          "company_url": "https://companies.example.com/acme-robotics",
          "current": true,
          "description": null,
          "started_on": {
            "year": 2023,
            "month": 1,
            "text": "2023-01"
          },
          "tenure_at_position": {
            "numYears": 3,
            "numMonths": 9
          },
          "tenure_at_company": {
            "numYears": 3,
            "numMonths": 9
          }
        }
      ],
      "identity_key": "lina dubois|acme robotics",
      "already_seen": true
    }
  ],
  "next_cursor": "eyJvZmZzZXQiOjN9",
  "omitted": {
    "suppressed": 0,
    "already_seen": 0,
    "unpaid": 0
  },
  "applied_filters": [
    {
      "field": "current_job_titles",
      "effect": "hard_filter",
      "detail": "Backend Engineer"
    }
  ],
  "ignored_fields": [],
  "cost": {
    "credits": 3,
    "cached_items": 3,
    "live_items": 0,
    "upstream_calls": 1
  }
}
```

HTTP 200. With dry_run=true. Example response.

```
{
  "dry_run": true,
  "estimate": {
    "credits_min": 0,
    "credits_max": 10,
    "balance_credits": 1250,
    "sufficient": true,
    "warning": null,
    "requires_confirm": false
  },
  "applied_filters": [
    {
      "field": "current_job_titles",
      "effect": "hard_filter",
      "detail": "Backend Engineer"
    }
  ],
  "ignored_fields": [],
  "cost": {
    "credits": 0,
    "cached_items": 0,
    "live_items": 0,
    "upstream_calls": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 idempotency_key_required · request_id in the body · Retryable: no This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview. { "error": { "code": "idempotency_key_required", "message": "This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 400 unsupported_filter · request_id in the body · Retryable: no skills is not a supported filter. { "error": { "code": "unsupported_filter", "message": "skills is not a supported filter.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "field": "skills" } } }
- 402 daily_credit_cap_reached · request_id in the body · Retryable: no This key's daily credit cap is reached. { "error": { "code": "daily_credit_cap_reached", "message": "This key's daily credit cap is reached.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "cap": "key", "resets_at": "2026-10-14T00:00:00.000Z" } }, "resets_at": "2026-10-14T00:00:00.000Z" }
- 402 free_tier_limit · request_id in the body · Retryable: no This workspace has used its 60 free search pages. A paid plan removes the cap. { "error": { "code": "free_tier_limit", "message": "This workspace has used its 60 free search pages. A paid plan removes the cap.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "kind": "search_pages", "limit": 60 } } }
- 402 insufficient_credits · request_id in the body · Retryable: no Not enough credits for this operation. { "error": { "code": "insufficient_credits", "message": "Not enough credits for this operation.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "required_credits": 25, "balance_credits": 3 } }, "required_credits": 25, "balance_credits": 3 }
- 409 approval_required · request_id in the body · Retryable: no This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it. { "error": { "code": "approval_required", "message": "This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "approval_threshold_credits": 500, "credits_max": 25 } }, "estimate": { "credits_min": 0, "credits_max": 25, "requires_confirm": false }, "confirm_token": "v1.payload.sig", "approval_threshold_credits": 500, "cost": { "credits": 0 } }
- 409 confirm_token_used · request_id in the body · Retryable: no This confirm_token was already used. Ask for a new preview to get another. { "error": { "code": "confirm_token_used", "message": "This confirm_token was already used. Ask for a new preview to get another.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_in_progress · request_id in the body · Retryable: yes A request with this Idempotency-Key is still running. Retry after it finishes. { "error": { "code": "idempotency_in_progress", "message": "A request with this Idempotency-Key is still running. Retry after it finishes.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_key_reused · request_id in the body · Retryable: no Idempotency-Key was already used with a different request. { "error": { "code": "idempotency_key_reused", "message": "Idempotency-Key was already used with a different request.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 429 upstream_busy · request_id in the body · Retryable: yes The profile source is busy. Retry after the delay. { "error": { "code": "upstream_busy", "message": "The profile source is busy. Retry after the delay.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_busy · request_id in the body · Retryable: yes The profile source is busy. Retry shortly. { "error": { "code": "upstream_busy", "message": "The profile source is busy. Retry shortly.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_error · request_id in the body · Retryable: yes The profile source returned a response that could not be used. Retry later. { "error": { "code": "upstream_error", "message": "The profile source returned a response that could not be used. Retry later.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "hint": "retry later" } } }
- 503 upstream_busy · request_id in the body · Retryable: yes The profile source is paused. { "error": { "code": "upstream_busy", "message": "The profile source is paused.", "request_id": "req_01J0REQUEST0EXAMPLE" }, "retry_at": "2026-10-13T15:00:00.000Z" }
- 504 gateway_timeout · request_id in the body · Retryable: yes 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. { "error": { "code": "gateway_timeout", "message": "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.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "retry_after_seconds": 10, "geo_ids": 4 } } }

#### Idempotency and retry

Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body. Generate a fresh key with uuidgen, python3 -c 'import uuid; print(uuid.uuid4())', or crypto.randomUUID(). A 402 and a 429 are not stored, so the same key still works after you fix the cause. Wait for Retry-After on 429.

#### Side effects and approval

A dry run has no side effects. The live call charges for what comes back. [Execution, preview and approval](https://recruitmcp.com/docs#execution) says when the call runs and when it returns 409 approval_required.

#### Next step

- [Enrich a full profile](https://recruitmcp.com/docs/api#enrichPeople)
- [Save the shortlist](https://recruitmcp.com/docs/api#createList)

### GET `/v1/lookups/locations`

#### Purpose

Find location ids

Turns a place name into geo_ids for people search filters. Free, including a place this server has not seen before. The result is cached. This call never charges. An empty result includes a hint. A region name points at a city or the country.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free. A place this server has not seen before is looked up and cached, and this call never charges.

#### Parameters

- q

  query · string · required Place name, for example London.

- prefer_area

  query · boolean · optional Prefer the metropolitan area over the city.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lookups/locations?q=London' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "geo_id": "90009496",
      "title": "London Area, United Kingdom",
      "is_area": true
    },
    {
      "geo_id": "102257491",
      "title": "Greater London, England, United Kingdom",
      "is_area": false
    }
  ],
  "next_cursor": null,
  "has_more": false,
  "cost": {
    "credits": 0,
    "cached_items": 0,
    "live_items": 0,
    "upstream_calls": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 402 free_tier_limit · request_id in the body · Retryable: no This workspace has used its 40 free location lookups for today. A paid plan removes the cap. { "error": { "code": "free_tier_limit", "message": "This workspace has used its 40 free location lookups for today. A paid plan removes the cap.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "kind": "geo_lookups", "limit": 40 } } }
- 429 upstream_busy · request_id in the body · Retryable: yes The profile source is busy. Retry after the delay. { "error": { "code": "upstream_busy", "message": "The profile source is busy. Retry after the delay.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_busy · request_id in the body · Retryable: yes The profile source is busy. Retry shortly. { "error": { "code": "upstream_busy", "message": "The profile source is busy. Retry shortly.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_error · request_id in the body · Retryable: yes The profile source returned a response that could not be used. Retry later. { "error": { "code": "upstream_error", "message": "The profile source returned a response that could not be used. Retry later.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "hint": "retry later" } } }
- 503 upstream_busy · request_id in the body · Retryable: yes The profile source is paused. { "error": { "code": "upstream_busy", "message": "The profile source is paused.", "request_id": "req_01J0REQUEST0EXAMPLE" }, "retry_at": "2026-10-13T15:00:00.000Z" }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search people with the geo id](https://recruitmcp.com/docs/api#searchPeople)

### GET `/v1/lookups/areas`

#### Purpose

Find the location ids around anchor places

Free. Anchor places and radius_km become at most 12 location ids. Under 20 km only the anchor city is used. At 20 km or more, major cities within radius_km of an anchor are included when coordinates are known. geo_ids is that set. The search index has no radius: pass geo_ids as the location filter of a people search.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- anchors

  query · array of string · required Place names. Repeat the parameter or separate with |.

- radius_km

  query · integer · optional Default 30.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lookups/areas?anchors=London' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "geo_ids": [
    "90009496"
  ],
  "entries": [
    {
      "geo_id": "90009496",
      "title": "London Area, United Kingdom",
      "is_area": true
    }
  ],
  "next_cursor": null,
  "has_more": false,
  "note": "The search index has no radius. Pass these geo ids as the location filter. Major cities within 30 km are included, at most 12 geo ids.",
  "cost": {
    "credits": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 402 free_tier_limit · request_id in the body · Retryable: no This workspace has used its 40 free location lookups for today. A paid plan removes the cap. { "error": { "code": "free_tier_limit", "message": "This workspace has used its 40 free location lookups for today. A paid plan removes the cap.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "kind": "geo_lookups", "limit": 40 } } }
- 429 upstream_busy · request_id in the body · Retryable: yes The profile source is busy. Retry after the delay. { "error": { "code": "upstream_busy", "message": "The profile source is busy. Retry after the delay.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_busy · request_id in the body · Retryable: yes The profile source is busy. Retry shortly. { "error": { "code": "upstream_busy", "message": "The profile source is busy. Retry shortly.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_error · request_id in the body · Retryable: yes The profile source returned a response that could not be used. Retry later. { "error": { "code": "upstream_error", "message": "The profile source returned a response that could not be used. Retry later.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "hint": "retry later" } } }
- 503 upstream_busy · request_id in the body · Retryable: yes The profile source is paused. { "error": { "code": "upstream_busy", "message": "The profile source is paused.", "request_id": "req_01J0REQUEST0EXAMPLE" }, "retry_at": "2026-10-13T15:00:00.000Z" }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search people with these geo ids](https://recruitmcp.com/docs/api#searchPeople)

### GET `/v1/lookups/companies`

#### Purpose

Search known companies

Searches the built-in company list. Free. For any other name use POST /v1/companies:resolve.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- q

  query · string · required Part of a company name.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lookups/companies?q=London' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "query_name": "Paperkiln",
      "company_name": "Paperkiln",
      "company_id": "51207",
      "company_url": "https://companies.example.com/paperkiln",
      "universal_name": "paperkiln",
      "industry": "Computer Software",
      "employee_count": 400,
      "website": "https://example.com",
      "not_found": false
    }
  ],
  "next_cursor": null,
  "has_more": false,
  "cost": {
    "credits": 1,
    "cached_items": 0,
    "live_items": 1,
    "upstream_calls": 1
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Resolve a name that is not on the built-in list](https://recruitmcp.com/docs/api#resolveCompanies)

### POST `/v1/companies:resolve`

#### Purpose

Resolve company names to company URLs

Use the URLs in current_companies or past_companies filters. A cache hit, a company on the built-in list, and a lookup with no result are free. A company found live costs 1 credit, unless the profile source marks that lookup free.

#### Scopes

sourcing:spend

#### Cost, free cases, and preview

Credit cost. 1 credit when a company is found live, unless the profile source marks that lookup free. A lookup with no result, a cache hit, or a built-in company is free.

Preview mode. dry_run=true as a query parameter, or dry_run set to true in the JSON body, is a preview. Either one spends nothing and changes nothing. The response includes dry_run true, an estimate, and cost.credits 0. Idempotency-Key is not required while dry_run is true. A live call omits the query parameter and omits dry_run from the body. On REST, omitting dry_run is the confirmation. On MCP it is confirm=true. [Preview (dry_run)](https://recruitmcp.com/docs/api#preview) shows one request. [Execution, preview and approval](https://recruitmcp.com/docs#execution) is the rule for this call.

#### Parameters

- names

  body · array of string · required Company names or company URLs. Up to 50.

- country

  body · string · optional Country that disambiguates common names.

- confirm_token

  body · string · optional From approval_required. Required only when the estimate is above the key threshold.

#### Minimal request

Example request. Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/companies:resolve?dry_run=true' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"names":["Paperkiln","Acme Robotics"],"country":"Belgium"}'
```

#### Success

HTTP 200. Live response. Example response.

```
{
  "object": "list",
  "data": [
    {
      "query_name": "Paperkiln",
      "company_name": "Paperkiln",
      "company_id": "51207",
      "company_url": "https://companies.example.com/paperkiln",
      "universal_name": "paperkiln",
      "industry": "Computer Software",
      "employee_count": 400,
      "website": "https://example.com",
      "not_found": false
    }
  ],
  "next_cursor": null,
  "has_more": false,
  "cost": {
    "credits": 1,
    "cached_items": 0,
    "live_items": 1,
    "upstream_calls": 1
  }
}
```

HTTP 200. With dry_run=true. Example response.

```
{
  "dry_run": true,
  "estimate": {
    "credits_min": 0,
    "credits_max": 1,
    "balance_credits": 1250,
    "sufficient": true,
    "warning": null,
    "requires_confirm": false,
    "cached_items": 0
  },
  "cost": {
    "credits": 0,
    "cached_items": 0,
    "live_items": 0,
    "upstream_calls": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 idempotency_key_required · request_id in the body · Retryable: no This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview. { "error": { "code": "idempotency_key_required", "message": "This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 402 daily_credit_cap_reached · request_id in the body · Retryable: no This key's daily credit cap is reached. { "error": { "code": "daily_credit_cap_reached", "message": "This key's daily credit cap is reached.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "cap": "key", "resets_at": "2026-10-14T00:00:00.000Z" } }, "resets_at": "2026-10-14T00:00:00.000Z" }
- 402 insufficient_credits · request_id in the body · Retryable: no Not enough credits for this operation. { "error": { "code": "insufficient_credits", "message": "Not enough credits for this operation.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "required_credits": 25, "balance_credits": 3 } }, "required_credits": 25, "balance_credits": 3 }
- 409 approval_required · request_id in the body · Retryable: no This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it. { "error": { "code": "approval_required", "message": "This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "approval_threshold_credits": 500, "credits_max": 25 } }, "estimate": { "credits_min": 0, "credits_max": 25, "requires_confirm": false }, "confirm_token": "v1.payload.sig", "approval_threshold_credits": 500, "cost": { "credits": 0 } }
- 409 confirm_token_used · request_id in the body · Retryable: no This confirm_token was already used. Ask for a new preview to get another. { "error": { "code": "confirm_token_used", "message": "This confirm_token was already used. Ask for a new preview to get another.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_in_progress · request_id in the body · Retryable: yes A request with this Idempotency-Key is still running. Retry after it finishes. { "error": { "code": "idempotency_in_progress", "message": "A request with this Idempotency-Key is still running. Retry after it finishes.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_key_reused · request_id in the body · Retryable: no Idempotency-Key was already used with a different request. { "error": { "code": "idempotency_key_reused", "message": "Idempotency-Key was already used with a different request.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 429 upstream_busy · request_id in the body · Retryable: yes The profile source is busy. Retry after the delay. { "error": { "code": "upstream_busy", "message": "The profile source is busy. Retry after the delay.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_busy · request_id in the body · Retryable: yes The profile source is busy. Retry shortly. { "error": { "code": "upstream_busy", "message": "The profile source is busy. Retry shortly.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_error · request_id in the body · Retryable: yes The profile source returned a response that could not be used. Retry later. { "error": { "code": "upstream_error", "message": "The profile source returned a response that could not be used. Retry later.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "hint": "retry later" } } }
- 503 upstream_busy · request_id in the body · Retryable: yes The profile source is paused. { "error": { "code": "upstream_busy", "message": "The profile source is paused.", "request_id": "req_01J0REQUEST0EXAMPLE" }, "retry_at": "2026-10-13T15:00:00.000Z" }
- 504 gateway_timeout · request_id in the body · Retryable: yes The call did not finish in time. Nothing was charged. Retry the same request. { "error": { "code": "gateway_timeout", "message": "The call did not finish in time. Nothing was charged. Retry the same request.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body. Generate a fresh key with uuidgen, python3 -c 'import uuid; print(uuid.uuid4())', or crypto.randomUUID(). A 402 and a 429 are not stored, so the same key still works after you fix the cause. Wait for Retry-After on 429.

#### Side effects and approval

A dry run has no side effects. The live call charges for what comes back. [Execution, preview and approval](https://recruitmcp.com/docs#execution) says when the call runs and when it returns 409 approval_required.

#### Next step

- [Search people at the company URL or id](https://recruitmcp.com/docs/api#searchPeople)

### GET `/v1/lookups/industries`

#### Purpose

List industries

Industry ids. Use the ids in industry_ids and exclude_industry_ids. Free.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- q

  query · string · optional Only values whose label contains this text.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lookups/industries' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "4",
      "label": "Computer Software"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search with industry_ids](https://recruitmcp.com/docs/api#searchPeople)

### GET `/v1/lookups/seniorities`

#### Purpose

List seniority levels

Seniority ids. Use the ids in seniority_level_ids and exclude_seniority_level_ids. Free.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- q

  query · string · optional Only values whose label contains this text.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lookups/seniorities' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "120",
      "label": "Senior"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search with seniority_level_ids](https://recruitmcp.com/docs/api#searchPeople)

### GET `/v1/lookups/experience-ranges`

#### Purpose

List experience ranges

Year buckets for total experience and time at the current company. 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. Use the ids in years_of_experience_ids and years_at_current_company_ids. Free.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- q

  query · string · optional Only values whose label contains this text.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lookups/experience-ranges' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "4",
      "label": "6 to 10 years"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search with years_of_experience_ids](https://recruitmcp.com/docs/api#searchPeople)

### GET `/v1/lookups/functions`

#### Purpose

List job functions

Job function ids. 8 is Engineering. Use the ids in function_ids and exclude_function_ids. Free.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- q

  query · string · optional Only values whose label contains this text.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lookups/functions' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "8",
      "label": "Engineering"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search with function_ids](https://recruitmcp.com/docs/api#searchPeople)

### GET `/v1/lookups/languages`

#### Purpose

List profile languages

Languages a profile is written in. Use the ids in profile_languages. Free.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- q

  query · string · optional Only values whose label contains this text.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lookups/languages' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "nl",
      "label": "Dutch"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search with profile_languages](https://recruitmcp.com/docs/api#searchPeople)

### GET `/v1/lookups/headcounts`

#### Purpose

List company size ranges

Current employer headcount. A is self-employed, B is 1-10, C is 11-50, D is 51-200, E is 201-500, F is 501-1,000, G is 1,001-5,000, H is 5,001-10,000, I is 10,001+. Use the ids in company_headcount. Free.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- q

  query · string · optional Only values whose label contains this text.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lookups/headcounts' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "D",
      "label": "51-200"
    }
  ],
  "next_cursor": null,
  "has_more": false
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search with company_headcount](https://recruitmcp.com/docs/api#searchPeople)

## Profiles

Enrich a profile, read a cached profile, match a person, and read jobs.

### First request

Enrich reads the full profile. Email and phone are a separate contacts call. The first request previews an enrich of the two ids from the spec example.

```
POST /v1/people:enrich?dry_run=true

{
  "ids": [
    "lead_8f2a1c_1",
    "lead_3c71e0_2"
  ],
  "max_age_days": 30
}
```

### Filters

ids and profile_urls name who to enrich, up to 100 together. max_age_days is how fresh a cached copy must be to stay free. The default is 30, and the range is 1 to 365. Match takes a name and a company.

### Pagination

GET /v1/jobs lists enrich jobs. limit defaults to 25 and the maximum is 100. Pass next_cursor back as cursor. A cached enrich returns the profiles now. A live enrich returns a job id to poll.

```
GET /v1/jobs?limit=10
```

### Recipes

#### Match one person by name and company

first_name, last_name, and company. The preview spends nothing.

```
POST /v1/people:match?dry_run=true

{
  "first_name": "Ada",
  "last_name": "Peeters",
  "company": "Acme"
}
```

### Reference

### POST `/v1/people:enrich`

#### Purpose

Enrich profiles

Full public professional profiles for up to 100 profile ids or profile URLs. This does not return email or phone; a contact lookup does that. The body is the full profile, including education. MCP enrich_profiles defaults view to full and returns that same profile; compact is opt-in. Credits stay the profile price either way. profile_id is the search id or a public profile URL. alternate_id is the other profile id for the same person when both are known. Either id reads the same cached profile. Cached copies younger than max_age_days are free. A live enrich usually queues a job: the response is 202 with job_id, poll_url (/v1/jobs/{job_id}), and status queued, plus a Location header, and X-Credits-Charged is 0. Poll GET /v1/jobs/{job_id} every few seconds until status is completed, or wait for job.completed. The call that queued the job is charged when it completes; the completed job has result.data and credits_charged, read from the ledger and equal to result.cost.credits, and the ledger row carries this call's request_id and the job_id. When everything is already stored the profiles come back at once (200). A suppressed person is omitted and never charged. Polling example:

```
KEY="<your API key>"
curl -s -X POST https://api.recruitmcp.com/v1/people:enrich \
  -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{"ids":["lead_8f2a1c_1"]}'
# 202: {"job_id":"job_01J0JOB0EXAMPLE","poll_url":"/v1/jobs/job_01J0JOB0EXAMPLE","status":"queued",...}
until curl -s https://api.recruitmcp.com/v1/jobs/job_01J0JOB0EXAMPLE \
  -H "Authorization: Bearer $KEY" | grep -q '"status":"\(completed\|failed\)"'; do sleep 3; done
curl -s https://api.recruitmcp.com/v1/jobs/job_01J0JOB0EXAMPLE -H "Authorization: Bearer $KEY"
# result.data holds the profiles; credits_charged equals result.cost.credits
```

#### Scopes

sourcing:spend

#### Cost, free cases, and preview

Credit cost. 2 credits per live profile that is found. The profile does not include email or phone. Cached profiles, missing profiles, and suppressed people are free.

Preview mode. dry_run=true as a query parameter, or dry_run set to true in the JSON body, is a preview. Either one spends nothing and changes nothing. The response includes dry_run true, an estimate, and cost.credits 0. Idempotency-Key is not required while dry_run is true. A live call omits the query parameter and omits dry_run from the body. On REST, omitting dry_run is the confirmation. On MCP it is confirm=true. [Preview (dry_run)](https://recruitmcp.com/docs/api#preview) shows one request. [Execution, preview and approval](https://recruitmcp.com/docs#execution) is the rule for this call.

#### Parameters

- ids

  body · array of string · optional Profile ids or public profile URLs. Up to 100.

- profile_ids

  body · array of string · optional Alias of ids, the same name POST /v1/people/contacts:fetch uses. Merged with ids.

- profile_urls

  body · array of string · optional Public profile URLs. Up to 100 together with ids.

- max_age_days

  body · integer · optional Cached copies younger than this are free. Default 30.

- enrich_companies

  body · boolean · optional Add industry and size to each experience entry.

- confirm_token

  body · string · optional From approval_required. Required only when the estimate is above the key threshold.

#### Minimal request

Example request. Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/people:enrich?dry_run=true' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"ids":["lead_8f2a1c_1","lead_3c71e0_2"],"max_age_days":30}'
```

#### Success

HTTP 202. Live response: 202 with job_id and poll_url. Example response.

```
{
  "id": "job_01J0JOB0EXAMPLE",
  "object": "job",
  "kind": "people_fetch",
  "job_id": "job_01J0JOB0EXAMPLE",
  "status": "queued",
  "poll_url": "/v1/jobs/job_01J0JOB0EXAMPLE",
  "credits_charged": 0,
  "progress": {
    "done": 0,
    "total": 1
  },
  "error": null,
  "result": null,
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}
```

HTTP 202. Then poll GET /v1/jobs/{job_id}: completed, with credits_charged. Example response.

```
{
  "id": "job_01J0JOB0EXAMPLE",
  "object": "job",
  "kind": "people_fetch",
  "job_id": "job_01J0JOB0EXAMPLE",
  "status": "completed",
  "poll_url": "/v1/jobs/job_01J0JOB0EXAMPLE",
  "credits_charged": 2,
  "progress": {
    "done": 1,
    "total": 1
  },
  "error": null,
  "result": {
    "data": [
      {
        "profile_id": "lead_8f2a1c_1",
        "full_name": "Ada Peeters",
        "headline": "Backend Engineer at Acme Robotics",
        "profile_url": "https://profiles.example.com/ada-peeters",
        "experience": [
          {
            "position": "Backend Engineer",
            "company": "Acme Robotics",
            "started": "2021-03",
            "current": true,
            "company_url": "https://companies.example.com/acme-robotics"
          }
        ],
        "skills": [
          "Go",
          "Kubernetes"
        ],
        "source": "profile_index",
        "fetched_at": "2026-10-13T09:00:00Z"
      }
    ],
    "not_found": [],
    "cost": {
      "credits": 2,
      "cached_items": 0,
      "live_items": 1,
      "upstream_calls": 1
    }
  },
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:04Z"
}
```

HTTP 200. 200 when everything is already stored. Example response.

```
{
  "object": "list",
  "data": [
    {
      "profile_id": "lead_8f2a1c_1",
      "full_name": "Ada Peeters",
      "headline": "Backend Engineer at Acme Robotics",
      "profile_url": "https://profiles.example.com/ada-peeters",
      "experience": [
        {
          "position": "Backend Engineer",
          "company": "Acme Robotics",
          "started": "2021-03",
          "current": true,
          "company_url": "https://companies.example.com/acme-robotics"
        }
      ],
      "skills": [
        "Go",
        "Kubernetes"
      ],
      "source": "profile_index",
      "fetched_at": "2026-10-13T09:00:00Z"
    },
    {
      "profile_id": "lead_3c71e0_2",
      "full_name": "Jonas Vermeulen",
      "headline": "Platform Engineer at Paperkiln",
      "profile_url": "https://profiles.example.com/jonas-vermeulen",
      "experience": [
        {
          "position": "Backend Engineer",
          "company": "Acme Robotics",
          "started": "2021-03",
          "current": true,
          "company_url": "https://companies.example.com/acme-robotics"
        }
      ],
      "skills": [
        "Go",
        "Kubernetes"
      ],
      "source": "profile_index",
      "fetched_at": "2026-10-13T09:00:00Z"
    }
  ],
  "not_found": [],
  "omitted": {
    "suppressed": 0
  },
  "cost": {
    "credits": 4,
    "cached_items": 0,
    "live_items": 2,
    "upstream_calls": 2
  }
}
```

HTTP 200. With dry_run=true. Example response.

```
{
  "dry_run": true,
  "estimate": {
    "credits_min": 0,
    "credits_max": 4,
    "balance_credits": 1250,
    "sufficient": true,
    "warning": null,
    "requires_confirm": false
  },
  "cost": {
    "credits": 0,
    "cached_items": 0,
    "live_items": 0,
    "upstream_calls": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 idempotency_key_required · request_id in the body · Retryable: no This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview. { "error": { "code": "idempotency_key_required", "message": "This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 402 insufficient_credits · request_id in the body · Retryable: no Not enough credits for this operation. { "error": { "code": "insufficient_credits", "message": "Not enough credits for this operation.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "required_credits": 25, "balance_credits": 3 } }, "required_credits": 25, "balance_credits": 3 }
- 409 approval_required · request_id in the body · Retryable: no This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it. { "error": { "code": "approval_required", "message": "This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "approval_threshold_credits": 500, "credits_max": 25 } }, "estimate": { "credits_min": 0, "credits_max": 25, "requires_confirm": false }, "confirm_token": "v1.payload.sig", "approval_threshold_credits": 500, "cost": { "credits": 0 } }
- 409 confirm_token_used · request_id in the body · Retryable: no This confirm_token was already used. Ask for a new preview to get another. { "error": { "code": "confirm_token_used", "message": "This confirm_token was already used. Ask for a new preview to get another.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_in_progress · request_id in the body · Retryable: yes A request with this Idempotency-Key is still running. Retry after it finishes. { "error": { "code": "idempotency_in_progress", "message": "A request with this Idempotency-Key is still running. Retry after it finishes.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_key_reused · request_id in the body · Retryable: no Idempotency-Key was already used with a different request. { "error": { "code": "idempotency_key_reused", "message": "Idempotency-Key was already used with a different request.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body. Generate a fresh key with uuidgen, python3 -c 'import uuid; print(uuid.uuid4())', or crypto.randomUUID(). A 402 and a 429 are not stored, so the same key still works after you fix the cause. Wait for Retry-After on 429.

#### Side effects and approval

A dry run has no side effects. The live call charges for what comes back. [Execution, preview and approval](https://recruitmcp.com/docs#execution) says when the call runs and when it returns 409 approval_required.

#### Next step

- [Poll the job when the response is 202](https://recruitmcp.com/docs/api#getJob)
- [Look up a contact](https://recruitmcp.com/docs/api#lookupContacts)

### GET `/v1/people/{profile_id}`

#### Purpose

Get a person from your cache

Never charges and never calls the person index. Returns 404 not_cached when the person has not been enriched in this workspace within max_age_days; enrich them with POST /v1/people:enrich. profile_id may also be a URL-encoded profile URL.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- profile_id

  path · string · required

- max_age_days

  query · integer · optional Default 30.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/people/lead_8f2a1c_1' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "profile_id": "lead_8f2a1c_1",
  "full_name": "Ada Peeters",
  "headline": "Backend Engineer at Acme Robotics",
  "profile_url": "https://profiles.example.com/ada-peeters",
  "experience": [
    {
      "position": "Backend Engineer",
      "company": "Acme Robotics",
      "company_url": "https://companies.example.com/acme-robotics",
      "started": "2021-03",
      "current": true
    }
  ],
  "education": [
    {
      "university": "University of Amsterdam",
      "url": "https://example.com/school/university-of-amsterdam",
      "degree": [
        "MSc",
        "Computer Science"
      ],
      "faculty": "Computer Science",
      "started_year": 2013,
      "ended_year": 2019
    }
  ],
  "skills": [
    "Go",
    "Kubernetes"
  ],
  "source": "profile_index",
  "fetched_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_cached · request_id in the body · Retryable: no This person is not in your cache. Enrich them with POST /v1/people:enrich. { "error": { "code": "not_cached", "message": "This person is not in your cache. Enrich them with POST /v1/people:enrich.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 suppressed · request_id in the body · Retryable: no This person is suppressed in this workspace. { "error": { "code": "suppressed", "message": "This person is suppressed in this workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Look up a contact for this profile](https://recruitmcp.com/docs/api#lookupPersonContacts)

### POST `/v1/people:match`

#### Purpose

Find one person by name and current company

Looks up one person by name and current company. Charged one lead credit only when exactly one person matches (outcome exact), and only the first time that person is returned. Ambiguous and no-match outcomes are free.

#### Scopes

sourcing:spend

#### Cost, free cases, and preview

Credit cost. 1 credit when exactly one person matches, the first time only. Ambiguous and no-match results are free.

Preview mode. dry_run=true as a query parameter, or dry_run set to true in the JSON body, is a preview. Either one spends nothing and changes nothing. The response includes dry_run true, an estimate, and cost.credits 0. Idempotency-Key is not required while dry_run is true. A live call omits the query parameter and omits dry_run from the body. On REST, omitting dry_run is the confirmation. On MCP it is confirm=true. [Preview (dry_run)](https://recruitmcp.com/docs/api#preview) shows one request. [Execution, preview and approval](https://recruitmcp.com/docs#execution) is the rule for this call.

#### Parameters

- first_name

  body · string · optional

- last_name

  body · string · optional

- full_name

  body · string · optional Instead of first_name and last_name.

- company

  body · string · required Current employer name or company URL.

- confirm_token

  body · string · optional From approval_required. Required only when the estimate is above the key threshold.

#### Minimal request

Example request. Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/people:match?dry_run=true' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"first_name":"Ada","last_name":"Peeters","company":"Acme"}'
```

#### Success

HTTP 200. Live response. Example response.

```
{
  "object": "person_match",
  "outcome": "exact",
  "match": {
    "profile_id": "lead_8f2a1c_1",
    "full_name": "Ada Peeters",
    "location": "London, England, United Kingdom",
    "alternate_id": null,
    "photo": null,
    "profile_url": "https://profiles.example.com/ada-peeters",
    "current_positions": [
      {
        "title": "Backend Engineer",
        "company_name": "Acme Robotics",
        "company_id": null,
        "company_url": "https://companies.example.com/acme-robotics",
        "current": true,
        "description": "Builds the fleet API in Go.",
        "started_on": {
          "year": 2021,
          "month": 3,
          "text": "2021-03"
        },
        "tenure_at_position": {
          "numYears": 5,
          "numMonths": 7
        },
        "tenure_at_company": {
          "numYears": 6
        }
      }
    ],
    "identity_key": "ada peeters|acme robotics",
    "already_seen": false
  },
  "cost": {
    "credits": 1,
    "cached_items": 0,
    "live_items": 1,
    "upstream_calls": 1
  }
}
```

HTTP 200. With dry_run=true. Example response.

```
{
  "dry_run": true,
  "estimate": {
    "credits_min": 0,
    "credits_max": 1,
    "balance_credits": 1250,
    "sufficient": true,
    "warning": null,
    "requires_confirm": false
  },
  "cost": {
    "credits": 0,
    "cached_items": 0,
    "live_items": 0,
    "upstream_calls": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 idempotency_key_required · request_id in the body · Retryable: no This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview. { "error": { "code": "idempotency_key_required", "message": "This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 402 daily_credit_cap_reached · request_id in the body · Retryable: no This key's daily credit cap is reached. { "error": { "code": "daily_credit_cap_reached", "message": "This key's daily credit cap is reached.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "cap": "key", "resets_at": "2026-10-14T00:00:00.000Z" } }, "resets_at": "2026-10-14T00:00:00.000Z" }
- 402 free_tier_limit · request_id in the body · Retryable: no This workspace has used its 60 free search pages. A paid plan removes the cap. { "error": { "code": "free_tier_limit", "message": "This workspace has used its 60 free search pages. A paid plan removes the cap.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "kind": "search_pages", "limit": 60 } } }
- 402 insufficient_credits · request_id in the body · Retryable: no Not enough credits for this operation. { "error": { "code": "insufficient_credits", "message": "Not enough credits for this operation.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "required_credits": 25, "balance_credits": 3 } }, "required_credits": 25, "balance_credits": 3 }
- 409 approval_required · request_id in the body · Retryable: no This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it. { "error": { "code": "approval_required", "message": "This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "approval_threshold_credits": 500, "credits_max": 25 } }, "estimate": { "credits_min": 0, "credits_max": 25, "requires_confirm": false }, "confirm_token": "v1.payload.sig", "approval_threshold_credits": 500, "cost": { "credits": 0 } }
- 409 confirm_token_used · request_id in the body · Retryable: no This confirm_token was already used. Ask for a new preview to get another. { "error": { "code": "confirm_token_used", "message": "This confirm_token was already used. Ask for a new preview to get another.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_in_progress · request_id in the body · Retryable: yes A request with this Idempotency-Key is still running. Retry after it finishes. { "error": { "code": "idempotency_in_progress", "message": "A request with this Idempotency-Key is still running. Retry after it finishes.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_key_reused · request_id in the body · Retryable: no Idempotency-Key was already used with a different request. { "error": { "code": "idempotency_key_reused", "message": "Idempotency-Key was already used with a different request.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 429 upstream_busy · request_id in the body · Retryable: yes The profile source is busy. Retry after the delay. { "error": { "code": "upstream_busy", "message": "The profile source is busy. Retry after the delay.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_busy · request_id in the body · Retryable: yes The profile source is busy. Retry shortly. { "error": { "code": "upstream_busy", "message": "The profile source is busy. Retry shortly.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_error · request_id in the body · Retryable: yes The profile source returned a response that could not be used. Retry later. { "error": { "code": "upstream_error", "message": "The profile source returned a response that could not be used. Retry later.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "hint": "retry later" } } }
- 503 upstream_busy · request_id in the body · Retryable: yes The profile source is paused. { "error": { "code": "upstream_busy", "message": "The profile source is paused.", "request_id": "req_01J0REQUEST0EXAMPLE" }, "retry_at": "2026-10-13T15:00:00.000Z" }

#### Idempotency and retry

Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body. Generate a fresh key with uuidgen, python3 -c 'import uuid; print(uuid.uuid4())', or crypto.randomUUID(). A 402 and a 429 are not stored, so the same key still works after you fix the cause. Wait for Retry-After on 429.

#### Side effects and approval

A dry run has no side effects. The live call charges for what comes back. [Execution, preview and approval](https://recruitmcp.com/docs#execution) says when the call runs and when it returns 409 approval_required.

#### Next step

- [Enrich the matched profile](https://recruitmcp.com/docs/api#enrichPeople)

### GET `/v1/jobs`

#### Purpose

List profile fetch jobs

Scope: \`sourcing:read\`.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- status

  query · "queued" | "running" | "parked" | "completed" | "failed" · optional Only jobs in this status.

- created_after

  query · string (date-time) · optional Only rows created after this time.

- created_before

  query · string (date-time) · optional Only rows created before this time.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/jobs' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "job_01J0JOB0EXAMPLE",
      "object": "job",
      "kind": "people_fetch",
      "job_id": "job_01J0JOB0EXAMPLE",
      "status": "completed",
      "poll_url": "/v1/jobs/job_01J0JOB0EXAMPLE",
      "credits_charged": 2,
      "progress": {
        "done": 1,
        "total": 1
      },
      "error": null,
      "result": {
        "data": [
          {
            "profile_id": "lead_8f2a1c_1",
            "full_name": "Ada Peeters",
            "headline": "Backend Engineer at Acme Robotics",
            "profile_url": "https://profiles.example.com/ada-peeters",
            "experience": [
              {
                "position": "Backend Engineer",
                "company": "Acme Robotics",
                "started": "2021-03",
                "current": true,
                "company_url": "https://companies.example.com/acme-robotics"
              }
            ],
            "skills": [
              "Go",
              "Kubernetes"
            ],
            "source": "profile_index",
            "fetched_at": "2026-10-13T09:00:00Z"
          }
        ],
        "not_found": [],
        "cost": {
          "credits": 2,
          "cached_items": 0,
          "live_items": 1,
          "upstream_calls": 1
        }
      },
      "created_at": "2026-10-13T09:00:00Z",
      "updated_at": "2026-10-13T09:00:04Z"
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read one job](https://recruitmcp.com/docs/api#getJob)

### GET `/v1/jobs/{job_id}`

#### Purpose

Get a job

Free. Poll the job_id from POST /v1/people:enrich that returned HTTP 202, every few seconds, until status is completed or failed. When the job is completed, result.data holds the profiles and credits_charged is what the ledger charged for the job, the same number as result.cost.credits. It is 0 until then. The call that queued the job is charged when it completes; this poll never charges. view defaults to full, the same profile enrich returns, including education. view=compact is the short profile. Polling example:

```
KEY="<your API key>"
curl -s -X POST https://api.recruitmcp.com/v1/people:enrich \
  -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{"ids":["lead_8f2a1c_1"]}'
# 202: {"job_id":"job_01J0JOB0EXAMPLE","poll_url":"/v1/jobs/job_01J0JOB0EXAMPLE","status":"queued",...}
until curl -s https://api.recruitmcp.com/v1/jobs/job_01J0JOB0EXAMPLE \
  -H "Authorization: Bearer $KEY" | grep -q '"status":"\(completed\|failed\)"'; do sleep 3; done
curl -s https://api.recruitmcp.com/v1/jobs/job_01J0JOB0EXAMPLE -H "Authorization: Bearer $KEY"
# result.data holds the profiles; credits_charged equals result.cost.credits
```

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- job_id

  path · string · required

- view

  query · "full" | "compact" · optional full is the default and includes education and experience. compact is the short profile.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/jobs/job_01J0JOB0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Completed. Example response.

```
{
  "id": "job_01J0JOB0EXAMPLE",
  "object": "job",
  "kind": "people_fetch",
  "job_id": "job_01J0JOB0EXAMPLE",
  "status": "completed",
  "poll_url": "/v1/jobs/job_01J0JOB0EXAMPLE",
  "credits_charged": 2,
  "progress": {
    "done": 1,
    "total": 1
  },
  "error": null,
  "result": {
    "data": [
      {
        "profile_id": "lead_8f2a1c_1",
        "full_name": "Ada Peeters",
        "headline": "Backend Engineer at Acme Robotics",
        "profile_url": "https://profiles.example.com/ada-peeters",
        "experience": [
          {
            "position": "Backend Engineer",
            "company": "Acme Robotics",
            "started": "2021-03",
            "current": true,
            "company_url": "https://companies.example.com/acme-robotics"
          }
        ],
        "skills": [
          "Go",
          "Kubernetes"
        ],
        "source": "profile_index",
        "fetched_at": "2026-10-13T09:00:00Z"
      }
    ],
    "not_found": [],
    "cost": {
      "credits": 2,
      "cached_items": 0,
      "live_items": 1,
      "upstream_calls": 1
    }
  },
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:04Z"
}
```

HTTP 200. Queued. Example response.

```
{
  "id": "job_01J0JOB0EXAMPLE",
  "object": "job",
  "kind": "people_fetch",
  "job_id": "job_01J0JOB0EXAMPLE",
  "status": "queued",
  "poll_url": "/v1/jobs/job_01J0JOB0EXAMPLE",
  "credits_charged": 0,
  "progress": {
    "done": 0,
    "total": 1
  },
  "error": null,
  "result": null,
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Look up a contact once the profile is stored](https://recruitmcp.com/docs/api#lookupContacts)

### GET `/v1/media/{token}`

#### Purpose

Get a profile photo

Public. The token in a profile photo field expires after 7 days, and each API response signs a new one. It is not tied to a workspace and does not contain the original address. Requests are limited per network. The image is cached on this server and the response is private for one day. Erasure and a confirmed opt-out delete that person's cached photo; every token for them then returns 404. No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- token

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/media/tok_01J0VERIFY0TOKEN0EXAMPLE'
```

#### Success

HTTP 200. Example response.

```
HTTP 200
Content-Type: image/jpeg
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no Photo not found. { "error": { "code": "not_found", "message": "Photo not found.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 502 upstream_error · request_id in the body · Retryable: yes The photo could not be loaded. { "error": { "code": "upstream_error", "message": "The photo could not be loaded.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

No next step on this call.

## Contacts

Look up email and phone, then read what is already stored.

### First request

A contact lookup reads email and phone for a person you have already fetched. The reserved id test_miss_0000 is the simplest preview: it is a miss, it spends nothing, and it does not need a fetched profile. prefer is personal.

```
POST /v1/people/test_miss_0000/contacts?dry_run=true

{
  "prefer": "personal"
}
```

### Filters

prefer is personal or work. It defaults to personal. The batch call takes profile_ids, up to 100.

### Pagination

One lookup is one person. The batch call takes up to 100 profile ids in one request. A running lookup returns job_id and poll_url. Poll GET /v1/contacts/jobs/{job_id}. There is no cursor on the contact body.

### Recipes

#### Preview a batch miss

The same reserved id, on the batch route.

```
POST /v1/people/contacts:fetch?dry_run=true

{
  "profile_ids": [
    "test_miss_0000"
  ],
  "prefer": "personal"
}
```

### Reference

### POST `/v1/people/contacts:fetch`

#### Purpose

Look up email and phone for many people

Email and phone, with a confidence score, for up to 100 fetched people. Each email includes type work or personal when the lookup provides it. Each result includes best_email and best_phone. best_email prefers personal, then an unlabeled address, then work. prefer is personal or work and defaults to personal. best_phone prefers a mobile number. The emails and phones lists stay complete. A flagged entry stays in the list with flagged true and flag_reason, and is left out of best_email and best_phone. Same pricing and the same { data, cost } body as the single lookup, including a repeat of a stored lookup. A live lookup usually answers 202 with status pending, job_id, and poll_url, and charges nothing yet: cost.credits, charged_this_request, and credits_charged are 0 and X-Credits-Charged is 0. Poll GET /v1/contacts/jobs/{job_id} every few seconds until status is completed, or wait for contacts.completed. The lookup that started the job is charged when it completes, only for people with an email or phone found. The completed poll shows that charge as credits_charged and charged_this_request, the same number as cost.credits and the ledger row, whose request_id is the X-Request-Id of the call that started the lookup. When every person is already stored the lookup answers 200 at once. A 202 also has canonical_id on each person. Replaying the Idempotency-Key after completion returns the completed result and does not charge again. That replay has replayed: true, cost.credits 0 and charged_this_request 0, like every replay. Each person's credits still show what that lookup charged. dry_run in the query or the JSON body previews and never charges, and accepts contacts:spend or sourcing:read. A malformed person id is 400 invalid_id. The reserved id test_miss_0000 always returns not_found, charges 0, and does not call the contact provider. It does not need a fetched profile, and it applies only to contact lookup. Polling example:

```
KEY="<your API key>"
curl -s -X POST https://api.recruitmcp.com/v1/people/contacts:fetch \
  -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{"profile_ids":["lead_8f2a1c_1"]}'
# 202: {"status":"pending","job_id":"cjob_01J0CJOB0EXAMPLE","poll_url":"/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE",...}
# Poll until status is completed (pending until then). The wait doubles from 2 s up to 30 s; stop on an error, give up after about 2 minutes.
delay=2
for i in 1 2 3 4 5 6 7 8; do
  body=$(curl -sS https://api.recruitmcp.com/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE -H "Authorization: Bearer $KEY")
  echo "$body" | grep -q '"status":"completed"' && break
  echo "$body" | grep -q '"error":{' && break
  sleep $delay
  delay=$((delay*2>30?30:delay*2))
done
echo "$body"
# data[].contacts.best_email and best_phone; credits_charged and charged_this_request equal cost.credits
```

#### Scopes

contacts:spend

#### Cost, free cases, and preview

Credit cost. A found contact costs 25 credits. A miss is free. dry_run=true returns the estimate and charges nothing.

Preview mode. dry_run=true as a query parameter, or dry_run set to true in the JSON body, is a preview. Either one spends nothing and changes nothing. The response includes dry_run true, an estimate, and cost.credits 0. Idempotency-Key is not required while dry_run is true. A live call omits the query parameter and omits dry_run from the body. On REST, omitting dry_run is the confirmation. On MCP it is confirm=true. [Preview (dry_run)](https://recruitmcp.com/docs/api#preview) shows one request. [Execution, preview and approval](https://recruitmcp.com/docs#execution) is the rule for this call.

#### Parameters

- profile_ids

  body · array of string · optional Profile ids already fetched. Up to 100. Send this or ids.

- ids

  body · array of string · optional Alias of profile_ids, the same name POST /v1/people:enrich uses. Merged with profile_ids.

- prefer

  body · "personal" | "work" · optional Email to prefer. personal, the default, ranks personal, then unlabeled, then work. work reverses that order. Phones still prefer mobile.

- dry_run

  body · boolean · optional When true, preview the cost and change nothing. Nothing is charged. Same as the dry_run query.

- confirm_token

  body · string · optional From approval_required. Required only when the estimate is above the key threshold.

#### Minimal request

Example request. Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/people/contacts:fetch?dry_run=true' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"profile_ids":["lead_8f2a1c_1"],"prefer":"personal"}'
```

#### Success

HTTP 202. Live response: 202 with job_id and poll_url. Example response.

```
{
  "status": "pending",
  "request_id": "req_4f1c2a9b7e3d5a60",
  "job_id": "cjob_01J0CJOB0EXAMPLE",
  "poll_url": "/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE",
  "canonical_id": "lead_8f2a1c_1",
  "data": [
    {
      "profile_id": "lead_8f2a1c_1",
      "canonical_id": "lead_8f2a1c_1",
      "contacts": {
        "status": "pending",
        "emails": [],
        "phones": [],
        "best_email": null,
        "best_phone": null,
        "expires_at": null
      },
      "credits": 0
    }
  ],
  "cost": {
    "credits": 0
  },
  "charged_this_request": 0,
  "credits_charged": 0
}
```

HTTP 202. Then poll GET /v1/contacts/jobs/{job_id}: completed, with credits_charged. Example response.

```
{
  "status": "completed",
  "request_id": "req_4f1c2a9b7e3d5a60",
  "job_id": "cjob_01J0CJOB0EXAMPLE",
  "poll_url": "/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE",
  "canonical_id": "lead_8f2a1c_1",
  "data": [
    {
      "profile_id": "lead_8f2a1c_1",
      "canonical_id": "lead_8f2a1c_1",
      "contacts": {
        "status": "ready",
        "emails": [
          {
            "value": "ada.peeters@example.com",
            "type": "personal",
            "line": "personal",
            "confidence": 0.9
          }
        ],
        "phones": [
          {
            "value": "+447700900001",
            "type": "personal",
            "line": "mobile",
            "confidence": 0.8
          }
        ],
        "best_email": {
          "value": "ada.peeters@example.com",
          "type": "personal",
          "line": "personal",
          "confidence": 0.9,
          "reason": "only_candidate"
        },
        "best_phone": {
          "value": "+447700900001",
          "type": "personal",
          "line": "mobile",
          "confidence": 0.8,
          "reason": "only_candidate"
        },
        "expires_at": "2026-11-12T09:00:00Z"
      },
      "credits": 25
    }
  ],
  "cost": {
    "credits": 25
  },
  "charged_this_request": 25,
  "credits_charged": 25
}
```

HTTP 200. 200 when everything is already stored. Example response.

```
{
  "data": [
    {
      "profile_id": "lead_8f2a1c_1",
      "contacts": {
        "status": "ready",
        "emails": [
          {
            "value": "ada.peeters@example.com",
            "type": "personal",
            "line": "personal",
            "confidence": 0.9
          }
        ],
        "phones": [
          {
            "value": "+447700900001",
            "type": "personal",
            "line": "mobile",
            "confidence": 0.8
          }
        ],
        "best_email": {
          "value": "ada.peeters@example.com",
          "type": "personal",
          "line": "personal",
          "confidence": 0.9,
          "reason": "only_candidate"
        },
        "best_phone": {
          "value": "+447700900001",
          "type": "personal",
          "line": "mobile",
          "confidence": 0.8,
          "reason": "only_candidate"
        },
        "expires_at": "2026-11-12T09:00:00Z"
      },
      "credits": 25,
      "cost": {
        "credits": 25,
        "cached_items": 0,
        "live_items": 1,
        "upstream_calls": 1
      }
    }
  ],
  "cost": {
    "credits": 25,
    "cached_items": 0,
    "live_items": 1,
    "upstream_calls": 1
  },
  "charged_this_request": 25
}
```

HTTP 200. With dry_run=true. Example response.

```
{
  "dry_run": true,
  "estimate": {
    "credits_min": 0,
    "credits_max": 25,
    "balance_credits": 1250,
    "sufficient": true,
    "warning": null,
    "requires_confirm": false
  },
  "cost": {
    "credits": 0,
    "cached_items": 0,
    "live_items": 0,
    "upstream_calls": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 idempotency_key_required · request_id in the body · Retryable: no This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview. { "error": { "code": "idempotency_key_required", "message": "This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 400 invalid_id · request_id in the body · Retryable: no profile_id is not a valid person id. { "error": { "code": "invalid_id", "message": "profile_id is not a valid person id.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "field": "profile_id" } } }
- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 402 daily_credit_cap_reached · request_id in the body · Retryable: no This key's daily credit cap is reached. { "error": { "code": "daily_credit_cap_reached", "message": "This key's daily credit cap is reached.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "cap": "key", "resets_at": "2026-10-14T00:00:00.000Z" } }, "resets_at": "2026-10-14T00:00:00.000Z" }
- 402 insufficient_credits · request_id in the body · Retryable: no Not enough credits for this operation. { "error": { "code": "insufficient_credits", "message": "Not enough credits for this operation.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "required_credits": 25, "balance_credits": 3 } }, "required_credits": 25, "balance_credits": 3 }
- 404 not_found · request_id in the body · Retryable: no Fetch the profile before asking for contacts. { "error": { "code": "not_found", "message": "Fetch the profile before asking for contacts.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 suppressed · request_id in the body · Retryable: no This profile is suppressed. Contact lookup is free. { "error": { "code": "suppressed", "message": "This profile is suppressed. Contact lookup is free.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 approval_required · request_id in the body · Retryable: no This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it. { "error": { "code": "approval_required", "message": "This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "approval_threshold_credits": 500, "credits_max": 25 } }, "estimate": { "credits_min": 0, "credits_max": 25, "requires_confirm": false }, "confirm_token": "v1.payload.sig", "approval_threshold_credits": 500, "cost": { "credits": 0 } }
- 409 confirm_token_used · request_id in the body · Retryable: no This confirm_token was already used. Ask for a new preview to get another. { "error": { "code": "confirm_token_used", "message": "This confirm_token was already used. Ask for a new preview to get another.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_in_progress · request_id in the body · Retryable: yes A request with this Idempotency-Key is still running. Retry after it finishes. { "error": { "code": "idempotency_in_progress", "message": "A request with this Idempotency-Key is still running. Retry after it finishes.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_key_reused · request_id in the body · Retryable: no Idempotency-Key was already used with a different request. { "error": { "code": "idempotency_key_reused", "message": "Idempotency-Key was already used with a different request.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 503 contacts_unavailable · request_id in the body · Retryable: no Contact lookup is not available on this server. Nothing was looked up and nothing was charged. { "error": { "code": "contacts_unavailable", "message": "Contact lookup is not available on this server. Nothing was looked up and nothing was charged.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 503 upstream_busy · request_id in the body · Retryable: yes Contact lookups are not configured on this server. { "error": { "code": "upstream_busy", "message": "Contact lookups are not configured on this server.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body. Generate a fresh key with uuidgen, python3 -c 'import uuid; print(uuid.uuid4())', or crypto.randomUUID(). A 402 and a 429 are not stored, so the same key still works after you fix the cause. Wait for Retry-After on 429.

#### Side effects and approval

A dry run has no side effects. The live call charges for what comes back. [Execution, preview and approval](https://recruitmcp.com/docs#execution) says when the call runs and when it returns 409 approval_required.

#### Next step

- [Poll a running lookup](https://recruitmcp.com/docs/api#getContactJob)
- [Read the stored contact later](https://recruitmcp.com/docs/api#getPersonContacts)
- [Save the person to a list](https://recruitmcp.com/docs/api#addListItems)

### POST `/v1/people/{profile_id}/contacts`

#### Purpose

Look up email and phone

Email and phone, with a confidence score. Each email includes type work or personal when the lookup provides it. Each result includes best_email and best_phone. best_email prefers personal, then an unlabeled address, then work. prefer is personal or work and defaults to personal. best_phone prefers a mobile number. The emails and phones lists stay complete. A flagged entry stays in the list with flagged true and flag_reason, and is left out of best_email and best_phone. Charged only when an email or phone is found; a miss is free. The person must be fetched first. The body is always { data, cost }, including one person and a repeat of a stored lookup. A live lookup usually answers 202 with status pending, job_id, and poll_url, and charges nothing yet: cost.credits, charged_this_request, and credits_charged are 0 and X-Credits-Charged is 0. Poll GET /v1/contacts/jobs/{job_id} every few seconds until status is completed, or wait for contacts.completed. The lookup that started the job is charged when it completes, only for people with an email or phone found. The completed poll shows that charge as credits_charged and charged_this_request, the same number as cost.credits and the ledger row, whose request_id is the X-Request-Id of the call that started the lookup. When every person is already stored the lookup answers 200 at once. A 202 also has canonical_id. Replaying the Idempotency-Key after completion returns the completed result and does not charge again. That replay has replayed: true, cost.credits 0 and charged_this_request 0, like every replay. Each person's credits still show what that lookup charged. dry_run in the query or the JSON body previews and never charges, and accepts contacts:spend or sourcing:read. A malformed person id is 400 invalid_id. The reserved id test_miss_0000 always returns not_found, charges 0, and does not call the contact provider. It does not need a fetched profile, and it applies only to contact lookup. Polling example:

```
KEY="<your API key>"
curl -s -X POST https://api.recruitmcp.com/v1/people/lead_8f2a1c_1/contacts \
  -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{}'
# 202: {"status":"pending","job_id":"cjob_01J0CJOB0EXAMPLE","poll_url":"/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE",...}
# Poll until status is completed (pending until then). The wait doubles from 2 s up to 30 s; stop on an error, give up after about 2 minutes.
delay=2
for i in 1 2 3 4 5 6 7 8; do
  body=$(curl -sS https://api.recruitmcp.com/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE -H "Authorization: Bearer $KEY")
  echo "$body" | grep -q '"status":"completed"' && break
  echo "$body" | grep -q '"error":{' && break
  sleep $delay
  delay=$((delay*2>30?30:delay*2))
done
echo "$body"
# data[].contacts.best_email and best_phone; credits_charged and charged_this_request equal cost.credits
```

#### Scopes

contacts:spend

#### Cost, free cases, and preview

Credit cost. A found contact costs 25 credits. A miss is free. dry_run=true returns the estimate and charges nothing.

Preview mode. dry_run=true as a query parameter, or dry_run set to true in the JSON body, is a preview. Either one spends nothing and changes nothing. The response includes dry_run true, an estimate, and cost.credits 0. Idempotency-Key is not required while dry_run is true. A live call omits the query parameter and omits dry_run from the body. On REST, omitting dry_run is the confirmation. On MCP it is confirm=true. [Preview (dry_run)](https://recruitmcp.com/docs/api#preview) shows one request. [Execution, preview and approval](https://recruitmcp.com/docs#execution) is the rule for this call.

#### Parameters

- profile_id

  path · string · required

- prefer

  body · "personal" | "work" · optional Email to prefer. personal, the default, ranks personal, then unlabeled, then work. work reverses that order. Phones still prefer mobile.

- dry_run

  body · boolean · optional When true, preview the cost and change nothing. Nothing is charged. Same as the dry_run query.

- confirm_token

  body · string · optional From approval_required. Required only when the estimate is above the key threshold.

#### Minimal request

Example request. Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/people/lead_8f2a1c_1/contacts?dry_run=true' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"prefer":"personal"}'
```

#### Success

HTTP 202. Live response: 202 with job_id and poll_url. Example response.

```
{
  "status": "pending",
  "request_id": "req_4f1c2a9b7e3d5a60",
  "job_id": "cjob_01J0CJOB0EXAMPLE",
  "poll_url": "/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE",
  "canonical_id": "lead_8f2a1c_1",
  "data": [
    {
      "profile_id": "lead_8f2a1c_1",
      "canonical_id": "lead_8f2a1c_1",
      "contacts": {
        "status": "pending",
        "emails": [],
        "phones": [],
        "best_email": null,
        "best_phone": null,
        "expires_at": null
      },
      "credits": 0
    }
  ],
  "cost": {
    "credits": 0
  },
  "charged_this_request": 0,
  "credits_charged": 0
}
```

HTTP 202. Then poll GET /v1/contacts/jobs/{job_id}: completed, with credits_charged. Example response.

```
{
  "status": "completed",
  "request_id": "req_4f1c2a9b7e3d5a60",
  "job_id": "cjob_01J0CJOB0EXAMPLE",
  "poll_url": "/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE",
  "canonical_id": "lead_8f2a1c_1",
  "data": [
    {
      "profile_id": "lead_8f2a1c_1",
      "canonical_id": "lead_8f2a1c_1",
      "contacts": {
        "status": "ready",
        "emails": [
          {
            "value": "ada.peeters@example.com",
            "type": "personal",
            "line": "personal",
            "confidence": 0.9
          }
        ],
        "phones": [
          {
            "value": "+447700900001",
            "type": "personal",
            "line": "mobile",
            "confidence": 0.8
          }
        ],
        "best_email": {
          "value": "ada.peeters@example.com",
          "type": "personal",
          "line": "personal",
          "confidence": 0.9,
          "reason": "only_candidate"
        },
        "best_phone": {
          "value": "+447700900001",
          "type": "personal",
          "line": "mobile",
          "confidence": 0.8,
          "reason": "only_candidate"
        },
        "expires_at": "2026-11-12T09:00:00Z"
      },
      "credits": 25
    }
  ],
  "cost": {
    "credits": 25
  },
  "charged_this_request": 25,
  "credits_charged": 25
}
```

HTTP 200. 200 when everything is already stored. Example response.

```
{
  "data": [
    {
      "profile_id": "lead_8f2a1c_1",
      "contacts": {
        "status": "ready",
        "emails": [
          {
            "value": "ada.peeters@example.com",
            "type": "personal",
            "line": "personal",
            "confidence": 0.9
          }
        ],
        "phones": [
          {
            "value": "+447700900001",
            "type": "personal",
            "line": "mobile",
            "confidence": 0.8
          }
        ],
        "best_email": {
          "value": "ada.peeters@example.com",
          "type": "personal",
          "line": "personal",
          "confidence": 0.9,
          "reason": "only_candidate"
        },
        "best_phone": {
          "value": "+447700900001",
          "type": "personal",
          "line": "mobile",
          "confidence": 0.8,
          "reason": "only_candidate"
        },
        "expires_at": "2026-11-12T09:00:00Z"
      },
      "credits": 25,
      "cost": {
        "credits": 25,
        "cached_items": 0,
        "live_items": 1,
        "upstream_calls": 1
      }
    }
  ],
  "cost": {
    "credits": 25,
    "cached_items": 0,
    "live_items": 1,
    "upstream_calls": 1
  },
  "charged_this_request": 25
}
```

HTTP 200. With dry_run=true. Example response.

```
{
  "dry_run": true,
  "estimate": {
    "credits_min": 0,
    "credits_max": 25,
    "balance_credits": 1250,
    "sufficient": true,
    "warning": null,
    "requires_confirm": false
  },
  "cost": {
    "credits": 0,
    "cached_items": 0,
    "live_items": 0,
    "upstream_calls": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 idempotency_key_required · request_id in the body · Retryable: no This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview. { "error": { "code": "idempotency_key_required", "message": "This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 400 invalid_id · request_id in the body · Retryable: no profile_id is not a valid person id. { "error": { "code": "invalid_id", "message": "profile_id is not a valid person id.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "field": "profile_id" } } }
- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 402 daily_credit_cap_reached · request_id in the body · Retryable: no This key's daily credit cap is reached. { "error": { "code": "daily_credit_cap_reached", "message": "This key's daily credit cap is reached.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "cap": "key", "resets_at": "2026-10-14T00:00:00.000Z" } }, "resets_at": "2026-10-14T00:00:00.000Z" }
- 402 insufficient_credits · request_id in the body · Retryable: no Not enough credits for this operation. { "error": { "code": "insufficient_credits", "message": "Not enough credits for this operation.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "required_credits": 25, "balance_credits": 3 } }, "required_credits": 25, "balance_credits": 3 }
- 404 not_found · request_id in the body · Retryable: no Fetch the profile before asking for contacts. { "error": { "code": "not_found", "message": "Fetch the profile before asking for contacts.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 suppressed · request_id in the body · Retryable: no This profile is suppressed. Contact lookup is free. { "error": { "code": "suppressed", "message": "This profile is suppressed. Contact lookup is free.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 approval_required · request_id in the body · Retryable: no This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it. { "error": { "code": "approval_required", "message": "This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "approval_threshold_credits": 500, "credits_max": 25 } }, "estimate": { "credits_min": 0, "credits_max": 25, "requires_confirm": false }, "confirm_token": "v1.payload.sig", "approval_threshold_credits": 500, "cost": { "credits": 0 } }
- 409 confirm_token_used · request_id in the body · Retryable: no This confirm_token was already used. Ask for a new preview to get another. { "error": { "code": "confirm_token_used", "message": "This confirm_token was already used. Ask for a new preview to get another.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_in_progress · request_id in the body · Retryable: yes A request with this Idempotency-Key is still running. Retry after it finishes. { "error": { "code": "idempotency_in_progress", "message": "A request with this Idempotency-Key is still running. Retry after it finishes.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 idempotency_key_reused · request_id in the body · Retryable: no Idempotency-Key was already used with a different request. { "error": { "code": "idempotency_key_reused", "message": "Idempotency-Key was already used with a different request.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 503 contacts_unavailable · request_id in the body · Retryable: no Contact lookup is not available on this server. Nothing was looked up and nothing was charged. { "error": { "code": "contacts_unavailable", "message": "Contact lookup is not available on this server. Nothing was looked up and nothing was charged.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 503 upstream_busy · request_id in the body · Retryable: yes Contact lookups are not configured on this server. { "error": { "code": "upstream_busy", "message": "Contact lookups are not configured on this server.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body. Generate a fresh key with uuidgen, python3 -c 'import uuid; print(uuid.uuid4())', or crypto.randomUUID(). A 402 and a 429 are not stored, so the same key still works after you fix the cause. Wait for Retry-After on 429.

#### Side effects and approval

A dry run has no side effects. The live call charges for what comes back. [Execution, preview and approval](https://recruitmcp.com/docs#execution) says when the call runs and when it returns 409 approval_required.

#### Next step

- [Poll a running lookup](https://recruitmcp.com/docs/api#getContactJob)
- [Read the stored contact](https://recruitmcp.com/docs/api#getPersonContacts)

### GET `/v1/contacts/jobs/{job_id}`

#### Purpose

Get a contact lookup

Free. Poll the job_id from a contact lookup that returned HTTP 202, every few seconds, until status is completed. Requires contacts:spend. status is pending until every person is stored, then completed with the emails, phones, best_email, and best_phone. cost.credits, credits_charged, and each person's credits are the credits that job charged, read from the ledger. While the job is pending they are all 0. charged_this_request is 0 while pending; once completed it is the same number as credits_charged, because the lookup that started the job is charged when the job completes. The poll itself never charges: X-Credits-Charged is 0 and polling again charges nothing more. An idempotent replay of the original lookup has charged_this_request 0 and cost.credits 0. Polling example:

```
KEY="<your API key>"
curl -s -X POST https://api.recruitmcp.com/v1/people/contacts:fetch \
  -H "Authorization: Bearer $KEY" -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" -d '{"profile_ids":["lead_8f2a1c_1"]}'
# 202: {"status":"pending","job_id":"cjob_01J0CJOB0EXAMPLE","poll_url":"/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE",...}
# Poll until status is completed (pending until then). The wait doubles from 2 s up to 30 s; stop on an error, give up after about 2 minutes.
delay=2
for i in 1 2 3 4 5 6 7 8; do
  body=$(curl -sS https://api.recruitmcp.com/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE -H "Authorization: Bearer $KEY")
  echo "$body" | grep -q '"status":"completed"' && break
  echo "$body" | grep -q '"error":{' && break
  sleep $delay
  delay=$((delay*2>30?30:delay*2))
done
echo "$body"
# data[].contacts.best_email and best_phone; credits_charged and charged_this_request equal cost.credits
```

#### Scopes

contacts:spend

#### Cost, free cases, and preview

Credit cost. Free. Polling a contact lookup does not spend credits.

#### Parameters

- job_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Completed. Example response.

```
{
  "status": "completed",
  "request_id": "req_4f1c2a9b7e3d5a60",
  "job_id": "cjob_01J0CJOB0EXAMPLE",
  "poll_url": "/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE",
  "canonical_id": "lead_8f2a1c_1",
  "data": [
    {
      "profile_id": "lead_8f2a1c_1",
      "canonical_id": "lead_8f2a1c_1",
      "contacts": {
        "status": "ready",
        "emails": [
          {
            "value": "ada.peeters@example.com",
            "type": "personal",
            "line": "personal",
            "confidence": 0.9
          }
        ],
        "phones": [
          {
            "value": "+447700900001",
            "type": "personal",
            "line": "mobile",
            "confidence": 0.8
          }
        ],
        "best_email": {
          "value": "ada.peeters@example.com",
          "type": "personal",
          "line": "personal",
          "confidence": 0.9,
          "reason": "only_candidate"
        },
        "best_phone": {
          "value": "+447700900001",
          "type": "personal",
          "line": "mobile",
          "confidence": 0.8,
          "reason": "only_candidate"
        },
        "expires_at": "2026-11-12T09:00:00Z"
      },
      "credits": 25
    }
  ],
  "cost": {
    "credits": 25
  },
  "charged_this_request": 25,
  "credits_charged": 25
}
```

HTTP 200. Pending. Example response.

```
{
  "status": "pending",
  "request_id": "req_4f1c2a9b7e3d5a60",
  "job_id": "cjob_01J0CJOB0EXAMPLE",
  "poll_url": "/v1/contacts/jobs/cjob_01J0CJOB0EXAMPLE",
  "canonical_id": "lead_8f2a1c_1",
  "data": [
    {
      "profile_id": "lead_8f2a1c_1",
      "canonical_id": "lead_8f2a1c_1",
      "contacts": {
        "status": "pending",
        "emails": [],
        "phones": [],
        "best_email": null,
        "best_phone": null,
        "expires_at": null
      },
      "credits": 0
    }
  ],
  "cost": {
    "credits": 0
  },
  "charged_this_request": 0,
  "credits_charged": 0
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the stored contact](https://recruitmcp.com/docs/api#getPersonContacts)

### GET `/v1/people/{profile_id}/contacts`

#### Purpose

Get stored contact details

Free. Only what an earlier lookup found. Each email includes type work or personal when the lookup provided it. best_email and best_phone are the stored pick. contacts:spend can read contacts it paid for. sourcing:read can read them too.

#### Scopes

sourcing:read, contacts:spend

#### Cost, free cases, and preview

Credit cost. Free. Reading or deleting a stored contact does not spend credits.

#### Parameters

- profile_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/people/lead_8f2a1c_1/contacts' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "profile_id": "lead_8f2a1c_1",
  "canonical_id": "id_01J0EXAMPLE000000",
  "contacts": {
    "status": "ready",
    "emails": [
      {}
    ],
    "phones": [
      {}
    ],
    "best_email": {
      "value": "string",
      "type": "personal",
      "confidence": 1,
      "reason": "only_candidate"
    },
    "best_phone": {
      "value": "string",
      "type": "personal",
      "confidence": 1,
      "reason": "only_candidate"
    },
    "expires_at": "2026-10-13T09:00:00Z"
  },
  "credits": 0,
  "cost": {
    "credits": 0,
    "cached_items": 0,
    "live_items": 0,
    "upstream_calls": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_id · request_id in the body · Retryable: no profile_id is not a valid person id. { "error": { "code": "invalid_id", "message": "profile_id is not a valid person id.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "field": "profile_id" } } }
- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No contacts are stored for this profile. { "error": { "code": "not_found", "message": "No contacts are stored for this profile.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 503 contacts_unavailable · request_id in the body · Retryable: no Contact lookup is not available on this server. Nothing was looked up and nothing was charged. { "error": { "code": "contacts_unavailable", "message": "Contact lookup is not available on this server. Nothing was looked up and nothing was charged.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Enroll the person](https://recruitmcp.com/docs/api#createEnrollments)

## Lists

Shortlists and the seen register. Writing them needs lists:write. A key with sourcing:spend is still accepted.

### First request

Lists are shortlists for this workspace. The first request reads them. A new workspace has none. Writing a list needs lists:write. A key with sourcing:spend is still accepted.

```
GET /v1/lists
```

### Filters

The list index has no search filters. Items are people, named by profile_id when you add them.

### Pagination

limit defaults to 25 and the maximum is 100. Pass next_cursor back as cursor. The same cursor is on list items and on the seen register.

```
GET /v1/lists?limit=10
```

### Recipes

#### Create a shortlist

name is required. items can wait.

```
POST /v1/lists

{
  "name": "Backend shortlist"
}
```

### Reference

### GET `/v1/lists`

#### Purpose

List candidate lists

Scope: \`sourcing:read\`.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lists' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "id_01J0EXAMPLE000000",
      "object": "list",
      "name": "Northwind Talent",
      "item_count": 1
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Create a list](https://recruitmcp.com/docs/api#createList)

### POST `/v1/lists`

#### Purpose

Create a candidate list

Scope: \`lists:write\`. A key with \`sourcing:spend\` is also accepted.

#### Scopes

lists:write, sourcing:spend

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- name

  body · string · required

- description

  body · string | null · optional

- items

  body · array of ListItemInput · optional

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/lists' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Backend Berlin shortlist","items":[{"profile_id":"lead_8f2a1c_1","note":"Strong Go background"}]}'
```

#### Success

HTTP 201. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "object": "list",
  "name": "Northwind Talent",
  "description": "Shortlist of backend engineers in Berlin.",
  "item_count": 1,
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Add people](https://recruitmcp.com/docs/api#addListItems)

### GET `/v1/lists/{list_id}`

#### Purpose

Get a list

Scope: \`sourcing:read\`.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- list_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lists/lst_01J0LIST0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "object": "list",
  "name": "Northwind Talent",
  "description": "Shortlist of backend engineers in Berlin.",
  "item_count": 1,
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the people on it](https://recruitmcp.com/docs/api#listListItems)

### PATCH `/v1/lists/{list_id}`

#### Purpose

Rename or describe a list

Scope: \`lists:write\`. A key with \`sourcing:spend\` is also accepted.

#### Scopes

lists:write, sourcing:spend

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- list_id

  path · string · required

- name

  body · string · optional

- description

  body · string | null · optional

#### Minimal request

Example request.

```
curl -sS -X PATCH 'https://api.recruitmcp.com/v1/lists/lst_01J0LIST0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Northwind Talent","description":"Shortlist of backend engineers in Berlin."}'
```

#### Success

HTTP 200. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "object": "list",
  "name": "Northwind Talent",
  "description": "Shortlist of backend engineers in Berlin.",
  "item_count": 1,
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Export the list](https://recruitmcp.com/docs/api#exportList)

### DELETE `/v1/lists/{list_id}`

#### Purpose

Delete a list

Scope: \`lists:write\`. A key with \`sourcing:spend\` is also accepted.

#### Scopes

lists:write, sourcing:spend

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- list_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/lists/lst_01J0LIST0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 204. Example response.

```
HTTP 204 No Content
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [List what remains](https://recruitmcp.com/docs/api#listLists)

### GET `/v1/lists/{list_id}/items`

#### Purpose

List the people on a list

Scope: \`sourcing:read\`.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- list_id

  path · string · required

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lists/lst_01J0LIST0EXAMPLE/items' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "id_01J0EXAMPLE000000",
      "object": "list_item"
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Enroll the list](https://recruitmcp.com/docs/api#createEnrollments)

### POST `/v1/lists/{list_id}/items`

#### Purpose

Add people to a list

People already on the list are skipped. The search id and the other profile id are the same person and are stored once, under the search id.

#### Scopes

lists:write, sourcing:spend

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- list_id

  path · string · required

- items

  body · array of ListItemInput · required

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/lists/lst_01J0LIST0EXAMPLE/items' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"items":[{"profile_id":"lead_8f2a1c_1"}]}'
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "id_01J0EXAMPLE000000",
      "object": "list_item"
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Set candidate status](https://recruitmcp.com/docs/api#upsertSeen)

### DELETE `/v1/lists/{list_id}/items/{item_id}`

#### Purpose

Remove a person from a list

Scope: \`lists:write\`. A key with \`sourcing:spend\` is also accepted.

#### Scopes

lists:write, sourcing:spend

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- list_id

  path · string · required

- item_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/lists/lst_01J0LIST0EXAMPLE/items/itm_01J0ITEM0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 204. Example response.

```
HTTP 204 No Content
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the list](https://recruitmcp.com/docs/api#listListItems)

### GET `/v1/lists/{list_id}/export`

#### Purpose

Export a list as CSV

One row per person. An empty email is filled from a contact lookup that found one. An opted-out address is left blank, and a suppressed person is omitted. The photo column is this API's media URL.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- list_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/lists/lst_01J0LIST0EXAMPLE/export' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
item_id,profile_id,full_name,company,email,profile_url,photo,company_url,note,added_at
itm_01J0ITEM0EXAMPLE,lead_8f2a1c_1,Ada Peeters,Northwind Labs,ada@northwind.example,https://profiles.northwind.example/ada-peeters,https://recruitmcp.com/v1/media/example,https://profiles.northwind.example/company/northwind-labs,,2026-10-13T09:00:00Z
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Enroll the list](https://recruitmcp.com/docs/api#createEnrollments)

### GET `/v1/seen`

#### Purpose

List the seen register

Everyone this workspace has seen, shortlisted, contacted, or rejected. A people search marks them already_seen, and drops them when exclude_seen is true.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- status

  query · "seen" | "shortlisted" | "contacted" | "rejected" | "do_not_contact" · optional Only entries with this status.

- tag

  query · string · optional Only entries with this tag.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/seen' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "id_01J0EXAMPLE000000",
      "object": "seen_entry",
      "status": "seen"
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Set a status](https://recruitmcp.com/docs/api#upsertSeen)

### POST `/v1/seen`

#### Purpose

Add or update seen entries

Up to 1000 people. An entry matching an existing one (same profile, or same name and employer) is updated. Use external_ids to keep your ATS ids next to each person.

#### Scopes

lists:write, sourcing:spend

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- entries

  body · array of SeenEntryInput · required

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/seen' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"entries":[{"profile_id":"lead_8f2a1c_1","status":"shortlisted","tags":["backend-berlin"],"external_ids":{"ats":"cand_123"}}]}'
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "id_01J0EXAMPLE000000",
      "object": "seen_entry",
      "status": "seen"
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search again with exclude_seen](https://recruitmcp.com/docs/api#searchPeople)

### POST `/v1/seen:check`

#### Purpose

Check people against the seen register

Up to 1000 identities, each a profile_url, profile_id, or full_name with current_employer. Free.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- identities

  body · array of object · required

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/seen:check' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"identities":[{"full_name":"Ada Peeters","current_employer":"Acme"}]}'
```

#### Success

HTTP 200. Example response.

```
{
  "data": [
    {}
  ]
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Update a status](https://recruitmcp.com/docs/api#upsertSeen)

### GET `/v1/seen/{seen_id}`

#### Purpose

Get a seen entry

Scope: \`sourcing:read\`.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- seen_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/seen/id_01J0EXAMPLE000000' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "object": "seen_entry",
  "status": "seen",
  "profile_url": "https://profiles.northwind.example/ada-peeters",
  "full_name": "Ada Peeters",
  "current_employer": "string",
  "note": "Shortlisted after the Berlin search.",
  "tags": [
    "string"
  ],
  "external_ids": {
    "key": "key_01J0APIKEY0EXAMPLE"
  },
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Change the status](https://recruitmcp.com/docs/api#updateSeenEntry)

### PATCH `/v1/seen/{seen_id}`

#### Purpose

Update a seen entry

external_ids are merged; tags replace the stored tags.

#### Scopes

lists:write, sourcing:spend

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- seen_id

  path · string · required

- status

  body · "seen" | "shortlisted" | "contacted" | "rejected" | "do_not_contact" · optional

- note

  body · string | null · optional

- tags

  body · array of string · optional

- external_ids

  body · object · optional

#### Minimal request

Example request.

```
curl -sS -X PATCH 'https://api.recruitmcp.com/v1/seen/id_01J0EXAMPLE000000' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"status":"rejected","note":"Not open to relocation."}'
```

#### Success

HTTP 200. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "object": "seen_entry",
  "status": "seen",
  "profile_url": "https://profiles.northwind.example/ada-peeters",
  "full_name": "Ada Peeters",
  "current_employer": "string",
  "note": "Shortlisted after the Berlin search.",
  "tags": [
    "string"
  ],
  "external_ids": {
    "key": "key_01J0APIKEY0EXAMPLE"
  },
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search again](https://recruitmcp.com/docs/api#searchPeople)

### DELETE `/v1/seen/{seen_id}`

#### Purpose

Remove a seen entry

The person can be returned by a later search. exclude_seen no longer drops them.

#### Scopes

lists:write, sourcing:spend

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- seen_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/seen/id_01J0EXAMPLE000000' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 204. Example response.

```
HTTP 204 No Content
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the register](https://recruitmcp.com/docs/api#listSeen)

## Account

The workspace, the keys that call it, the balance, and usage.

### First request

Account is the workspace, its keys, and its credits. The first request reads the workspace that owns the key.

```
GET /v1/workspace
```

### Filters

These calls do not take search filters. GET /v1/billing/transactions accepts kind: grant, purchase, usage, or expiry.

```
GET /v1/billing/transactions?kind=grant
```

### Pagination

The credit ledger uses limit and cursor. limit defaults to 25 and the maximum is 100. GET /v1/usage groups spend. It is not a cursor page.

```
GET /v1/billing/transactions?limit=10
```

### Recipes

#### Read the credit balance

The balance by source, including the next expiries.

```
GET /v1/billing/balance
```

### Reference

### POST `/v1/workspaces`

#### Purpose

Create a workspace

Returns the first API key once. When email verification is on, signup credits are added after the email link is opened. attribution is optional. When it is omitted, the rmcp_attr cookie from the first visit is stored instead. marketing_opt_in is optional and defaults to false. No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- name

  body · string · required

- email

  body · string · required Owner email. Required: the confirmation link goes there, and the free credits are added once it is opened. A missing or malformed address is 400 invalid_request with details.field email.

- first_name

  body · string | null · optional Owner first name. Optional. Not taken from the workspace name.

- marketing_opt_in

  body · boolean · optional Optional. Leave false, or omit it, until the person ticks the product-updates checkbox.

- attribution

  body · object · optional

- attribution.utm_source

  body · string | null · optional utm_source from the first visit.

- attribution.utm_medium

  body · string | null · optional

- attribution.utm_campaign

  body · string | null · optional

- attribution.utm_content

  body · string | null · optional

- attribution.utm_term

  body · string | null · optional

- attribution.referrer

  body · string | null · optional https referrer without a query string.

- attribution.landing_path

  body · string | null · optional Path of the first page. Starts with /.

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/workspaces' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Northwind Talent","email":"ops@northwind.example","marketing_opt_in":false}'
```

#### Success

HTTP 201. Example response.

```
{
  "id": "ws_01J0WORKSPACE0EXAMPLE",
  "name": "Northwind Search",
  "api_key": "rmcp_live_examplekeyexamplekeyexample",
  "balance_credits": 0,
  "scopes": [
    "sourcing:read",
    "sourcing:spend",
    "lists:write",
    "contacts:spend",
    "outreach:read",
    "outreach:write",
    "outreach:send",
    "outreach:approve",
    "privacy:write",
    "account:read",
    "account:manage",
    "billing:manage"
  ],
  "email_verification": "sent",
  "signup_credits_pending": 1250
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no email is required. Free credits are added after you confirm it. { "error": { "code": "invalid_request", "message": "email is required. Free credits are added after you confirm it.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "field": "email" }, "fix": "Send email with the owner's address. The API key comes back at once; the free credits are added when the link in the confirmation email is opened.", "docs": "/docs/errors#400" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Run a search with the new key](https://recruitmcp.com/docs/api#searchPeople)

### POST `/v1/signup`

#### Purpose

Create a workspace

Same as POST /v1/workspaces. attribution is optional. When it is omitted, the rmcp_attr cookie from the first visit is stored instead. No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- name

  body · string · required

- email

  body · string · required Owner email. Required: the confirmation link goes there, and the free credits are added once it is opened. A missing or malformed address is 400 invalid_request with details.field email.

- first_name

  body · string | null · optional Owner first name. Optional. Not taken from the workspace name.

- marketing_opt_in

  body · boolean · optional Optional. Leave false, or omit it, until the person ticks the product-updates checkbox.

- attribution

  body · object · optional

- attribution.utm_source

  body · string | null · optional utm_source from the first visit.

- attribution.utm_medium

  body · string | null · optional

- attribution.utm_campaign

  body · string | null · optional

- attribution.utm_content

  body · string | null · optional

- attribution.utm_term

  body · string | null · optional

- attribution.referrer

  body · string | null · optional https referrer without a query string.

- attribution.landing_path

  body · string | null · optional Path of the first page. Starts with /.

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/signup' \
  -H 'Content-Type: application/json' \
  -d '{"name":"Northwind Talent","email":"ops@northwind.example","marketing_opt_in":false}'
```

#### Success

HTTP 201. Example response.

```
{
  "id": "ws_01J0WORKSPACE0EXAMPLE",
  "name": "Northwind Search",
  "api_key": "rmcp_live_examplekeyexamplekeyexample",
  "balance_credits": 0,
  "scopes": [
    "sourcing:read",
    "sourcing:spend",
    "lists:write",
    "contacts:spend",
    "outreach:read",
    "outreach:write",
    "outreach:send",
    "outreach:approve",
    "privacy:write",
    "account:read",
    "account:manage",
    "billing:manage"
  ],
  "email_verification": "sent",
  "signup_credits_pending": 1250
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no email is required. Free credits are added after you confirm it. { "error": { "code": "invalid_request", "message": "email is required. Free credits are added after you confirm it.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "field": "email" }, "fix": "Send email with the owner's address. The API key comes back at once; the free credits are added when the link in the confirmation email is opened.", "docs": "/docs/errors#400" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Run a search with the new key](https://recruitmcp.com/docs/api#searchPeople)

### POST `/v1/workspaces/verify`

#### Purpose

Confirm a signup email

No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- token

  body · string · required

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/workspaces/verify' \
  -H 'Content-Type: application/json' \
  -d '{"token":"tok_01J0VERIFY0TOKEN0EXAMPLE"}'
```

#### Success

HTTP 200. Example response.

```
{
  "verified": true,
  "workspace_id": "ws_01J0WORKSPACE0EXAMPLE",
  "granted_credits": 1250
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 invalid_verification_link · request_id in the body · Retryable: no This confirmation link is invalid, used, or expired. { "error": { "code": "invalid_verification_link", "message": "This confirmation link is invalid, used, or expired.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the balance after the grant](https://recruitmcp.com/docs/api#getBalance)

### POST `/v1/workspaces/verify/resend`

#### Purpose

Send the confirmation email again

For a workspace whose signup email is not confirmed yet. The answer is the same whether or not the address has such a workspace. At most 3 emails per address per hour; above that the answer is the same and nothing is sent. No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- email

  body · string · required

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/workspaces/verify/resend' \
  -H 'Content-Type: application/json' \
  -d '{"email":"ada@example.com"}'
```

#### Success

HTTP 202. Example response.

```
{
  "object": "verification_resend",
  "message": "The request was accepted."
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Confirm with the link from the email](https://recruitmcp.com/docs/api#verifyWorkspaceEmail)

### POST `/v1/workspaces/recover`

#### Purpose

Ask for a new API key by email

For a lost key. When the address is the exact, confirmed owner address of a workspace, a link is emailed to that address. The link works once, expires in 15 minutes, and asks for a confirmation before it issues a new key. The answer is the same whether or not the address has a workspace. At most 3 emails per address per hour; above that the answer is the same and nothing is sent. At most 10 requests per network per hour. No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- email

  body · string · required

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/workspaces/recover' \
  -H 'Content-Type: application/json' \
  -d '{"email":"ada@example.com"}'
```

#### Success

HTTP 202. Example response.

```
{
  "object": "key_recovery_request",
  "message": "The request was accepted."
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

No next step on this call.

### POST `/v1/workspaces/recover/link`

#### Purpose

Check a key recovery link

Names the workspace a recovery link is for, without using the link. Same 404 for a link that is invalid, used, or expired. No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- token

  body · string · required

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/workspaces/recover/link' \
  -H 'Content-Type: application/json' \
  -d '{"token":"tok_01J0VERIFY0TOKEN0EXAMPLE"}'
```

#### Success

HTTP 200. Example response.

```
{
  "object": "key_recovery_link",
  "workspace_name": "ws_01J0WORKSPACE0EXAMPLE",
  "expires_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 invalid_recovery_link · request_id in the body · Retryable: no This recovery link is invalid, used, or expired. Ask for a new one at /recover. { "error": { "code": "invalid_recovery_link", "message": "This recovery link is invalid, used, or expired. Ask for a new one at /recover.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

No next step on this call.

### POST `/v1/workspaces/recover/confirm`

#### Purpose

Confirm a key recovery

Uses the link once. Returns a new API key, shown once, with the scopes the old keys had. The workspace's other secret keys are revoked. Keys bound to a customer and OAuth connections stay. A confirmation email goes to the workspace address with the keys and connections still active and how to revoke them. Same 404 for a link that is invalid, used, or expired. No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- token

  body · string · required

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/workspaces/recover/confirm' \
  -H 'Content-Type: application/json' \
  -d '{"token":"tok_01J0VERIFY0TOKEN0EXAMPLE"}'
```

#### Success

HTTP 200. Example response.

```
{
  "object": "key_recovery",
  "workspace_id": "ws_01J0WORKSPACE0EXAMPLE",
  "workspace_name": "ws_01J0WORKSPACE0EXAMPLE",
  "api_key": "rmcp_live_examplekeyexamplekeyexample",
  "key_prefix": "key_01J0APIKEY0EXAMPLE",
  "revoked_keys": 1
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 invalid_recovery_link · request_id in the body · Retryable: no This recovery link is invalid, used, or expired. Ask for a new one at /recover. { "error": { "code": "invalid_recovery_link", "message": "This recovery link is invalid, used, or expired. Ask for a new one at /recover.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

No next step on this call.

### GET `/v1/workspace`

#### Purpose

Get the current workspace and key

Scope: \`account:read\`.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

No parameters.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/workspace' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "id": "ws_01J0WORKSPACE0EXAMPLE",
  "object": "workspace",
  "name": "Northwind Search",
  "email": "ada@northwind.example",
  "first_name": "Ada",
  "account_id": "ws_01J0WORKSPACE0EXAMPLE",
  "customer": null,
  "balance_credits": 1250,
  "low_balance_credits": 250,
  "plan": "free",
  "marketing_opt_in": false,
  "marketing_consent_at": "2026-10-15T12:00:00.000Z",
  "marketing_consent_source": "signup",
  "marketing_consent_text_version": "2026-10-15",
  "lifecycle_suppressed": false,
  "api_key": {
    "id": "key_01J0APIKEY0EXAMPLE",
    "name": "default",
    "prefix": "rmcp_live_ab12",
    "kind": "secret",
    "scopes": [
      "sourcing:read",
      "sourcing:spend",
      "lists:write"
    ],
    "daily_cap_credits": null,
    "approval_threshold_credits": null,
    "approval_threshold_enrollments": null,
    "expires_at": null
  },
  "api_version": "2026-10-13"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

Only the common errors apply. See Common headers and errors.

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [List keys](https://recruitmcp.com/docs/api#listApiKeys)

### PATCH `/v1/workspace`

#### Purpose

Update workspace preferences

credits.low fires when the balance drops below low_balance_credits. The default is 250. Send null to restore the default. marketing_opt_in stores the product-updates choice. lifecycle_suppressed stops follow-up emails. Send at least one of these fields.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- low_balance_credits

  body · integer | null · optional Whole credits. Null restores the default.

- marketing_opt_in

  body · boolean · optional Product updates and tips. Omit to leave the stored choice.

- lifecycle_suppressed

  body · boolean · optional Stop follow-up emails for this workspace. Omit to leave the stored flag.

#### Minimal request

Example request.

```
curl -sS -X PATCH 'https://api.recruitmcp.com/v1/workspace' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"low_balance_credits":1,"marketing_opt_in":false,"lifecycle_suppressed":false}'
```

#### Success

HTTP 200. Example response.

```
{
  "id": "ws_01J0WORKSPACE0EXAMPLE",
  "object": "workspace",
  "name": "Northwind Search",
  "email": "ada@northwind.example",
  "first_name": "Ada",
  "account_id": "ws_01J0WORKSPACE0EXAMPLE",
  "customer": null,
  "balance_credits": 1250,
  "low_balance_credits": 250,
  "plan": "free",
  "marketing_opt_in": false,
  "marketing_consent_at": "2026-10-15T12:00:00.000Z",
  "marketing_consent_source": "signup",
  "marketing_consent_text_version": "2026-10-15",
  "lifecycle_suppressed": false,
  "api_key": {
    "id": "key_01J0APIKEY0EXAMPLE",
    "name": "default",
    "prefix": "rmcp_live_ab12",
    "kind": "secret",
    "scopes": [
      "sourcing:read",
      "sourcing:spend",
      "lists:write"
    ],
    "daily_cap_credits": null,
    "approval_threshold_credits": null,
    "approval_threshold_enrollments": null,
    "expires_at": null
  },
  "api_version": "2026-10-13"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the workspace](https://recruitmcp.com/docs/api#getWorkspace)

### GET `/v1/api-keys`

#### Purpose

List API keys

Scope: \`account:read\`. Acts on the account; keys bound to a customer are refused.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/api-keys' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "key_1a2b3c4d5e6f7a8b9c",
      "object": "api_key",
      "name": "n8n production",
      "prefix": "rmcp_live_ab12",
      "kind": "secret",
      "scopes": [
        "sourcing:read",
        "sourcing:spend"
      ],
      "customer_id": null,
      "daily_cap_credits": 50,
      "approval_threshold_credits": null,
      "approval_threshold_enrollments": null,
      "api_version": "2026-10-13",
      "created_at": "2026-10-13T09:00:00Z",
      "last_used_at": null,
      "expires_at": null
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Create a key](https://recruitmcp.com/docs/api#createApiKey)

### POST `/v1/api-keys`

#### Purpose

Create an API key

A key never gets more scopes, a higher daily cap, or a higher approval threshold than the key that creates it. Omitted scopes copy the caller. Do not put account:manage or billing:manage on an agent key. Set customer_id to bind the key to one customer. approval_threshold_credits null or omitted means no HTTP threshold and 500 credits on MCP. approval_threshold_enrollments null or omitted means no HTTP threshold and 25 scheduled enrollments on MCP. The key pins the RecruitMCP-Version sent with this request, or the current version.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- name

  body · string · optional

- scopes

  body · array of "sourcing:read" | "sourcing:spend" | "lists:write" | "contacts:spend" | "outreach:read" | "outreach:write" | "outreach:send" | "outreach:approve" | "privacy:write" | "account:read" | "account:manage" | "billing:manage" · optional Default: every scope of the calling key.

- daily_cap_credits

  body · number | null · optional Credits this key may spend per UTC day.

- approval_threshold_credits

  body · integer | null · optional Search, profile, company, and contact calls above this estimate return approval_required. Null means unset.

- approval_threshold_enrollments

  body · integer | null · optional An enrollment whose summary.scheduled is above this returns approval_required. Null means unset.

- customer_id

  body · string | null · optional

- expires_at

  body · string | null · optional

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/api-keys' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Read-only dashboard","scopes":["sourcing:read","account:read"]}'
```

#### Success

HTTP 201. Example response.

```
{
  "id": "key_1a2b3c4d5e6f7a8b9c",
  "object": "api_key",
  "name": "n8n production",
  "prefix": "rmcp_live_ab12",
  "kind": "secret",
  "scopes": [
    "sourcing:read",
    "sourcing:spend"
  ],
  "customer_id": null,
  "daily_cap_credits": 50,
  "approval_threshold_credits": null,
  "approval_threshold_enrollments": null,
  "api_version": "2026-10-13",
  "created_at": "2026-10-13T09:00:00Z",
  "last_used_at": null,
  "expires_at": null,
  "api_key": "rmcp_live_examplekeyexamplekeyexample"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Call search with the new key](https://recruitmcp.com/docs/api#searchPeople)

### DELETE `/v1/api-keys/{key_id}`

#### Purpose

Revoke an API key

Runs and jobs started with the key stop at their next step. Revoking the workspace's only active key leaves no key that can call the API, so it needs \`?confirm_revoke=true\`; without it the call is 409 confirmation_required and nothing is revoked. Create a new key first. If no key is left, "Lost your key?" on the console login emails a link to get a new one.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- key_id

  path · string · required

- confirm_revoke

  query · boolean · optional Required to revoke the workspace's only active key. Ignored when other active keys remain.

#### Minimal request

Example request.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/api-keys/key_01J0APIKEY0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 204. Example response.

```
HTTP 204 No Content
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [List the keys that remain](https://recruitmcp.com/docs/api#listApiKeys)

### GET `/v1/billing/plans`

#### Purpose

List plans and credit rules

No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

No parameters.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/billing/plans'
```

#### Success

HTTP 200. Example response.

```
{
  "data": [
    {}
  ],
  "rules": {
    "key": {}
  },
  "signup_credits": 1250
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

Only the common errors apply. See Common headers and errors.

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the balance](https://recruitmcp.com/docs/api#getBalance)

### GET `/v1/billing/balance`

#### Purpose

Get the credit balance

Account balance by source. A customer-bound key receives that customer's remaining daily cap, or null when the customer has no cap. It does not receive the account pool, buckets, or subscription.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

No parameters.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/billing/balance' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "balance_credits": 1250,
  "by_source": {
    "key": 1
  },
  "buckets": [
    {
      "source": "free",
      "remaining_credits": 1250
    }
  ],
  "upcoming_expiries": [],
  "next_expiry": null
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

Only the common errors apply. See Common headers and errors.

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Buy a credit pack](https://recruitmcp.com/docs/api#createCheckout)

### GET `/v1/usage`

#### Purpose

Get credit usage

A customer-bound key or a RecruitMCP-Customer header sees only that customer's spend. A bound key's balance is its remaining daily cap, or null when the customer has no cap. An account key still sees the account balance.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- group_by

  query · "day" | "purpose" | "api_key" | "customer" · optional day, purpose, api_key, or customer. Default day.

- from

  query · string (date-time) · optional Inclusive start.

- to

  query · string (date-time) · optional Inclusive end.

- customer_id

  query · string · optional Only spend inside this customer. Account keys only.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/usage' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "usage",
  "customer_id": "cus_01J0CUSTOMER0EXAMPLE",
  "balance_credits": 1250,
  "daily_cap_credits": 1,
  "group_by": "day",
  "buckets": [
    {}
  ],
  "credits": {
    "key": {}
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the balance](https://recruitmcp.com/docs/api#getBalance)

### GET `/v1/billing/transactions`

#### Purpose

List credit transactions

Credit grants, purchases, usage, and expiries, newest first. A customer-bound key sees the account ledger. Upstream cost rows are omitted. kind filters to grant, purchase, usage, or expiry. description says what the row is. A profile enrich that did not finish or found nothing is returned as a positive grant with a description that the credits were returned. request_id is the X-Request-Id of the API or MCP call that caused the charge; for an async enrich or contact lookup it is the call that started the job, and job_id names that job. Both are null when unknown, for example on older rows.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- kind

  query · "grant" | "purchase" | "usage" | "expiry" · optional Only rows of this kind.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/billing/transactions' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "led_01J0LEDGER0EXAMPLE",
      "object": "credit_transaction",
      "kind": "usage",
      "purpose": "contact_enrichment",
      "description": "Contact lookup",
      "credits": -25,
      "created_at": "2026-10-15T04:17:00.000Z",
      "api_key_id": "key_01J0KEY0EXAMPLE",
      "run_id": null,
      "customer_id": null,
      "request_id": "req_4f1c2a9b7e3d5a60",
      "job_id": "cjob_01J0CJOB0EXAMPLE"
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

No next step on this call.

### POST `/v1/billing/checkout`

#### Purpose

Start a credit pack checkout

Starts checkout for the credit pack on sale. Prices are listed at https://recruitmcp.com/pricing. Checkout saves the card for off-session auto-refill. Removed pack ids return 400.

#### Scopes

billing:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- pack

  body · string · required Pack id. The only pack is topup.

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/billing/checkout' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"pack":"topup"}'
```

#### Success

HTTP 200. Example response.

```
{
  "mode": "stripe",
  "url": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4e5f6",
  "session_id": "cs_test_a1b2c3d4e5f6",
  "credits": 7500
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the balance after payment](https://recruitmcp.com/docs/api#getBalance)

### POST `/v1/billing/subscriptions`

#### Purpose

Start a subscription checkout

Scope: \`billing:manage\`. Acts on the account; keys bound to a customer are refused.

#### Scopes

billing:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- plan

  body · string · required Plan id: starter, growth, or scale.

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/billing/subscriptions' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"plan":"growth"}'
```

#### Success

HTTP 200. Example response.

```
{
  "mode": "stripe",
  "url": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4e5f6",
  "session_id": "cs_test_a1b2c3d4e5f6",
  "credits": 7500
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the balance](https://recruitmcp.com/docs/api#getBalance)

### POST `/v1/billing/subscription/plan`

#### Purpose

Change the subscription plan

An upgrade charges the prorated price difference right away. The plan changes only once that invoice is paid; then the workspace gets the new plan's monthly credits minus the old plan's, once per change. status is upgraded when the payment went through, requires_action when the card needs authentication on hosted_invoice_url, and payment_failed when it was declined (the plan does not change). A downgrade (status scheduled_downgrade) is not prorated: there is no refund and no credits are taken back. The plan stays as it is until the next cycle, effective_at; then the lower price and credits start. Asking for the current plan while a downgrade is scheduled cancels the downgrade (status downgrade_canceled). An upgrade while a downgrade is scheduled is refused with status downgrade_scheduled (scheduled_plan and effective_at name the downgrade): cancel the downgrade first. While an upgrade payment is still pending, any change is refused with status payment_pending and hosted_invoice_url. While the plan is set to cancel in the billing portal, any change is refused with status cancel_scheduled, effective_at (the cancel date) and a message: resume the plan in the billing portal first. A subscription without automatic tax is refused with status tax_location_required and a message while the billing address cannot be used to add VAT: add it in the billing portal, then try again. Nothing is changed or charged on a refusal.

#### Scopes

billing:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- plan

  body · string · required Plan id: starter, growth, or scale.

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/billing/subscription/plan' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"plan":"growth"}'
```

#### Success

HTTP 200. Example response.

```
{
  "key": {}
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the balance](https://recruitmcp.com/docs/api#getBalance)

### GET `/v1/billing/auto-refill`

#### Purpose

Get auto-refill

Threshold, monthly caps, whether a card is on file, and how many refills have been granted this UTC month. Account keys only.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

No parameters.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/billing/auto-refill' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "auto_refill",
  "enabled": true,
  "threshold_credits": 200,
  "max_refills_per_month": 3,
  "max_eur_per_month": 297,
  "card_on_file": true,
  "refills_this_month": 0,
  "amount_eur_this_month": 0,
  "disabled_reason": null
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

Only the common errors apply. See Common headers and errors.

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Turn auto-refill on](https://recruitmcp.com/docs/api#updateAutoRefill)

### PUT `/v1/billing/auto-refill`

#### Purpose

Enable or update auto-refill

When the balance is below threshold_credits, the worker charges the pack on sale off-session, up to the monthly caps. Prices are listed at https://recruitmcp.com/pricing. A card must already be on file from a pack checkout. Credits are granted only after the payment succeeds. Account keys only.

#### Scopes

billing:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- enabled

  body · boolean · optional Default true. False keeps the card and turns charging off.

- threshold_credits

  body · integer · required Refill when the balance is below this.

- max_refills_per_month

  body · integer · required Maximum charges in a UTC month.

- max_eur_per_month

  body · integer · required Euro cap for a UTC month. At least the price of one pack. Prices are listed at https://recruitmcp.com/pricing.

#### Minimal request

Example request.

```
curl -sS -X PUT 'https://api.recruitmcp.com/v1/billing/auto-refill' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"enabled":true,"threshold_credits":200,"max_refills_per_month":3,"max_eur_per_month":297}'
```

#### Success

HTTP 200. Example response.

```
{
  "object": "auto_refill",
  "enabled": true,
  "threshold_credits": 200,
  "max_refills_per_month": 3,
  "max_eur_per_month": 297,
  "card_on_file": true,
  "refills_this_month": 0,
  "amount_eur_this_month": 0,
  "disabled_reason": null
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the settings](https://recruitmcp.com/docs/api#getAutoRefill)

### DELETE `/v1/billing/auto-refill`

#### Purpose

Turn auto-refill off

Keeps the saved card and the last threshold. Account keys only.

#### Scopes

billing:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

No parameters.

#### Minimal request

Example request.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/billing/auto-refill' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "auto_refill",
  "enabled": true,
  "threshold_credits": 200,
  "max_refills_per_month": 3,
  "max_eur_per_month": 297,
  "card_on_file": true,
  "refills_this_month": 0,
  "amount_eur_this_month": 0,
  "disabled_reason": null
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

Only the common errors apply. See Common headers and errors.

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Confirm it is off](https://recruitmcp.com/docs/api#getAutoRefill)

### GET `/v1/billing/developer`

#### Purpose

Get Developer billing

Spending limit and current-period metered usage. 404 feature_disabled when Developer billing is off for this deployment. Account keys only.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

No parameters.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/billing/developer' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "developer_billing",
  "status": "inactive",
  "spend_limit_eur": 1,
  "effective_limit_eur": 1,
  "min_spend_limit_eur": 1,
  "max_spend_limit_eur": 1,
  "usage_credits": 1,
  "usage_eur": 1,
  "period_start": "2026-10-13T09:00:00Z",
  "period_end": "2026-10-13T09:00:00Z",
  "first_invoice_paid": false,
  "card_on_file": true,
  "suspended_reason": "string",
  "price_eur_per_credit": 1
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 404 feature_disabled · request_id in the body · Retryable: no Developer billing is not enabled. { "error": { "code": "feature_disabled", "message": "Developer billing is not enabled.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

No next step on this call.

### PATCH `/v1/billing/developer`

#### Purpose

Set the Developer spending limit

Whole euros from 10 to 1000. The first unpaid cycle is also capped, and a server ceiling can be lower. 404 feature_disabled when Developer billing is off. Account keys only.

#### Scopes

billing:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- spend_limit_eur

  body · integer · required Monthly limit in euros.

#### Minimal request

Example request.

```
curl -sS -X PATCH 'https://api.recruitmcp.com/v1/billing/developer' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"spend_limit_eur":50}'
```

#### Success

HTTP 200. Example response.

```
{
  "object": "developer_billing",
  "status": "inactive",
  "spend_limit_eur": 1,
  "effective_limit_eur": 1,
  "min_spend_limit_eur": 1,
  "max_spend_limit_eur": 1,
  "usage_credits": 1,
  "usage_eur": 1,
  "period_start": "2026-10-13T09:00:00Z",
  "period_end": "2026-10-13T09:00:00Z",
  "first_invoice_paid": false,
  "card_on_file": true,
  "suspended_reason": "string",
  "price_eur_per_credit": 1
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 feature_disabled · request_id in the body · Retryable: no Developer billing is not enabled. { "error": { "code": "feature_disabled", "message": "Developer billing is not enabled.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

No next step on this call.

### POST `/v1/billing/developer/checkout`

#### Purpose

Start Developer card setup

Stripe Checkout in setup mode saves a card. Usage after the prepaid balance is zero is metered and billed monthly in arrears. Refused while a plan subscription is open. 404 feature_disabled when Developer billing is off. Account keys only.

#### Scopes

billing:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

No parameters.

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/billing/developer/checkout' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "mode": "stripe",
  "url": "https://checkout.stripe.com/c/pay/cs_test_a1b2c3d4e5f6",
  "session_id": "cs_test_a1b2c3d4e5f6",
  "credits": 7500
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 404 feature_disabled · request_id in the body · Retryable: no Developer billing is not enabled. { "error": { "code": "feature_disabled", "message": "Developer billing is not enabled.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 503 internal · request_id in the body · Retryable: no Developer billing is not configured. { "error": { "code": "internal", "message": "Developer billing is not configured.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

No next step on this call.

## Outreach

Outreach runs from your agent on your own connected account

## Webhooks & events

Read the event log, or subscribe an HTTPS endpoint.

### GET `/v1/events`

#### Purpose

List events

Everything that happened on the account in the last 30 days: jobs, contact lookups, enrollments, replies, accounts, and credits. A customer key sees only its customer's events. The same events are pushed to webhook endpoints.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- type

  query · string · optional One event type. Same filter as types.

- types

  query · array of string · optional Event types, comma separated. Default all.

- customer_id

  query · string · optional Only events from this customer. Use none for account-level events.

- created_after

  query · string (date-time) · optional Only events after this time.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/events' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "evt_job_job_3c4d_20261013093000",
      "object": "event",
      "type": "job.completed",
      "created_at": "2026-10-13T09:30:00Z",
      "customer_id": null,
      "data": {
        "key": {}
      }
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Subscribe a webhook](https://recruitmcp.com/docs/api#createWebhookEndpoint)

### GET `/v1/events/{event_id}`

#### Purpose

Get an event

Scope: \`account:read\`.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- event_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/events/evt_01J0EVENT0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "id": "evt_job_job_3c4d_20261013093000",
  "object": "event",
  "type": "job.completed",
  "created_at": "2026-10-13T09:30:00Z",
  "customer_id": null,
  "data": {
    "key": {}
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [See which endpoints receive this type](https://recruitmcp.com/docs/api#listWebhookEndpoints)

### GET `/v1/webhook-endpoints`

#### Purpose

List webhook endpoints

Scope: \`account:read\`. Acts on the account; keys bound to a customer are refused.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/webhook-endpoints' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "id_01J0EXAMPLE000000",
      "object": "webhook_endpoint",
      "url": "https://profiles.northwind.example/ada-peeters",
      "events": [
        "evt_01J0EVENT0EXAMPLE"
      ],
      "status": "enabled"
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Add an endpoint](https://recruitmcp.com/docs/api#createWebhookEndpoint)

### POST `/v1/webhook-endpoints`

#### Purpose

Add a webhook endpoint

Up to 10 per account. The URL must be public https; private, loopback, and link-local addresses are refused, the address is checked again on every delivery, and redirects are not followed. Each delivery is a POST with the event as JSON and a X-RecruitMCP-Signature header: t=\<unix seconds>,v1=\<hex HMAC-SHA256 of "\<t>.\<exact request body>" with the endpoint secret>. Reject a delivery whose t is more than five minutes old. Failed deliveries are retried with backoff for about a day; use the event id to ignore duplicates.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- url

  body · string · optional Public https URL. Private and loopback addresses are refused.

- events

  body · array of string · optional Default \["\*"].

- customer_id

  body · string | null · optional

- description

  body · string | null · optional

- status

  body · "enabled" | "disabled" · optional

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/webhook-endpoints' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://hooks.example.com/recruitmcp","events":["job.completed","reply.received"]}'
```

#### Success

HTTP 201. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "object": "webhook_endpoint",
  "url": "https://profiles.northwind.example/ada-peeters",
  "events": [
    "evt_01J0EVENT0EXAMPLE"
  ],
  "customer_id": "cus_01J0CUSTOMER0EXAMPLE",
  "description": "Shortlist of backend engineers in Berlin.",
  "status": "enabled",
  "secret_hint": "string",
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z",
  "secret": "sec_01J0SIGNING0SECRET0EXAMPLE"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Send a test event](https://recruitmcp.com/docs/api#testWebhookEndpoint)

### GET `/v1/webhook-endpoints/{endpoint_id}`

#### Purpose

Get a webhook endpoint

Scope: \`account:read\`. Acts on the account; keys bound to a customer are refused.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- endpoint_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/webhook-endpoints/we_01J0WEBHOOK0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "object": "webhook_endpoint",
  "url": "https://profiles.northwind.example/ada-peeters",
  "events": [
    "evt_01J0EVENT0EXAMPLE"
  ],
  "customer_id": "cus_01J0CUSTOMER0EXAMPLE",
  "description": "Shortlist of backend engineers in Berlin.",
  "status": "enabled",
  "secret_hint": "string",
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Rotate the secret](https://recruitmcp.com/docs/api#rotateWebhookSecret)

### PATCH `/v1/webhook-endpoints/{endpoint_id}`

#### Purpose

Update a webhook endpoint

Scope: \`account:manage\`. Acts on the account; keys bound to a customer are refused.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- endpoint_id

  path · string · required

- url

  body · string · optional Public https URL. Private and loopback addresses are refused.

- events

  body · array of string · optional Default \["\*"].

- customer_id

  body · string | null · optional

- description

  body · string | null · optional

- status

  body · "enabled" | "disabled" · optional

#### Minimal request

Example request.

```
curl -sS -X PATCH 'https://api.recruitmcp.com/v1/webhook-endpoints/we_01J0WEBHOOK0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://hooks.example.com/recruitmcp","events":["job.completed","reply.received"],"status":"disabled"}'
```

#### Success

HTTP 200. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "object": "webhook_endpoint",
  "url": "https://profiles.northwind.example/ada-peeters",
  "events": [
    "evt_01J0EVENT0EXAMPLE"
  ],
  "customer_id": "cus_01J0CUSTOMER0EXAMPLE",
  "description": "Shortlist of backend engineers in Berlin.",
  "status": "enabled",
  "secret_hint": "string",
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Send a test event](https://recruitmcp.com/docs/api#testWebhookEndpoint)

### DELETE `/v1/webhook-endpoints/{endpoint_id}`

#### Purpose

Delete a webhook endpoint

Scope: \`account:manage\`. Acts on the account; keys bound to a customer are refused.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- endpoint_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/webhook-endpoints/we_01J0WEBHOOK0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 204. Example response.

```
HTTP 204 No Content
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [List endpoints](https://recruitmcp.com/docs/api#listWebhookEndpoints)

### POST `/v1/webhook-endpoints/{endpoint_id}/rotate-secret`

#### Purpose

Rotate an endpoint's signing secret

The old secret stops working immediately.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- endpoint_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/webhook-endpoints/we_01J0WEBHOOK0EXAMPLE/rotate-secret' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "object": "webhook_endpoint",
  "url": "https://profiles.northwind.example/ada-peeters",
  "events": [
    "evt_01J0EVENT0EXAMPLE"
  ],
  "customer_id": "cus_01J0CUSTOMER0EXAMPLE",
  "description": "Shortlist of backend engineers in Berlin.",
  "status": "enabled",
  "secret_hint": "string",
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z",
  "secret": "sec_01J0SIGNING0SECRET0EXAMPLE"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Confirm delivery with the new secret](https://recruitmcp.com/docs/api#testWebhookEndpoint)

### POST `/v1/webhook-endpoints/{endpoint_id}/test`

#### Purpose

Send a test event

Sends one signed ping event now and reports the answer. Not retried.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- endpoint_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/webhook-endpoints/we_01J0WEBHOOK0EXAMPLE/test' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "ok": false,
  "status": 1,
  "event_id": "evt_01J0EVENT0EXAMPLE",
  "error": "invalid_request"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the event log](https://recruitmcp.com/docs/api#listEvents)

## Privacy & opt-out

Erase a person, delete stored contacts, or suppress them so search leaves them out.

### DELETE `/v1/people/{profile_id}`

#### Purpose

Erase a person from this workspace

Deletes the cached profile, contacts, seen entries, and list items for this person in the workspace. Does not suppress them. POST /v1/erasures with suppress true also blocks future results.

#### Scopes

privacy:write

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- profile_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/people/lead_8f2a1c_1' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 204. Example response.

```
HTTP 204 No Content
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Erase and suppress so they do not return](https://recruitmcp.com/docs/api#createErasure)

### DELETE `/v1/people/{profile_id}/contacts`

#### Purpose

Delete stored contact details

Scope: \`privacy:write\`.

#### Scopes

privacy:write

#### Cost, free cases, and preview

Credit cost. Free. Reading or deleting a stored contact does not spend credits.

#### Parameters

- profile_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/people/lead_8f2a1c_1/contacts' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 204. Example response.

```
HTTP 204 No Content
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_id · request_id in the body · Retryable: no profile_id is not a valid person id. { "error": { "code": "invalid_id", "message": "profile_id is not a valid person id.", "request_id": "req_01J0REQUEST0EXAMPLE", "details": { "field": "profile_id" } } }
- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No contacts are stored for this profile. { "error": { "code": "not_found", "message": "No contacts are stored for this profile.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Confirm the contacts are gone](https://recruitmcp.com/docs/api#getPersonContacts)

### POST `/v1/erasures`

#### Purpose

Erase a person and suppress them

Deletes every row in this workspace that names the person and, unless suppress is false, adds a hashed suppression so they never come back in search or contact lookup.

#### Scopes

privacy:write

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- profile_id

  body · string · optional

- profile_url

  body · string · optional

- suppress

  body · boolean · optional Default true.

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/erasures' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"profile_id":"lead_8f2a1c_1"}'
```

#### Success

HTTP 200. Example response.

```
{
  "subject": "string",
  "erased": {
    "key": {}
  },
  "suppressed": false,
  "cost": {
    "credits": 0,
    "cached_items": 0,
    "live_items": 0,
    "upstream_calls": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read suppressions](https://recruitmcp.com/docs/api#listSuppressions)

### GET `/v1/suppressions`

#### Purpose

List suppressions

Hashes only. Suppressions always apply to search.

#### Scopes

sourcing:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/suppressions' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "id_01J0EXAMPLE000000",
      "object": "suppression",
      "kind": "profile"
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Add a suppression](https://recruitmcp.com/docs/api#createSuppression)

### POST `/v1/suppressions`

#### Purpose

Suppress a person

Blocks this person from search, runs, lists, and contact lookups in this workspace. Match on a profile URL, a company URL, an email, or a name together with a company. A name alone is refused: it is not unique.

#### Scopes

privacy:write

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- profile_url

  body · string · optional

- company_url

  body · string · optional

- email

  body · string · optional

- full_name

  body · string · optional

- company

  body · string · optional

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/suppressions' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"full_name":"Ada Peeters","company":"Acme Robotics"}'
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "id_01J0EXAMPLE000000",
      "object": "suppression",
      "kind": "profile"
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search again; suppressed people are omitted](https://recruitmcp.com/docs/api#searchPeople)

### DELETE `/v1/suppressions/{suppression_id}`

#### Purpose

Lift a suppression

Opt-outs the person filed themselves cannot be lifted.

#### Scopes

privacy:write

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- suppression_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/suppressions/id_01J0EXAMPLE000000' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 204. Example response.

```
HTTP 204 No Content
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read suppressions](https://recruitmcp.com/docs/api#listSuppressions)

### DELETE `/v1/privacy/cache`

#### Purpose

Delete cached profiles, pages, and contact details

Deletes every cached profile, cached search page, and stored contact detail in this workspace. It cannot be undone, and the next search, fetch, or contact lookup pays again. \`?dry_run=true\` returns what would be deleted and deletes nothing. Executing needs \`?confirm_delete=true\`; without it the call is 400 confirmation_required and nothing is deleted.

#### Scopes

privacy:write

#### Cost, free cases, and preview

Credit cost. dry_run=true returns the estimate and spends nothing. The executing call reports cost.credits.

Preview mode. dry_run=true as a query parameter, or dry_run set to true in the JSON body, is a preview. Either one spends nothing and changes nothing. The response includes dry_run true, an estimate, and cost.credits 0. Idempotency-Key is not required while dry_run is true. A live call omits the query parameter and omits dry_run from the body. On REST, omitting dry_run is the confirmation. On MCP it is confirm=true. [Preview (dry_run)](https://recruitmcp.com/docs/api#preview) shows one request. [Execution, preview and approval](https://recruitmcp.com/docs#execution) is the rule for this call.

#### Parameters

- confirm_delete

  query · boolean · optional Required to delete. Without it nothing is deleted.

#### Minimal request

Example request. Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/privacy/cache?dry_run=true' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "string",
  "dry_run": true,
  "deleted": true,
  "would_delete": [
    "string"
  ],
  "effect": "string",
  "execute": "string",
  "cost": {
    "credits": 0,
    "cached_items": 0,
    "live_items": 0,
    "upstream_calls": 0
  }
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body. Generate a fresh key with uuidgen, python3 -c 'import uuid; print(uuid.uuid4())', or crypto.randomUUID(). A 402 and a 429 are not stored, so the same key still works after you fix the cause. Wait for Retry-After on 429.

#### Side effects and approval

A dry run has no side effects. The live call charges for what comes back. [Execution, preview and approval](https://recruitmcp.com/docs#execution) says when the call runs and when it returns 409 approval_required.

#### Next step

- [A cached read is now not_cached](https://recruitmcp.com/docs/api#getPerson)

### POST `/v1/privacy/opt-out`

#### Purpose

Opt out (for the person themselves)

Anyone can ask to be removed. A confirmation link is emailed; once confirmed, the person is suppressed for every workspace. No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- profile_url

  body · string · optional

- email

  body · string · optional

- contact_email

  body · string · optional Where to send the confirmation link, when email is not given.

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/privacy/opt-out' \
  -H 'Content-Type: application/json' \
  -d '{"profile_url":"https://profiles.northwind.example/ada-peeters","email":"ada@northwind.example","contact_email":"ada@northwind.example"}'
```

#### Success

HTTP 202. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "status": "active",
  "message": "The request was accepted."
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 503 internal · request_id in the body · Retryable: no Opt-out requests are not available on this server yet. { "error": { "code": "internal", "message": "Opt-out requests are not available on this server yet.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Confirm the opt-out](https://recruitmcp.com/docs/api#confirmOptOut)

### GET `/v1/privacy/opt-out/confirm`

#### Purpose

Confirm an opt-out

No authentication.

#### Scopes

No key.

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- token

  query · string · required Token from the confirmation email.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/privacy/opt-out/confirm?token=tok_01J0VERIFY0TOKEN0EXAMPLE'
```

#### Success

HTTP 200. Example response.

```
{
  "id": "id_01J0EXAMPLE000000",
  "status": "active",
  "message": "The request was accepted."
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 503 internal · request_id in the body · Retryable: no Opt-out requests are not available on this server yet. { "error": { "code": "internal", "message": "Opt-out requests are not available on this server yet.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [The person is suppressed](https://recruitmcp.com/docs/api#listSuppressions)

## Customers

Sub-workspaces for a platform. Each customer has its own seen register, lists, and caps. Credits stay on the account.

### GET `/v1/customers`

#### Purpose

List customers

Customers are sub-workspaces for platforms that serve their own clients. Each has its own seen register, lists, suppressions, and daily cap; credits come from your account.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- external_id

  query · string · optional Only the customer with this external_id.

- limit

  query · integer · optional How many items to return. Default 25, at most 100.

- cursor

  query · string · optional next_cursor from the previous response. An invalid cursor is 400 invalid_request, message Invalid cursor.

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/customers' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "object": "list",
  "data": [
    {
      "id": "cus_8d1f0a2b3c4d5e6f7a",
      "object": "customer",
      "name": "Northwind Recruiting",
      "external_id": "tenant_42",
      "metadata": {
        "key": "key_01J0APIKEY0EXAMPLE"
      },
      "daily_cap_credits": 100,
      "status": "active",
      "created_at": "2026-10-13T09:00:00Z",
      "updated_at": "2026-10-13T09:00:00Z"
    }
  ],
  "next_cursor": "cur_01J0PAGE0CURSOR0EXAMPLE",
  "has_more": true
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Create a customer workspace](https://recruitmcp.com/docs/api#createCustomer)

### POST `/v1/customers`

#### Purpose

Create a customer

Select it on any request with the RecruitMCP-Customer header, or create a key bound to it.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- name

  body · string · optional Required on create.

- external_id

  body · string | null · optional Your id for this tenant.

- metadata

  body · object · optional

- daily_cap_credits

  body · integer | null · optional Credits this customer may spend per UTC day. Null for no cap.

- status

  body · "active" | "disabled" · optional Update only.

#### Minimal request

Example request.

```
curl -sS -X POST 'https://api.recruitmcp.com/v1/customers' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Northwind Recruiting","external_id":"tenant_42","daily_cap_credits":100}'
```

#### Success

HTTP 201. Example response.

```
{
  "id": "cus_8d1f0a2b3c4d5e6f7a",
  "object": "customer",
  "name": "Northwind Recruiting",
  "external_id": "tenant_42",
  "metadata": {
    "key": "key_01J0APIKEY0EXAMPLE"
  },
  "daily_cap_credits": 100,
  "status": "active",
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 409 conflict · request_id in the body · Retryable: no The resource is in a state that refuses this change. { "error": { "code": "conflict", "message": "The resource is in a state that refuses this change.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the customer](https://recruitmcp.com/docs/api#getCustomer)

### GET `/v1/customers/{customer_id}`

#### Purpose

Get a customer

Scope: \`account:read\`. Acts on the account; keys bound to a customer are refused.

#### Scopes

account:read

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- customer_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X GET 'https://api.recruitmcp.com/v1/customers/cus_01J0CUSTOMER0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 200. Example response.

```
{
  "id": "cus_8d1f0a2b3c4d5e6f7a",
  "object": "customer",
  "name": "Northwind Recruiting",
  "external_id": "tenant_42",
  "metadata": {
    "key": "key_01J0APIKEY0EXAMPLE"
  },
  "daily_cap_credits": 100,
  "status": "active",
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Search inside the customer](https://recruitmcp.com/docs/api#searchPeople)

### PATCH `/v1/customers/{customer_id}`

#### Purpose

Update a customer

A disabled customer refuses every request and its runs stop at their next step.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- customer_id

  path · string · required

- name

  body · string · optional Required on create.

- external_id

  body · string | null · optional Your id for this tenant.

- metadata

  body · object · optional

- daily_cap_credits

  body · integer | null · optional Credits this customer may spend per UTC day. Null for no cap.

- status

  body · "active" | "disabled" · optional Update only.

#### Minimal request

Example request.

```
curl -sS -X PATCH 'https://api.recruitmcp.com/v1/customers/cus_01J0CUSTOMER0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Northwind Recruiting","external_id":"tenant_42","daily_cap_credits":250,"status":"active"}'
```

#### Success

HTTP 200. Example response.

```
{
  "id": "cus_8d1f0a2b3c4d5e6f7a",
  "object": "customer",
  "name": "Northwind Recruiting",
  "external_id": "tenant_42",
  "metadata": {
    "key": "key_01J0APIKEY0EXAMPLE"
  },
  "daily_cap_credits": 100,
  "status": "active",
  "created_at": "2026-10-13T09:00:00Z",
  "updated_at": "2026-10-13T09:00:00Z"
}
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [Read the customer](https://recruitmcp.com/docs/api#getCustomer)

### DELETE `/v1/customers/{customer_id}`

#### Purpose

Delete a customer and its data

Revokes its bound keys and deletes its seen register, lists, and caches. Spend stays on your ledger.

#### Scopes

account:manage

#### Cost, free cases, and preview

Credit cost. Free.

#### Parameters

- customer_id

  path · string · required

#### Minimal request

Example request.

```
curl -sS -X DELETE 'https://api.recruitmcp.com/v1/customers/cus_01J0CUSTOMER0EXAMPLE' \
  -H "Authorization: Bearer $RECRUITMCP_KEY"
```

#### Success

HTTP 204. Example response.

```
HTTP 204 No Content
```

#### Errors

Each error has status, error.code, and error.request_id. The same id is in X-Request-Id. What to do: when Retryable is yes, wait and send the same request again. When it is no, change the request or the account.

- 400 invalid_request · request_id in the body · Retryable: no A field is missing or has a value this endpoint does not accept. { "error": { "code": "invalid_request", "message": "A field is missing or has a value this endpoint does not accept.", "request_id": "req_01J0REQUEST0EXAMPLE" } }
- 404 not_found · request_id in the body · Retryable: no No record with this id exists in the workspace. { "error": { "code": "not_found", "message": "No record with this id exists in the workspace.", "request_id": "req_01J0REQUEST0EXAMPLE" } }

#### Idempotency and retry

This call does not take an Idempotency-Key. A 429 still waits for Retry-After.

#### Side effects and approval

This call does not spend credits and does not need approval.

#### Next step

- [List customers](https://recruitmcp.com/docs/api#listCustomers)

## Shell examples

Create a workspace, look up London, and preview a people search. The preview spends nothing. The same scripts are executed by the repository tests.

```
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="${RECRUITMCP_DOMAIN:-recruitmcp.com}"
BASE="${RECRUITMCP_BASE:-https://api.${DOMAIN}}"
MCP="${RECRUITMCP_MCP:-https://mcp.${DOMAIN}/mcp}"
WS=$(curl -fsS -X POST "$BASE/v1/workspaces" \
  -H 'content-type: application/json' \
  -d '{"name":"Northwind Search","email":"ada@northwind.example"}')
RECRUITMCP_KEY=$(node -e 'const j=JSON.parse(process.argv[1]); if(!String(j.api_key||"").startsWith("rmcp_live_")) process.exit(1); process.stdout.write(j.api_key)' "$WS")

PLACES=$(curl -fsS "$BASE/v1/lookups/locations?q=London,%20United%20Kingdom&prefer_area=true" \
  -H "authorization: Bearer $RECRUITMCP_KEY")
node -e 'const j=JSON.parse(process.argv[1]); if(!/^[0-9]+$/.test(j.data?.[0]?.geo_id||"")) { console.error(j); process.exit(1); }' "$PLACES"
GEO=$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).data[0].geo_id)' "$PLACES")

DRY=$(curl -fsS -X POST "$BASE/v1/people/search?dry_run=true" \
  -H "authorization: Bearer $RECRUITMCP_KEY" \
  -H 'content-type: application/json' \
  -d "$(node -e 'process.stdout.write(JSON.stringify({filters:{current_job_titles:["Backend Engineer"],geo_ids:[process.argv[1]],years_of_experience_ids:["4"]},exclude_seen:true}))' "$GEO")")
node -e 'const j=JSON.parse(process.argv[1]); const e=j.estimate||{}; if(j.dry_run!==true||e.requires_confirm!==false||e.credits_min!==0||j.cost?.credits!==0||typeof e.credits_max!=="number"||typeof e.balance_credits!=="number"||typeof e.sufficient!=="boolean"||!("warning" in e)){console.error(j); process.exit(1)}' "$DRY"
echo "$DRY"
```

### Idempotency

Idempotency-Key is required unless dry_run=true. Reuse a key only to retry the identical request. A new or changed request needs a new key. The same key with a different body returns 409 idempotency_key_reused. A second call with the same key waits for the first and replays it. A replay has Idempotent-Replayed: true, X-Credits-Charged: 0, and replayed: true with cost.credits 0 in the body. This script reuses one key on purpose, to show the replay.

```
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="${RECRUITMCP_DOMAIN:-recruitmcp.com}"
BASE="${RECRUITMCP_BASE:-https://api.${DOMAIN}}"
MCP="${RECRUITMCP_MCP:-https://mcp.${DOMAIN}/mcp}"
WS=$(curl -fsS -X POST "$BASE/v1/workspaces" -H 'content-type: application/json' -d '{"name":"Northwind Search","email":"ada@northwind.example"}')
RECRUITMCP_KEY=$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).api_key)' "$WS")
BODY='{"filters":{"current_job_titles":["Backend Engineer"]}}'
ONE=$(curl -fsS -X POST "$BASE/v1/people/search" -H "authorization: Bearer $RECRUITMCP_KEY" -H 'content-type: application/json' -H 'idempotency-key: search-backend' -d "$BODY")
BAL1=$(curl -fsS "$BASE/v1/workspace" -H "authorization: Bearer $RECRUITMCP_KEY")
TWO=$(curl -fsS -X POST "$BASE/v1/people/search" -H "authorization: Bearer $RECRUITMCP_KEY" -H 'content-type: application/json' -H 'idempotency-key: search-backend' -d "$BODY")
BAL2=$(curl -fsS "$BASE/v1/workspace" -H "authorization: Bearer $RECRUITMCP_KEY")
node -e 'const [one,two,a,b]=process.argv.slice(1).map((v)=>JSON.parse(v)); if(!one.data||two.replayed!==true||two.cost.credits!==0||JSON.stringify(one.data)!==JSON.stringify(two.data)) process.exit(1); if(a.balance_credits!==b.balance_credits) process.exit(2);' "$ONE" "$TWO" "$BAL1" "$BAL2"
```

### A limited key

```
#!/usr/bin/env bash
set -euo pipefail
DOMAIN="${RECRUITMCP_DOMAIN:-recruitmcp.com}"
BASE="${RECRUITMCP_BASE:-https://api.${DOMAIN}}"
MCP="${RECRUITMCP_MCP:-https://mcp.${DOMAIN}/mcp}"
WS=$(curl -fsS -X POST "$BASE/v1/workspaces" -H 'content-type: application/json' -d '{"name":"Northwind Search","email":"ada@northwind.example"}')
RECRUITMCP_KEY=$(node -e 'process.stdout.write(JSON.parse(process.argv[1]).api_key)' "$WS")
CREATED=$(curl -fsS -X POST "$BASE/v1/api-keys" -H "authorization: Bearer $RECRUITMCP_KEY" -H 'content-type: application/json' -d '{"name":"read only","scopes":["sourcing:read"]}')
KID=$(node -e 'const j=JSON.parse(process.argv[1]); if(!j.api_key||!j.scopes?.includes("sourcing:read")||j.scopes.includes("sourcing:spend")) process.exit(1); process.stdout.write(j.id)' "$CREATED")
curl -fsS -X DELETE "$BASE/v1/api-keys/$KID" -H "authorization: Bearer $RECRUITMCP_KEY" -o /dev/null
```

---

Source: https://recruitmcp.com/docs/api
