# LinkedIn Profile Scraper — Public Profile Data by URL (`accountable_eel/linkedin-profile-lookup`) Actor

LinkedIn profile scraper with no cookies and no account. Give it public LinkedIn profile links and get one flat row each: name, headline, location, current title and company, roles with dates, schools, follower and connection counts, and the photo. Nothing found, nothing charged.

- **URL**: https://apify.com/accountable\_eel/linkedin-profile-lookup.md
- **Developed by:** [Adrian Voss](https://apify.com/accountable_eel) (community)
- **Categories:** Lead generation, Social media
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $6.08 / 1,000 profile founds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## LinkedIn Profile Lookup: LinkedIn Profile Scraper, No Cookies

Give this actor a list of public LinkedIn profile links and it fetches each one the way a
signed-out browser does, then hands you a flat row per person: name, headline, location, current
job title and company, the current roles the page lists with their start and end years, schools
with years, follower and connection counts, and the profile photo. No login. No cookies. No
`li_at` session token to paste in, no account of yours at risk, and nothing read from behind
LinkedIn's sign-in wall.

The data comes from the `schema.org` Person block LinkedIn itself publishes in the page source of
every public profile, so a row is the profile's own structured description of itself rather than a
guess assembled from scraped text. When LinkedIn decides a request should sign in first, the actor
says so in the row's `status` and charges you nothing.

### Who it's for

Recruiters and sourcers who already have a shortlist of profile links and need them as a table
instead of forty browser tabs. Sales teams who capture a LinkedIn URL on a form or in a CRM field
and want the name, title, company and location filled in behind it before the first email goes out.
Founders checking that an inbound lead's title still matches what their CRM said six months ago.
And anyone building an agent or a workflow that has a profile link and needs the person behind it
in a shape a script can read, without keeping a logged-in LinkedIn session alive somewhere.

If you do not have the profile links yet, this is the wrong tool: it looks up profiles you already
name, it does not search LinkedIn for people matching a description.

### Why this one

- **No cookies, no session token, no account.** Every request is the plain public page. You are
  never asked to hand over a `li_at` cookie, and no LinkedIn account of yours can be restricted
  because of a run here.
- **Read from LinkedIn's own structured data.** The row is built from the `schema.org` Person
  block in the page source, not from CSS selectors chasing a design that changes monthly. Field
  names stay put, which is what an agent or a Clay column needs.
- **It tells you when the page was reduced.** LinkedIn often answers with a trimmed version of the
  profile in which job titles and past employers are replaced by rows of asterisks. Those never
  reach your table: the fields come back empty and the row is flagged `masked: true`, so you can
  filter or re-run instead of importing placeholder text into a CRM. See the note under "What you
  get" for how often that happens, measured rather than guessed.
- **A block is a labelled miss, not a silent hole.** If LinkedIn asks the request to sign in, the
  row says `status: BLOCKED` with a plain explanation and costs nothing. A profile that no longer
  exists says `NOT_FOUND`. A company page pasted by mistake says `BAD_FORMAT`.
- **Never charged for a miss.** Billing fires only on a row where a real profile came back.
- **Residential proxy by default.** LinkedIn's sign-in wall is driven by the reputation of the IP
  asking, so the actor ships with a residential proxy already selected, and retries a blocked
  request on a fresh address rather than handing you the failure.

### What you get

One row per profile you submit. Every row starts with the same five fields, then adds the profile
fields you selected in "Which columns do you want?" (all of them by default).

| Field | Type | Description |
|---|---|---|
| `query` | text | The profile you submitted, echoed back |
| `found` | boolean | `true` when a public profile was read |
| `status` | text | `OK` on a hit, or `NOT_FOUND`, `BLOCKED`, `BAD_FORMAT`, `REQUEST_FAILED` on a miss |
| `message` | text | Plain-English reason for a miss, present only when `found` is `false` |
| `scrapedAt` | ISO 8601 datetime | When the page was fetched |
| `profileUrl` | link | Canonical profile link, `https://www.linkedin.com/in/<public id>` |
| `publicId` | text | The part of the link after `/in/`, LinkedIn's stable public identifier |
| `fullName` | text | Name as the public page shows it |
| `headline` | text | The one-line headline under the name |
| `location` | text | Location as shown, e.g. `Seattle, Washington, United States` |
| `currentTitle` | text | Job title of the first current role listed |
| `currentCompany` | text | Employer for that role |
| `currentCompanyUrl` | link | That employer's LinkedIn company page |
| `experience` | array | Current roles the public page lists, each `{title, company, companyUrl, location, startDate, endDate}` |
| `education` | array | Schools the public page lists, each `{school, degree, startDate, endDate}` |
| `followers` | number | Exact follower count, taken from the page's structured data rather than the rounded "41M" the top card prints |
| `connections` | text | Connection count as LinkedIn displays it, e.g. `500+`, which is why it is text and not a number |
| `profileImageUrl` | image | Profile photo on LinkedIn's media CDN |
| `source` | text | `jsonld` when the row came from the page's structured-data block, `html` when it was assembled from the top card instead |
| `masked` | boolean | `true` when LinkedIn served the reduced page and hid some titles or employers |

Three honest limits on that table, stated here rather than left for you to discover in the data.
`education[].degree` is null, because the logged-out page does not publish degree or field of study
at all. `experience` covers the roles the public page presents as current; full career history is
shown only to signed-in members and is not fetched. And there is no skills column, because no
Skills section appeared in any of the fifteen public profile fetches this actor was built and
tested against, so offering one would mean shipping a column that is always empty.

The bigger caveat is `masked`. LinkedIn decides per request whether to serve the complete profile
page or a reduced one, and across those same fifteen fetches, of thirteen different profiles, on
2026-09-08, the complete page came back four times. That split did not track the route (a plain
home connection got it 2 of 7 times, a residential proxy 1 of 6) and it did not track the profile
either: the same profile served both versions inside fifteen minutes. On a reduced page the name,
location, current employer, follower and connection counts, schools and photo still come through;
the headline and job titles do not, and the row is flagged `masked: true` so you can see exactly
which rows those are. Plan for headline and `currentTitle` to be present on a minority of rows,
not on all of them.

### Pricing

$8 per 1,000 profiles, plus a $0.00005 start fee. Misses (`found:false`) are never charged.

A run of 1,000 profiles costs about **$8** if every one comes back, less when some are blocked or
gone, and less again on a paid Apify plan. Compare that with a per-seat enrichment platform billing
$0.08 to $0.40 per enriched row on top of a licence, or with LinkedIn profile actors listed at
$100 per 1,000. Rows that come back `found: false`, for any reason, are free.

### Use it from Clay, n8n, Make, or an AI agent

This actor runs synchronously over plain HTTP — call it directly from a script, a workflow tool, or an AI agent, no Apify Console needed once you have an API token.

```bash
curl "https://api.apify.com/v2/acts/accountable_eel~linkedin-profile-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"profileUrls":["https://www.linkedin.com/in/williamhgates","satyanadella"]}'
```

**n8n.** Add an HTTP Request node: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~linkedin-profile-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body Content Type `JSON`, JSON Body `{"profileUrls":["https://www.linkedin.com/in/williamhgates","satyanadella"]}` (swap in an expression from an earlier node for a real value).

**Clay.** Add an "HTTP API" column: Method `POST`, URL `https://api.apify.com/v2/acts/accountable_eel~linkedin-profile-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>`, Body `{"profileUrls":["{{profile}}"]}`, mapping the row's profile into the `profileUrls` array.

**MCP.** In Claude, Cursor, or any MCP client with the Apify MCP server, ask for "LinkedIn Profile Scraper, No Cookies | Apify" — the agent will find and run this actor.

### How to use

1. **In the Apify Console.** Open the actor page and click **Start** — the `profileUrls` field is already pre-filled with a working example. Results land in the run's dataset as soon as each item is found.
2. **Via the API.** Call it directly with a POST request — no Console needed once you have an API token:
   ```bash
   curl "https://api.apify.com/v2/acts/accountable_eel~linkedin-profile-lookup/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
     -X POST \
     -H "Content-Type: application/json" \
     -d '{"profileUrls":["https://www.linkedin.com/in/williamhgates","satyanadella"]}'
   ```
3. **On a schedule.** Save this actor as an Apify **Task** with the input you want, then add a **Schedule** (hourly, daily, weekly) so it runs on its own — no server of your own required.

1) Open the Input tab and paste your profile links into "LinkedIn profiles to look up", one per
   line. A full link, a bare `linkedin.com/in/...`, or just the identifier after `/in/` all work.
2) Leave "Try it first" on for the first run. It stops after 5 profiles so you can see the shape of
   the output before spending on the whole list.
3) Leave the proxy on residential. It is the default, and it is the single biggest factor in how
   many of your profiles come back rather than blocked.
4) Keep "Max concurrency" low, 2 to 3, on a long list. Speed here buys blocks, not throughput.
5) Optionally narrow "Which columns do you want?", or use "Only keep rows containing" and "Skip
   rows containing" to filter by keyword, for example to keep only profiles whose row mentions a
   city or a job title.
6) Turn "Try it first" off and press Start for the full list.

### Input

```json
{
  "profileUrls": [
    "https://www.linkedin.com/in/williamhgates",
    "satyanadella"
  ]
}
```

One profile per line. Paste the full profile link, or just the part after /in/. Accepted formats: https://www.linkedin.com/in/williamhgates, linkedin.com/in/williamhgates, williamhgates.

### Output

One row per profile submitted:

| query | found | status | profileUrl | publicId | fullName | headline | location | currentTitle | currentCompany | currentCompanyUrl | experience | education | followers | connections | profileImageUrl | source | masked | scrapedAt |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| williamhgates | true | OK | https://www.linkedin.com/in/williamhgates | williamhgates | Bill Gates | Chair, Gates Foundation and Founder, Breakthrough Energy | Seattle, Washington, United States | Co-chair | Gates Foundation | https://www.linkedin.com/company/gates-foundation | \[{"title":"Co-chair","company":"Gates Foundation","companyUrl":"https://www.linkedin.com/company/gates-foundation","location":null,"startDate":"2000","endDate":null},{"title":"Founder","company":"Breakthrough Energy","companyUrl":"https://www.linkedin.com/company/breakthrough-energy","location":null,"startDate":"2015","endDate":null},{"title":"Co-founder","company":"Microsoft","companyUrl":"https://www.linkedin.com/company/microsoft","location":null,"startDate":"1975","endDate":null}] | \[{"school":"Harvard University","degree":null,"startDate":"1973","endDate":"1975"}] | 40632099 | 8 | https://media.licdn.com/dms/image/v2/D5603AQF-RYZP55jmXA/profile-displayphoto-shrink\_200\_200/B56ZRi8g.aGsAY-/0/1736826818802 | jsonld | false | 2026-09-08T17:37:29.066Z |

A profile that could not be read comes back as a row with `"found": false`, a `status`, and a
`message` explaining which of the four reasons it was, and is never charged.

### Tips

- Run a handful of profiles you know by heart first. It is the fastest way to confirm the fields
  land where you expect before you point a long list at it.
- Filter on `masked: true` rather than trusting an empty `currentTitle`. A masked row is LinkedIn
  trimming the page for that request, not the member leaving the field blank. If the headline
  matters to you, treat a masked row as "ask again later", not as "this person has no headline".
- `publicId` is the join key, not `fullName`. People change their display name and their headline;
  the identifier after `/in/` is what stays the same.
- A wave of `BLOCKED` rows means the addresses you are running from are being asked to sign in.
  Lower "Max concurrency", leave residential proxy on, and re-run the blocked rows later. They cost
  you nothing the first time.
- Store `followers` as a number and `connections` as text. LinkedIn caps the connection display at
  `500+`, so forcing it into a numeric column silently loses the distinction between 500 and 30,000.

### vs. alternatives

| | What it costs | What you get | Trade-off |
|---|---|---|---|
| **This actor** (`linkedin-profile-lookup`) | $0.008 per profile found at the FREE tier, less on paid tiers, $0.00005 actor start, nothing for a miss | One flat row per public profile: name, location, current company, current roles with dates, schools with dates, follower and connection counts, photo, and the headline and job title on the rows LinkedIn serves in full | Public profile pages only. Current roles, not full career history; no degree, field of study or skills, because the logged-out page does not publish them; and the headline and job title arrive on a minority of rows, flagged by `masked`. It looks up profiles you name; it does not search for people. |
| **HarvestAPI's LinkedIn profile actors** | `linkedin-profile-search` is listed at $100 per 1,000 profiles | A whole family: profile lookup, people search, company and post coverage, and the category's most-used profile scraper at a 4.77 rating | If you need to *find* people by criteria rather than look up links you already hold, that is a search product and this is not one. |
| **Clay** | $0.08 to $0.40 per enriched row in credits, on top of a seat | A whole enrichment workspace: waterfalls across dozens of providers, plus the table and the sequencing around it | If you want one place that does everything and you are not counting rows, that is Clay. This is one column, priced per column, callable *from* Clay via its HTTP step. |
| Doing it yourself | Your time, plus residential proxy spend, plus keeping up with a sign-in wall that moves | The same fields | What this absorbs: the structured-data parsing, the reduced-page detection so asterisks never reach your table, the block-versus-missing-versus-bad-input distinction, and retrying a blocked address instead of failing the row. |

Prices for third-party tools are their published list prices as of September 2026 and are not
tracked here. Check the vendor before relying on the comparison.

### FAQ

**Do I need a LinkedIn account, or to paste in a cookie?**
No. That is the point of this actor. It reads the public profile page exactly as a signed-out
browser gets it. You are never asked for credentials, a session cookie or a `li_at` token, and no
account of yours is put at risk by a run.

**Why did a row come back with `found: false`?**
One of four reasons, and `status` says which. `BLOCKED`: LinkedIn asked that request to sign in
instead of serving the page. `NOT_FOUND`: no public profile exists at that address any more, or the
member has turned public visibility off. `BAD_FORMAT`: the input was not a member profile, most
often a `/company/` page pasted by mistake. `REQUEST_FAILED`: the request did not complete after
retries. None of the four is charged.

**What is `masked: true`?**
LinkedIn answers some requests with a reduced version of the profile in which job titles and past
employers are replaced by rows of asterisks. Rather than pass that through, the actor leaves those
fields empty and sets `masked: true`. Name, location, current employer, follower and connection
counts, schools and photo still come through, so the row is worth having. Expect most rows to be
flagged this way: on the fifteen fetches behind this build, the complete page came back four times.
The decision looks like a per-request one on LinkedIn's side rather than a property of the profile
or of where you run from, so re-running a masked row later is worth a try, and it costs you a
second charge for that profile.

**Why is `education[].degree` null?**
Because the logged-out profile page does not publish it. Degree and field of study are shown to
signed-in members only. The field is kept in the row so the output shape never changes between
runs, but it is left null rather than filled from a guess.

**Why is there no skills column?**
Because the logged-out page has no Skills section. None of the fifteen public profile fetches this
actor was built against carried one, in either version of the page, so a skills column would be
empty on every row. It is better to say that here than to ship the column.

**Does it return full career history?**
No. The public page shows the roles a member presents as current, and that is what `experience`
contains. Everything earlier is behind the sign-in wall, and this actor does not go there.

**Am I charged for a miss?**
No. Billing fires only when a real profile was read. Blocked rows, missing profiles and malformed
input all produce a row explaining themselves, unless you turn on "Hide rows with no result", and
none of them cost anything.

**Do I need to configure a proxy?**
It is already configured. Residential proxy is the shipped default because LinkedIn's sign-in wall
keys on the reputation of the address asking. You can change the group in the Advanced section, but
lowering it is the most common reason a list comes back mostly blocked.

**Is this a live fetch or a stored database?**
Live. Every run fetches the current page. Nothing is stored between runs, so a member who changed
jobs yesterday shows the new one today.

**Can an AI agent call this directly?**
Yes. It is on the Apify MCP server, so an agent in Claude, Cursor or any other MCP client can find
and run it by name, and the REST endpoint above works from any script or workflow tool.

### Personal data, and your responsibility

This actor reads public LinkedIn profile pages, which are personal data about identifiable people.
It reads nothing behind the sign-in wall and it holds nothing between runs: each row is fetched
live and written to your own dataset, which only you control.

Whether you may collect, store and use that data is a question about you and your purpose, not
about this tool. If you are in the EU or the UK, or your subjects are, the GDPR applies to what you
do next: you need a lawful basis, and duties around transparency, retention, and honouring a
request to be erased. LinkedIn's own User Agreement also restricts automated collection, and it
binds anyone using it under an account. Nothing here is legal advice, and running this actor is not
a determination that your use is lawful. That call, and the compliance work behind it, is yours.

### Related actors

- [LinkedIn Jobs Search Lookup](https://apify.com/accountable_eel/linkedin-jobs-search-lookup) run
  a public LinkedIn job search by keyword and location and get one row per posting.
- [Company Domain Enrichment](https://apify.com/accountable_eel/company-domain-enrichment) turn the
  employer you just found into firmographics: registration, DNS, tech stack, hiring signal.
- [ATS Jobs Unified Lookup](https://apify.com/accountable_eel/ats-jobs-unified-lookup) check what a
  company is hiring for across the six major applicant tracking systems.

# Actor input Schema

## `profileUrls` (type: `array`):

One profile per line. Paste the full profile link, or just the part after /in/. Accepted formats: https://www.linkedin.com/in/williamhgates, linkedin.com/in/williamhgates, williamhgates. You're only charged for the ones we actually find — a miss costs nothing.

## `testRun` (type: `boolean`):

Turn this on to test your input on a small sample before running the full list. Turn it off to process everything.

## `onlyFound` (type: `boolean`):

Only keep rows where something was actually found. Misses are always free, whether or not you show them here.

## `includeKeywords` (type: `array`):

Optional. Only keep results that mention at least one of these words (e.g. a job title, a city, a product name). Leave empty to keep everything.

## `excludeKeywords` (type: `array`):

Optional. Drop any result that mentions one of these words. Leave empty to skip nothing.

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

Optional. Stop the run once this many results have been found — useful for a quick, cheap sample. Leave blank for no limit.

## `columns` (type: `array`):

Choose which pieces of information to include in each result row. All are included by default.

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

Parallel requests. Keep conservative — this target has no browser fallback, so getting blocked costs more than slow-and-steady.

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

Apify Proxy config. Residential recommended for anti-bot-sensitive targets.

## Actor input object example

```json
{
  "profileUrls": [
    "https://www.linkedin.com/in/williamhgates",
    "satyanadella"
  ],
  "testRun": false,
  "onlyFound": false,
  "includeKeywords": [],
  "excludeKeywords": [],
  "columns": [
    "profileUrl",
    "publicId",
    "fullName",
    "headline",
    "location",
    "currentTitle",
    "currentCompany",
    "currentCompanyUrl",
    "experience",
    "education",
    "followers",
    "connections",
    "profileImageUrl",
    "source",
    "masked"
  ],
  "maxConcurrency": 2,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  }
}
```

# Actor output Schema

## `results` (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 = {
    "profileUrls": [
        "https://www.linkedin.com/in/williamhgates",
        "satyanadella"
    ],
    "includeKeywords": [],
    "excludeKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("accountable_eel/linkedin-profile-lookup").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 = {
    "profileUrls": [
        "https://www.linkedin.com/in/williamhgates",
        "satyanadella",
    ],
    "includeKeywords": [],
    "excludeKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("accountable_eel/linkedin-profile-lookup").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 '{
  "profileUrls": [
    "https://www.linkedin.com/in/williamhgates",
    "satyanadella"
  ],
  "includeKeywords": [],
  "excludeKeywords": []
}' |
apify call accountable_eel/linkedin-profile-lookup --silent --output-dataset

```

## MCP server setup

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

```

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/Zdv4gdFl8M4KbpeD4/builds/a5AwmettvKav5w1Hj/openapi.json
