# LinkedIn Profile Search & Email to Name - No Cookies (`andrew_babo/linkedin-profile-search`) Actor

Reverse-look up a work email to the right person: full name, job title, current company - or search LinkedIn people by keyword, company, location, school. Verified two-way match, blank instead of wrong. No login, no cookies. JSON, CSV, Excel.

- **URL**: https://apify.com/andrew\_babo/linkedin-profile-search.md
- **Developed by:** [Andrew Babo](https://apify.com/andrew_babo) (community)
- **Categories:** Lead generation, Business, SEO tools
- **Stats:** 2,206 total users, 1,092 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## LinkedIn Profile Search & Email to Name Lookup (No Cookies, No Login)

**Turn a work email into a verified person — full name, job title and current company — or search LinkedIn people by keyword, company, location or school.** No LinkedIn account, no cookies, no session sharing, no ban risk on your own profile.

Built for cold outreach, CRM enrichment and lead qualification teams who cannot afford to greet a stranger by the wrong name.

***

### Table of contents

- [What this LinkedIn scraper does](#what-this-linkedin-scraper-does)
- [Benchmark: 25 real work emails](#benchmark-25-real-work-emails-measured-not-estimated)
- [Why "blank" beats "wrong"](#why-blank-beats-wrong)
- [Quick start](#quick-start)
- [Input reference](#input-reference)
- [Output reference](#output-reference)
- [Common recipes](#common-recipes)
- [Use cases](#use-cases)
- [Speed and cost](#speed-and-cost)
- [How the matching works](#how-the-matching-works)
- [Limits and honest caveats](#limits-and-honest-caveats)
- [FAQ](#faq)

***

### What this LinkedIn scraper does

| Mode | You give | You get |
|---|---|---|
| **Reverse email lookup** | `dylan.church@mintselection.com` | Full name, job title, current company, profile URL, location, photo |
| **People search** | `head of growth fintech`, London, Revolut | Matching public profiles with the full public profile data |

Both modes read **public LinkedIn pages only**. Nothing here depends on a logged-in session, so there is nothing to get restricted, banned or rate-limited on your account — because there is no account.

The email mode is the reason this actor exists: most LinkedIn search actors take a name and a company and return a list of *plausible* people. This actor takes the one identifier you actually have after an email-verification pass — the address itself — and resolves it to a single verified identity, or tells you honestly that it cannot.

***

### Benchmark: 25 real work emails (measured, not estimated)

Side-by-side against a leading LinkedIn profile search actor on the same 25 real work emails:

| | This actor | A leading LinkedIn profile search actor |
|---|---|---|
| Correct person from **email only** | **19 / 25** | 0 / 25 (email input is not supported) |
| Correct person given **name + company** | n/a (not needed) | 14 / 25 top result |
| Wrong person returned | **0** | 7 / 25 (top result was a different human) |
| Total time for 25 emails | **47 seconds** | ~16 seconds per single lookup |
| Cost per email | **~$0.003** | ~$0.10 per search |

The 6 remaining emails came back **blank with a reason code** — by design, not by failure.

***

### Why "blank" beats "wrong"

Most email-to-name tools take the first search result with a similar name and attach it to your campaign. That is how "Hi Ruth" ends up in an email to John. This actor scores every candidate on two independent axes:

- **Name axis** — does the name inside the email match the profile name? (`first.last`, `flast`, `firstl`, `firstlast`, initials…)
- **Company axis** — does the email domain match the company the person actually works for? Confirmed on LinkedIn, and when LinkedIn hides the employer, on an external source such as the company team page or a press release.

**Both must agree.** If only one agrees, or two candidates tie, the row comes back empty with a machine-readable reason instead of a guess. Every blank row also carries `rejectedCandidates`, so you can audit the decision instead of trusting it.

***

### Quick start

1. Open the actor and paste one or more work emails into **Emails**.
2. Leave **Mode** on `full` and set the proxy group to **RESIDENTIAL** (already the default).
3. Press **Start**. Each email takes a few seconds; 25 emails at `maxConcurrency: 25` finish in under a minute.
4. Download the results as JSON, CSV or Excel from the **Dataset** tab, or pull them through the API.

Email mode, minimal:

```json
{
  "emails": ["dylan.church@mintselection.com"]
}
```

Keyword mode, minimal:

```json
{
  "search": "head of growth fintech",
  "locations": ["London"],
  "maxItems": 50
}
```

***

### Input reference

| Field | Type | Default | Description |
|---|---|---|---|
| `emails` | array | — | Work emails to reverse-look up. Provide this **or** `search`, not both. Each entry is either a plain address (`"jane@acme.com"`) or an object carrying its own company context: `{ "email": "jane@acme.com", "companyName": "Acme Group", "companyDomain": "acme.com" }`. Per-entry hints win over the run-wide `companyName` / `companyDomain`, so a single run can cover thousands of addresses from different employers. |
| `search` | string | — | Free-text people search query (e.g. `"product manager fintech"`). |
| `locations` | string\[] | — | Narrow keyword search by location (e.g. `["London"]`). |
| `currentCompanies` | string\[] | — | Narrow keyword search by current employer (e.g. `["Revolut"]`). |
| `schools` | string\[] | — | Narrow keyword search by school. |
| `companyName` | string | — | Run-wide employer brand-name hint for email mode (e.g. `"Emerald Carrying Company"`). Used twice: to **find** extra candidates (an additional public search for `"Name" "Company" linkedin` when nothing found so far mentions that company) and to **confirm** a profile whose employer LinkedIn hides from guests. Useful when the email domain is an abbreviation of the company name. Overridden by a per-email hint. |
| `companyDomain` | string | — | Run-wide employer domain hint for email mode. When set, it replaces the domain derived from the email for company matching. Overridden by a per-email hint. |
| `includeRawCandidates` | boolean | `false` | When on, every row also includes `rawCandidates[]` — the full pre-filter candidate list with scores, matched signals and rejection reasons, so you can apply your own acceptance rules. |
| `maxItems` | integer | 100 | Maximum profiles returned in keyword mode. |
| `mode` | `fast` / `full` | `full` | `fast` returns identity fields only; `full` adds experience, education, summary, languages. |
| `maxConcurrency` | integer | 8 | Parallel lookups. 25 is safe and the fastest tested setting. |
| `maxRunTimeSecs` | integer | 3600 | Hard stop for the whole run. |
| `cacheTtlDays` | integer | 14 | Profiles are cached by slug; repeat lookups within the TTL are near-free. Set `0` to always fetch fresh. |
| `proxyConfiguration` | object | RESIDENTIAL | Residential proxies are strongly recommended (see FAQ). |
| `sessionCookies` | string | — | Optional `li_at` cookie if you want to use your own session. Nothing depends on it. |

***

### Output reference

One dataset row per email (email mode) or per profile (keyword mode), exportable to **JSON, CSV, Excel, XML or HTML** and available through the Apify API.

| Field | Meaning |
|---|---|
| `email` | The input email (email mode) |
| `matched` | `true` only when both name and company axes confirm the same profile |
| `confidence` | 0–1 match score. On a blank row (`matched: false`) this is the score of the best candidate that was rejected, so you can see how close the row came |
| `confidenceLabel` | `high` (≥ 0.8), `medium` (≥ 0.6) or `low` — always present, including on blank rows — pick your own acceptance threshold |
| `matchedSignals` | Which verification signals fired, e.g. `["name-in-email", "domain-abbreviation"]` |
| `bestCandidateUrl` | Blank rows only: the profile that scored highest but was not published |
| `reasonCode` | Machine-readable decision (table below) |
| `fullName`, `firstName`, `lastName` | Parsed name |
| `headline`, `currentTitle`, `currentCompany`, `currentCompanyUrl` | Current role |
| `titleSource` | Where the job title came from: `profile` (experience section), `headline` (profile top card), `search-snippet` (public search result line) or `null` when no title could be verified. Titles are never guessed. A headline that reads like a pitch ("Interested in mobile technology and e-Commerce.") is kept in `headline` and **not** promoted to `currentTitle`. |
| `companySource` | Where the employer came from: `profile`, `search-snippet`, `input-hint` (the `companyName` you sent with the email, used only on a verified match) or `null` |
| `companies` | All companies seen on the profile (current + past) |
| `profileUrl` | Canonical LinkedIn profile URL |
| `location`, `country` | Public location |
| `photoUrl`, `followers`, `connections` | Public profile metadata |
| `summary`, `languages`, `experience[]`, `education[]` | `full` mode only |
| `dataQuality` | `full` or `partial` (partial = LinkedIn served an obfuscated guest page) |
| `rejectedCandidates[]` | Candidates that failed verification, each with `confidence`, `confidenceLabel`, `nameScore`, `domainScore`, `matchedSignals` and `rejectedReason` — for auditing blank rows and tuning your own threshold |
| `rawCandidates[]` | Only when `includeRawCandidates` is on: the full pre-filter candidate list with per-candidate scores, `matchedSignals`, `accepted` flag and rejection reason |
| `scrapedAt` | ISO timestamp |

#### Matched signals

`matchedSignals` tells you exactly which evidence confirmed the match:

| Signal | Meaning |
|---|---|
| `name-in-email` | The email local part encodes the person's name (`first.last`, `flast`, …) |
| `domain-website` | The email domain matches the company website on the profile (strongest) |
| `domain-company-name` | The domain matches the company name on the profile |
| `domain-abbreviation` | The domain is an abbreviation of the company name (`emcarry` ↔ Emerald Carrying Company, `ms` ↔ Mint Selection) |
| `domain-past-company` | The domain matches a past employer (weaker) |
| `domain-in-page-text` | The company is masked, but the domain root appears in the visible profile text |
| `company-name-hint` | Your `companyName` input matched the company on the profile |
| `outside-source` | A page outside LinkedIn (company team page, press release) ties this person to the domain |

#### Reason codes

| reasonCode | Meaning |
|---|---|
| `MATCHED` | Name and company both confirm the same profile |
| `AMBIGUOUS` | Two or more candidates are equally plausible |
| `DOMAIN_MISMATCH` | Name matches, but the person does not work at that domain |
| `NAME_MISMATCH` | Nothing with a matching name was found |
| `NOT_FOUND` | No public profile surfaced at all |
| `GENERIC_EMAIL` | Gmail/Outlook address — no company signal exists |
| `ROLE_EMAIL` | `info@`, `sales@` — not a person |
| `TIME_LIMIT` | The caller's run-time limit was reached; this row was saved before shutdown and can be retried |

#### Example row

```json
{
  "email": "satya.nadella@microsoft.com",
  "matched": true,
  "confidence": 0.96,
  "reasonCode": "MATCHED",
  "fullName": "Satya Nadella",
  "headline": "Chairman and CEO at Microsoft",
  "currentCompany": "Microsoft",
  "profileUrl": "https://www.linkedin.com/in/satyanadella"
}
```

***

### Common recipes

**Enrich a CRM export.** Export your leads as a CSV with an `email` column, upload it in the input editor (the actor accepts pasted lists), run, then download the dataset as Excel and join it back on the email column.

**Skip non-people cheaply.** Role mailboxes (`info@`, `sales@`) and free-mail addresses are rejected *before* any network traffic, so a dirty list costs almost nothing to clean.

**Verify a cached row is still current.** Set `cacheTtlDays: 0` to bypass the cache and re-read the profile — useful before a big send.

**Audit a blank row.** Open the row's `rejectedCandidates`: you will see every candidate that was considered, its name/company scores, and why it was rejected.

**Call it from your own app.** Use the Apify API or the JavaScript/Python client with `call()`; the dataset items map one-to-one to your input emails.

***

### Use cases

- **Cold email personalisation** — you have a verified email, you need the real name and title before you write the first line.
- **CRM enrichment** — fill missing `name`, `title`, `company` fields on inbound leads.
- **Lead qualification** — confirm the contact still works at the company on the email domain.
- **Recruiting** — search candidates by keyword, company, location or school.
- **Data hygiene** — flag role mailboxes and free-mail addresses before they enter a sequence.

***

### Speed and cost

- 25 emails in **47 seconds** at `maxConcurrency: 25`.
- **~$0.003 per email** including proxy usage, measured on a real run.
- Repeat lookups are close to free: profiles are cached by slug for `cacheTtlDays` (default 14).

***

### How the matching works

1. **Parse the email.** Role mailboxes and free-mail providers are rejected instantly. Personal emails yield candidate name patterns and a company domain.
2. **Discover candidates.** Search engines surface public `/in/` profile URLs for the name; a direct slug guess from the email pattern is tried in parallel.
3. **Read the public page.** Structured data on the public profile gives name, headline, employer, location, experience and education.
4. **Score two axes.** The name pattern must match the profile name, and the email domain must match the employer. If LinkedIn hides the employer on the guest page, the actor looks for an independent confirmation (company team page, press release) before deciding.
5. **Decide.** Both axes agree → `MATCHED`. Anything else → blank row with a reason code and the rejected candidates attached.

***

### Limits and honest caveats

- **Coverage depends on public visibility.** Profiles with no public page, or pages LinkedIn heavily obfuscates for guest traffic, resolve to blank rather than to a guess. In the benchmark that was 6 of 25.
- **Very short API timeouts reduce coverage.** The actor respects the run timeout imposed by the caller and saves unfinished emails with `TIME_LIMIT` before shutdown instead of letting the run end as `TIMED-OUT`. For reliable enrichment, allow at least 60 seconds; large batches should use the default.
- **Very common names** (think `john.smith@apple.com`) may resolve to a real person with that exact name at that exact company who is still the *wrong* person. The two-axis check keeps this rare, but no public-data tool can eliminate it entirely.
- **No email extraction.** Public profiles do not carry reliable email addresses, so this actor never invents them. It goes the other way: email in, identity out.

***

### FAQ

**Do I need a LinkedIn account or cookies?**
No. The actor reads public pages. An optional `li_at` field exists only if you want to use your own session — nothing depends on it.

**Will it return email addresses?**
No. It goes the other way: email in, identity out.

**Why did some rows come back empty?**
Because the two-way check did not pass. Read `reasonCode` and `rejectedCandidates` to see exactly why. An empty row is a deliberate answer, not a failure.

**Which proxies should I use?**
Residential. LinkedIn serves partially hidden ("\*\*\*\*\*") guest pages to datacenter addresses; `dataQuality` reports `partial` when that happens, and the actor never publishes an obfuscated field as if it were text.

**Is scraping public LinkedIn data legal?**
Publicly accessible pages are generally scrapable, but you are responsible for how you use the data, including GDPR/CCPA duties for personal data. This actor collects no private, logged-in-only data.

**Can I run it on a schedule or from my own app?**
Yes — Apify Schedules, the API, or any of the JavaScript/Python clients.

***

### For AI agents (MCP-ready)

This actor is designed to be called by AI agents, not just humans. It works out of the box with the [Apify MCP Server](https://mcp.apify.com) — add it to Claude Desktop, Cursor or any MCP client and the agent can resolve emails and search people on its own:

```json
{
  "mcpServers": {
    "linkedin-profile-search": {
      "url": "https://mcp.apify.com/?actors=andrew_babo/linkedin-profile-search",
      "headers": { "Authorization": "Bearer <YOUR_APIFY_TOKEN>" }
    }
  }
}
```

#### Agent skill (paste into your agent's instructions)

```text
You can resolve work emails to verified people using the
"linkedin-profile-search" tool.

WHEN TO USE
- You have a work email and need the person's full name, job title,
  current company and LinkedIn URL.
- You need to find people by job title / company / location / school.

HOW TO CALL
- Email lookup: { "emails": ["jane@acme.com"] }
  Better accuracy with context:
  { "emails": [{ "email": "jane@acme.com", "companyName": "Acme Group" }] }
- People search: { "search": "head of growth fintech",
                   "currentCompanies": ["Revolut"], "locations": ["London"] }

OUTPUT CONTRACT
- matched: true  → fullName, jobTitle, currentCompany, profileUrl are
  verified on TWO independent axes (name pattern + company). Safe to use
  in a cold email greeting.
- matched: false → do NOT guess or fill a name. Read reasonCode:
  NO_CANDIDATE (no public profile found), NO_COMPANY_EVIDENCE (person
  exists but employer could not be confirmed), ROLE_OR_FUNCTIONAL_EMAIL,
  FREE_EMAIL_PROVIDER, BELOW_MIN_CONFIDENCE. Report the reason, move on.
- NEVER treat an empty row as an error. A blank row is a correct answer.
- dataQuality: "partial" means LinkedIn hid some fields from guest
  traffic; hidden fields are never fabricated.

COST
- ~$0.003 per email, 25 emails in under a minute. Cache makes repeat
  lookups nearly free for cacheTtlDays (default 14).
```

# Actor input Schema

## `emails` (type: `array`):

Turn a verified work email into the person behind it: full name, job title, current company. A result is only returned when the name in the email and the email domain both point at the same public profile. Each entry can be a plain address ("jane@acme.com") or an object with its own company context: {"email": "jane@acme.com", "companyName": "Acme Group", "companyDomain": "acme.com"} - so one run can cover thousands of addresses from different employers.

## `search` (type: `string`):

Find people by job title, skill or name, e.g. "head of growth fintech". Leave empty when you only use email lookup.

## `currentCompanies` (type: `array`):

Restrict the keyword search to people at these companies.

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

Restrict the keyword search to these locations, e.g. "London", "Singapore".

## `schools` (type: `array`):

Restrict the keyword search to alumni of these schools.

## `companyName` (type: `string`):

The employer's brand name, if you already know it (e.g. from your CRM). Helps when the email domain is an abbreviation of the company name, like emcarry.com.au for Emerald Carrying Company.

## `companyDomain` (type: `string`):

The employer's main domain, if you already know it. When set, it replaces the domain taken from the email address for company matching.

## `includeRawCandidates` (type: `boolean`):

Also return every candidate considered for each email, with its scores, matched signals and rejection reason, so you can apply your own acceptance rules.

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

Short = name, headline and company only (cheapest). Full = complete public profile with experience and education.

## `minConfidence` (type: `integer`):

How certain the email match has to be before the person is returned, as a percentage. Lower it to get more matches, raise it to be stricter. Anything below the bar is returned blank with a reason.

## `maxCandidatesPerEmail` (type: `integer`):

How many possible profiles to weigh per email before deciding. Higher finds more, costs more.

## `maxItems` (type: `integer`):

Maximum number of rows to return.

## `maxConcurrency` (type: `integer`):

How many lookups run in parallel.

## `cacheTtlDays` (type: `integer`):

Repeat lookups of the same person are served from cache and cost nothing. Set 0 to disable.

## `maxRunTimeSecs` (type: `integer`):

Stop the run after this many seconds.

## `proxyConfiguration` (type: `object`):

Residential proxies are strongly recommended: LinkedIn serves partially hidden pages to datacenter addresses.

## `liAtCookie` (type: `string`):

Not required. Only use it if you want richer pages from your own logged-in session.

## Actor input object example

```json
{
  "emails": [
    "dylan.church@mintselection.com"
  ],
  "includeRawCandidates": false,
  "mode": "full",
  "minConfidence": 70,
  "maxCandidatesPerEmail": 5,
  "maxItems": 100,
  "maxConcurrency": 8,
  "cacheTtlDays": 14,
  "maxRunTimeSecs": 3600,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `RUN_SUMMARY` (type: `string`):

No description

# 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 = {
    "emails": [
        "dylan.church@mintselection.com"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("andrew_babo/linkedin-profile-search").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 = { "emails": ["dylan.church@mintselection.com"] }

# Run the Actor and wait for it to finish
run = client.actor("andrew_babo/linkedin-profile-search").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 '{
  "emails": [
    "dylan.church@mintselection.com"
  ]
}' |
apify call andrew_babo/linkedin-profile-search --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,andrew_babo/linkedin-profile-search"
        }
    }
}
```

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/KGWgXNRD1aYhDYDeA/builds/WTTRceNVj06xNwTOx/openapi.json
