# Contact Enrichment API - Emails and Phones by Profile (`nabeelbaghoor/contact-enrichment-api`) Actor

Turn LinkedIn profile URLs, email addresses, phone numbers or profile ids into full people records with work emails, personal emails, direct phone numbers, job history, education, skills and social profiles. Or search for people by title, company, location and seniority. Pay per result.

- **URL**: https://apify.com/nabeelbaghoor/contact-enrichment-api.md
- **Developed by:** [Nabeel Hassan](https://apify.com/nabeelbaghoor) (community)
- **Categories:** Business, Developer tools, Lead generation
- **Stats:** 2 total users, 1 monthly users, 57.1% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$15.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Contact Enrichment API - Emails and Phones by Profile

Give it a list of LinkedIn profile URLs and get back work emails, personal emails and direct phone numbers, with the job history, education, skills and social profiles behind each person. Or search for the people first, by title, company, location and seniority, then enrich only the ones worth the credit. One flat row per person.

### What this actor does

- **Takes four kinds of identifier, mixed freely.** LinkedIn profile URLs, email addresses, phone numbers and provider profile ids can all be in the same list. Each line is classified before the request, and profile URLs are reduced to their canonical form first, so tracking parameters, locale prefixes and trailing slashes do not break a match.
- **Runs without a webhook.** This provider delivers enrichment to a callback URL by default, which an actor has no way to host. Every request here uses the provider's own synchronous mode instead, which returns the answers in the response body. That is the documented way to use this API from somewhere that cannot receive a callback.
- **Separates work addresses from personal ones.** The provider returns contacts as a typed list rather than as columns. The actor splits them into a first email, a work email, a personal email and a first phone number, and keeps the complete lists alongside with each entry's subtype and confidence rating. Require a work address and a mixed list becomes one you can actually run a B2B sequence against.
- **Returns the whole person, not just the contact.** Job title, employer, company website, industry, company size and staff count, start date, total years of experience, every past role, education with degrees and years, skills, headline, summary, photo and every social profile the provider holds.
- **Searches for people you do not know yet.** Filter by job title, employer, location, industry, department, seniority band, education, years of experience and free text keywords, with boolean syntax on the text fields. Search returns overviews without contact details, which is the point: it is for deciding who is worth spending a credit on.
- **Handles a scroll token that expires in fifteen seconds.** Search paging uses a token with a very short life. The actor requests the next page before writing the current one to the dataset, because writing first can take longer than the token lives, and it exempts paging from the ordinary request spacing.
- **Batches up to one hundred identifiers per request.** Results are matched back to your input by the identifier the provider echoes rather than by position, because it drops duplicates and failures from the array and every later index would otherwise land on the wrong input.
- **Never charges for a miss.** Only rows carrying a resolved person are billed.

### Input

| Field | What it does |
| --- | --- |
| `mode` | Enrich identifiers, or search for people. |
| `items` | LinkedIn URLs, emails, phones or profile ids to enrich. |
| `currentTitle` | Job title to search for. Boolean syntax supported. |
| `includePastTitles` | Match the title across a whole career. |
| `currentCompany` | Employer to search for. Boolean syntax supported. |
| `includePastCompanies` | Match the company across a whole career, to find alumni. |
| `locations` | Cities, states or countries. |
| `industries` | Industry names as the provider spells them. |
| `departments` | Restrict to these functions. |
| `levels` | Restrict to these seniority bands. |
| `keywords` | Free text across skills, education and the profile. |
| `fullName` | Search by name. |
| `education` | University, faculty or degree to match. |
| `yearsOfExperienceFrom` / `yearsOfExperienceTo` | Career length bounds. |
| `openToWork` | Only people flagged as open to new roles. |
| `requireEmail` | Keep only people with an email address. |
| `requireWorkEmail` | Keep only people with a work address. |
| `requirePhone` | Keep only people with a phone number. |
| `batchSize` | Identifiers per request, up to one hundred. |
| `pageSize` | Profiles per search page, up to one hundred. |
| `requestsPerMinute` | Pace the run under your plan's rate limit. |
| `skipNotFound` | Leave unresolved identifiers out of the dataset. |
| `maxResults` | Hard cap on rows, and therefore on spend and run time. |
| `apiKey` | Your own API key. Stored as a secret. |

### Example output

```json
{
  "found": true,
  "mode": "enrich",
  "query": "https://www.linkedin.com/in/janedoe",
  "queryType": "linkedinUrl",
  "status": "success",
  "uid": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
  "fullName": "Jane Doe",
  "headline": "VP of Engineering at Example",
  "location": "London, United Kingdom",
  "email": "jane.doe@example.com",
  "workEmail": "jane.doe@example.com",
  "personalEmail": "jane@personalmail.example",
  "emails": [
    { "value": "jane.doe@example.com", "subType": "work", "rating": 100, "info": null }
  ],
  "phone": "+442071234567",
  "phones": [
    { "value": "+442071234567", "subType": "work_phone", "rating": 100, "info": null }
  ],
  "jobTitle": "VP of Engineering",
  "organization": "Example",
  "companyWebsite": "https://example.com",
  "companyIndustry": "Computer Software",
  "companyStaffCount": 480,
  "experienceYears": 14,
  "skills": ["Kubernetes", "Distributed Systems"],
  "linkedinUrl": "https://www.linkedin.com/in/janedoe",
  "error": null,
  "raw": { }
}
```

### Frequently asked questions

#### What can I use as an input identifier?

A LinkedIn profile URL, an email address, a phone number, or a profile id from the provider's own database. All four can be mixed in one list and the actor sorts them out before the request. The LinkedIn URL is the most common starting point because it is the identifier people already have: exported from a saved search, a Sales Navigator list or a set of connections. The provider matches on the canonical form of the URL, so the actor strips tracking parameters and locale prefixes before sending it.

#### What is the difference between enrichment and search?

Enrichment starts from someone you can already point at and returns everything about them, including contact details, and it costs a credit per person. Search starts from a description of the people you want, returns profile overviews without contact details, and counts against a daily search quota rather than your credits. The usual sequence is search first to build a shortlist, review it, then enrich only the profile ids that are worth paying for.

#### Does this need a webhook or a callback server?

No. This provider's enrichment route delivers to a callback URL by default, which is fine for a server you control and impossible for an actor. Every request here sets the provider's own synchronous flag, which returns the answers in the response body instead. The tradeoff the provider documents is that synchronous mode uses its internal data only, so contact coverage is somewhat lower than the callback route achieves, in exchange for needing no infrastructure at all.

#### How do I get only work email addresses?

Turn on require a work email address. The provider tags every contact with a subtype, and the actor lifts the first address tagged as a work address into its own column and drops rows that have none. That matters more than it sounds: a personal address on a B2B sequence is both a worse response rate and, in several jurisdictions, a different legal footing than a business address.

#### What does the rating on a contact mean?

It is the provider's confidence in that specific contact detail, and it comes back as either seventy or one hundred. One hundred means the detail is verified. Seventy means it is inferred and worth treating as a best guess. The actor keeps the rating on every entry in the `emails` and `phones` lists rather than flattening it away, so you can decide per address rather than per person.

#### How does the search paging work?

The first search returns a page of profiles plus a scroll token and a request id. Each following page is fetched by posting that token back. The token expires fifteen seconds after it is issued, which is short, so the actor requests the next page before writing the current one to the dataset, and it exempts paging from the ordinary pacing between requests. If a session does expire, the run stops cleanly with everything collected so far rather than failing.

#### How large can one enrichment request be?

One hundred identifiers, which is the provider's own ceiling. The actor defaults to the full hundred. Throughput beyond that is limited per minute rather than per request, and trial accounts are limited far more tightly than paid ones, so lower the requests per minute setting if you are testing on a trial key.

#### Do I need my own API key?

Yes. This actor does not include data access. You use your own API key from SignalHire, which is the provider whose contact data API this actor calls, created on their dashboard. Your own plan, credit balance and terms apply. Paste the key into the `apiKey` field, where it is stored as an Apify secret. The key travels in a request header rather than on the URL. Note that access to the search route is granted by the provider on request, so a new key may enrich before it can search.

#### What happens when an identifier resolves to nobody?

The provider answers with a per item status inside an otherwise successful response, so a miss is a real answer rather than an error. The row is written with `found: false`, the provider's own status and an `error` explaining it, and the run continues. Misses are not charged for. If the provider reports the credit balance is used up, the run stops with everything collected so far rather than working through the rest of the list for nothing.

#### How much does a run cost?

Pricing is pay per result: you are charged for each person resolved into the dataset, and never for misses, for rows dropped by the filters, or for duplicates. Apify platform usage is included in the per-result price. Your own API provider's credit balance is separate and billed by them.

### Keyword map

contact enrichment API, people enrichment API, LinkedIn email finder, LinkedIn profile to email, work email finder, direct dial phone numbers, mobile number finder, reverse email lookup, B2B contact data, people search API, prospect search, lead enrichment, CRM enrichment, sales prospecting data, recruiting sourcing API, candidate sourcing, boolean people search, seniority filter, department filter, open to work search, profile id enrichment

# Actor input Schema

## `mode` (type: `string`):

Enrich resolves people you already have a handle on, by LinkedIn profile URL, email address, phone number or provider profile id, into full profiles with work emails, personal emails and direct phone numbers. Search finds people you do not know yet by job title, company, location, seniority and keywords, and returns profile overviews without contact details.

## `items` (type: `array`):

One identifier per line. LinkedIn profile URLs, email addresses, phone numbers and provider profile ids can be mixed freely in the same list, and each is classified before the request. Profile URLs are reduced to their canonical form first, so tracking parameters and locale prefixes do not matter. Enrichment only.

## `currentTitle` (type: `string`):

Job title to search for. Boolean syntax is supported by the provider, so terms can be combined with AND, OR and NOT and grouped with brackets. Search only. For example ("VP of Engineering" OR "Head of Engineering").

## `includePastTitles` (type: `boolean`):

Match the title against a person's whole career rather than only their current role. Useful when you want people who have done the job before, not only those doing it now. Search only.

## `currentCompany` (type: `string`):

Employer to search for. Boolean syntax is supported, so several companies can be combined with OR. Search only. For example (Stripe OR Shopify).

## `includePastCompanies` (type: `boolean`):

Match the company against a person's whole career rather than only their current employer. This is how you find alumni of a company rather than its current staff. Search only.

## `locations` (type: `array`):

Cities, states or countries, one per line. Several locations are treated as alternatives. Search only.

## `industries` (type: `array`):

Industry names as the provider spells them, one per line, such as Computer Software, Financial Services or Hospital & Health Care. Several are treated as alternatives. Search only.

## `departments` (type: `array`):

Restrict the search to people in these functions. Search only.

## `levels` (type: `array`):

Restrict the search to these seniority bands. Combining C-Level, VP and Head is the usual shape of a decision maker search. Search only.

## `keywords` (type: `string`):

Free text matched across skills, education and the rest of the profile. Boolean syntax is supported. Search only. For example (Kubernetes AND Terraform).

## `fullName` (type: `string`):

Search by a person's name when you know who you are looking for but have no profile URL for them. Search only. For example Jane Doe.

## `education` (type: `string`):

University, faculty or degree to match. Boolean syntax is supported. Search only. For example (Stanford OR MIT).

## `yearsOfExperienceFrom` (type: `integer`):

Only return people with at least this many years of total career experience. Leave empty for no lower bound. Search only.

## `yearsOfExperienceTo` (type: `integer`):

Only return people with at most this many years of total career experience. Leave empty for no upper bound. Search only.

## `openToWork` (type: `boolean`):

Restrict the search to people who have flagged themselves as open to new roles. Useful for recruiting, useless for sales. Search only.

## `requireEmail` (type: `boolean`):

Only keep people the provider returned at least one email address for. Applied after the provider answers, so it reduces dataset noise and Apify spend rather than provider credits.

## `requireWorkEmail` (type: `boolean`):

Only keep people with an address the provider marked as a work address, rather than a personal one. This is the filter that turns a mixed list into one usable for B2B outreach.

## `requirePhone` (type: `boolean`):

Only keep people the provider returned at least one phone number for.

## `batchSize` (type: `integer`):

How many identifiers to resolve in one request, up to the provider's own ceiling of one hundred. Larger batches mean far fewer requests for a big list. Enrichment only.

## `pageSize` (type: `integer`):

How many profiles the provider returns per search page, up to its own ceiling of one hundred. Search only.

## `requestsPerMinute` (type: `integer`):

Pace the run so it stays under whatever rate your plan allows. Trial accounts are limited far more tightly than paid ones. Lower this if the provider starts returning rate limit responses.

## `skipNotFound` (type: `boolean`):

Leave inputs the provider had nothing for out of the dataset. Off by default, because keeping the misses is what lets you see which identifiers resolved and which did not. Misses are never charged for either way.

## `maxResults` (type: `integer`):

Stop after this many rows. This is the cap on both spend and run time.

## `apiKey` (type: `string`):

Your own API key for the contact data API. Sent as a request header, never on the URL. Required for every lookup. Stored as a secret.

## Actor input object example

```json
{
  "mode": "enrich",
  "items": [
    "https://www.linkedin.com/in/janedoe"
  ],
  "includePastTitles": false,
  "includePastCompanies": false,
  "departments": [],
  "levels": [],
  "openToWork": false,
  "requireEmail": false,
  "requireWorkEmail": false,
  "requirePhone": false,
  "batchSize": 100,
  "pageSize": 100,
  "requestsPerMinute": 60,
  "skipNotFound": false,
  "maxResults": 100
}
```

# Actor output Schema

## `results` (type: `string`):

One row per person, with work and personal emails, direct phone numbers, job history, education, skills and social profiles.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "items": [
        "https://www.linkedin.com/in/janedoe"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("nabeelbaghoor/contact-enrichment-api").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "items": ["https://www.linkedin.com/in/janedoe"] }

# Run the Actor and wait for it to finish
run = client.actor("nabeelbaghoor/contact-enrichment-api").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "items": [
    "https://www.linkedin.com/in/janedoe"
  ]
}' |
apify call nabeelbaghoor/contact-enrichment-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,nabeelbaghoor/contact-enrichment-api"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/Zcu7asRaBGMDfX8wM/builds/mffnVaMUaYhhGpMW5/openapi.json
