---
title: "Look up email and phone"
method: POST
path: "/v1/people/{profile_id}/contacts"
tags: ["Contacts"]
---

# Look up email and phone

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

## Request

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

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:
```bash
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
```

Scope: `contacts:spend`.

## Path parameters

- `profile_id` string, required

## Query parameters

- `dry_run` boolean

## Headers

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

## Request body

- ContactLookupRequest
  - `prefer` 'personal' | 'work': Email to prefer. personal, the default, ranks personal, then unlabeled, then work. work reverses that order. Phones still prefer mobile.
  - `dry_run` boolean: When true, preview the cost and change nothing. Nothing is charged. Same as the dry_run query.
  - `confirm` boolean: Deprecated. Accepted and ignored. An authorized lookup runs without it.
  - `confirm_token` string: From approval_required. Required only when the estimate is above the key threshold.

## Response `200`

200 when everything is already stored: a ContactsLookup at once. With ?dry_run=true, a Preview; nothing is charged or sent.

- union
  - ContactsLookup
    - `data` Contacts[], required
      - `profile_id` string, required
      - `canonical_id` string: Search id for this person when both profile ids are known.
      - `contacts` object, required
        - `status` 'ready' | 'not_found' | 'pending' | 'expired', required
        - `emails` object[], required
          - `value` string
          - `type` 'personal' | 'work', nullable: work or personal when the lookup provides it. Null when it does not.
          - `line` 'mobile' | 'work_phone' | 'work' | 'personal': Provider line. mobile and work_phone stay on phones.
          - `confidence` number, nullable
          - `flagged` boolean: True when a plausibility check says this entry is clearly not this person.
          - `flag_reason` string: Short reason, at most 12 words. Present when flagged is true.
        - `phones` object[], required
          - `value` string
          - `type` 'personal' | 'work'
          - `line` 'mobile' | 'work_phone' | 'work' | 'personal': Provider line. mobile and work_phone stay on phones.
          - `confidence` number, nullable
          - `flagged` boolean: True when a plausibility check says this entry is clearly not this person.
          - `flag_reason` string: Short reason, at most 12 words. Present when flagged is true.
        - `best_email` object, nullable, required
          - `value` string, required
          - `type` 'personal' | 'work', nullable, required: work or personal when the lookup provides it. Null when it does not.
          - `line` 'mobile' | 'work_phone' | 'work' | 'personal': Provider line. mobile and work_phone stay on phones.
          - `confidence` number, nullable, required
          - `reason` 'only_candidate' | 'mobile' | 'highest_rating' | 'personal_preferred' | 'work_preferred' | 'matches_employer_domain' | 'llm_plausibility', required: Why this address or number was chosen. llm_plausibility means a flagged entry changed the pick.
        - `best_phone` object, nullable, required
          - `value` string, required
          - `type` 'personal' | 'work', nullable, required: work or personal when the lookup provides it. Null when it does not.
          - `line` 'mobile' | 'work_phone' | 'work' | 'personal': Provider line. mobile and work_phone stay on phones.
          - `confidence` number, nullable, required
          - `reason` 'only_candidate' | 'mobile' | 'highest_rating' | 'personal_preferred' | 'work_preferred' | 'matches_employer_domain' | 'llm_plausibility', required: Why this address or number was chosen. llm_plausibility means a flagged entry changed the pick.
        - `expires_at` string, date-time, nullable
      - `credits` number: Credits charged for this person on the lookup. 0 on a miss, a cache hit, a pending lookup, and when this response did not itself charge.
      - `cost` Cost
        - `credits` integer, required: Credits charged by this call. Whole credits. 0 on an idempotent replay, where the body also has replayed: true and the header Idempotent-Replayed: true.
        - `cached_items` integer: Items read from cache. On a replay, the value of the first response.
        - `live_items` integer: Items fetched live by this call. 0 on a replay.
        - `upstream_calls` integer: Index calls made by this call. 0 on a replay.
    - `cost` Cost, required
      - `credits` integer, required: Credits charged by this call. Whole credits. 0 on an idempotent replay, where the body also has replayed: true and the header Idempotent-Replayed: true.
      - `cached_items` integer: Items read from cache. On a replay, the value of the first response.
      - `live_items` integer: Items fetched live by this call. 0 on a replay.
      - `upstream_calls` integer: Index calls made by this call. 0 on a replay.
    - `status` 'pending' | 'completed': pending while the lookup is running. completed when the job poll has the stored contacts.
    - `request_id` string, nullable: Id of the running lookup.
    - `job_id` string: Set on HTTP 202. Poll GET /v1/contacts/jobs/{job_id}.
    - `poll_url` string: Path of GET /v1/contacts/jobs/{job_id}. Requires contacts:spend.
    - `canonical_id` string, nullable: Search id when one person was requested.
    - `charged_this_request` number: Credits charged for the lookup this body describes. Matches cost.credits. On a 202 and on a pending poll it is 0. On a completed poll it is what the lookup that started the job was charged at completion, the same as credits_charged; the poll itself charges nothing. 0 on an idempotent replay, where cost.credits is also 0.
    - `credits_charged` number: Set on a contact job (HTTP 202 and GET /v1/contacts/jobs/{job_id}). Credits the ledger charged for this job: 0 while pending, then the same number as cost.credits.
  - Preview: Returned when dry_run=true. Nothing was spent or changed. Search, profile, company, and contact previews set dry_run true and requires_confirm false.
    - `dry_run` true, required: True on this preview. Nothing was charged and nothing was changed.
    - `estimate` Estimate, required
      - `credits_min` integer, required: Lowest expected spend. Whole credits.
      - `credits_max` integer, required: Ceiling. Spend never passes it. Whole credits.
      - `expected_pages` integer
      - `expected_profile_fetches` integer
      - `skipped_pages_from_checkpoints` integer: Pages already paid for in an earlier search. Not charged again.
      - `cached_items` integer: Items served from cache, free.
      - `balance_credits` integer, nullable: Account balance. A customer-bound key gets that customer's remaining daily cap, or null when the customer has no cap.
      - `sufficient` boolean: True when the visible balance covers credits_max, or when that balance is hidden. False when a live call can return 402 insufficient_credits.
      - `warning` string, nullable: Null when sufficient is true. Otherwise the live call can return 402 insufficient_credits.
      - `requires_confirm` boolean, required: False on a search, profile, company, or contact preview. True when the call still needs an explicit confirm.
    - `cost` Cost
      - `credits` integer, required: Credits charged by this call. Whole credits. 0 on an idempotent replay, where the body also has replayed: true and the header Idempotent-Replayed: true.
      - `cached_items` integer: Items read from cache. On a replay, the value of the first response.
      - `live_items` integer: Items fetched live by this call. 0 on a replay.
      - `upstream_calls` integer: Index calls made by this call. 0 on a replay.

## Other responses

- `202`: Live response, usually. The work is queued: the body has job_id, poll_url, and status. Nothing is charged yet. Poll GET /v1/contacts/jobs/{job_id} every few seconds until status is completed.
- `400`: `idempotency_key_required` (do not retry): This route spends credits or sends a message. Send an Idempotency-Key header, or dry_run=true for a preview. `invalid_id` (do not retry): profile_id is not a valid person id. `invalid_request` (do not retry): A field is missing or has a value this endpoint does not accept. `unsupported_version` (do not retry): RecruitMCP-Version is not a supported version.
- `401`: `unauthorized` (do not retry): Missing bearer API key.
- `402`: `daily_credit_cap_reached` (do not retry): This key's daily credit cap is reached. `insufficient_credits` (do not retry): Not enough credits for this operation.
- `403`: `forbidden` (do not retry): This customer is disabled. `insufficient_scope` (do not retry): This key does not include the scope this endpoint requires.
- `404`: `not_found` (do not retry): Fetch the profile before asking for contacts. `suppressed` (do not retry): This profile is suppressed. Contact lookup is free.
- `409`: `approval_required` (do not retry): This call is above the key's approval threshold. Nothing was charged. Send confirm_token to run it. `confirm_token_used` (do not retry): This confirm_token was already used. Ask for a new preview to get another. `idempotency_in_progress` (retry): A request with this Idempotency-Key is still running. Retry after it finishes. `idempotency_key_reused` (do not retry): Idempotency-Key was already used with a different request.
- `429`: `rate_limited` (retry): Per-key rate limit exceeded.
- `503`: `contacts_unavailable` (do not retry): Contact lookup is not available on this server. Nothing was looked up and nothing was charged. `upstream_busy` (retry): Contact lookups are not configured on this server.

## Changes

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

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

---

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