# LinkedIn Profile Scraper - No Cookies, No Login (`lanternlab318/linkedin-profile`) Actor

- **URL**: https://apify.com/lanternlab318/linkedin-profile.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.99 / 1,000 profiles

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?

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 Scraper - No Cookies, No Login

Turn public LinkedIn profile URLs into structured career data: **full name, location and country, profile photo, exact follower count, connections, About text, certifications, languages, volunteering, projects, publications, honours and courses** - plus **headline and the full experience and education history** on the profiles LinkedIn shows those for. **$2.99 per 1,000 profiles, no start fee** - and only profiles we actually read are charged. No LinkedIn login, no cookies, no account of your own, ever.

> **Read this before you buy.** LinkedIn puts the work and study history of **most** profiles behind its sign-in blur, and no logged-out scraper can get past it. In our measurement of 22 real profiles, **4 returned the full experience and education history and 8 did not**. The 8 still returned everything else in full - name, location, photo, followers, connections, About, certifications, languages, volunteering, projects, publications, honours, courses. Those rows are marked `experienceEducationBlurred: true` with `partialReason: "experience_education_blurred"`, so you always know which kind you got. If full job history on every profile is what you need, no cookie-free Actor can give it to you.

Input:

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

Output - one row per URL you gave, in your order. Sample from the real page of `/in/williamhgates`:

```json
{
  "recordType": "profile",
  "status": "ok",
  "inputUrl": "https://www.linkedin.com/in/williamhgates",
  "profileUrl": "https://www.linkedin.com/in/williamhgates",
  "publicIdentifier": "williamhgates",
  "canonicalUrl": "https://www.linkedin.com/in/williamhgates",
  "duplicateOf": null,
  "partial": false,
  "partialReason": null,
  "statusText": "This profile was read from its public LinkedIn page.",
  "fullName": "Bill Gates",
  "firstName": "Bill",
  "lastName": "Gates",
  "headline": "Chair, Gates Foundation and Founder, Breakthrough Energy",
  "location": { "text": "Seattle, Washington, United States", "country": "US" },
  "profilePictureUrl": "https://media.licdn.com/dms/image/v2/D5603AQF-RYZP55jmXA/profile-displayphoto-shrink_200_200/…",
  "followersCount": 40747516,
  "followersText": "41M followers",
  "connectionsText": "8 connections",
  "about": "Chair of the Gates Foundation. Founder of Breakthrough Energy. Co-founder of Microsoft. Voracious reader. Avid traveler. Active blogger.",
  "experience": [
    {
      "title": "Co-chair",
      "companyName": "Gates Foundation",
      "companyLinkedinUrl": "https://www.linkedin.com/company/gates-foundation",
      "location": null,
      "startDate": "2000",
      "endDate": null,
      "isCurrent": true,
      "durationText": "26 years",
      "description": null
    }
  ],
  "education": [
    {
      "school": "Harvard University",
      "schoolLinkedinUrl": "https://www.linkedin.com/school/harvard-university/",
      "degree": null,
      "fieldOfStudy": null,
      "startDate": "1973",
      "endDate": "1975"
    }
  ],
  "certifications": [],
  "languages": [],
  "volunteering": [],
  "projects": [],
  "publications": [],
  "honors": [],
  "courses": [],
  "collectedAt": "2026-10-24T15:32:10.411Z"
}
```

Limits - read these before you buy:

- **Most profiles' experience and education are behind LinkedIn's sign-in blur.** LinkedIn serves them one combined "Experience & Education" block with a single entry visible and the rest blurred out, and masks the same entries inside the page's structured data with asterisks. Those masked values are never written out as if they were data; `experience` and `education` then hold whatever fragment was actually visible, or nothing, and the row carries `experienceEducationBlurred: true` plus a `statusText` sentence saying so. **An empty `experience` on a flagged row does not mean the person has no work history.** The same profiles also get their About text cut short behind a "see more" prompt (`aboutTruncated: true`).
- **No email addresses, no phone numbers, no personal websites.** LinkedIn does not show them to a logged-out visitor, and this Actor does not buy them from a contact-enrichment service either. If you need them, this is the wrong Actor.
- **No headline on a blurred profile.** LinkedIn renders no headline element at all on that variant, so `headline` is `null` there rather than guessed from the About text or the current role.
- **No skills.** LinkedIn renders the skills section only after sign-in, so there is nothing to read.
- **No posts, no activity, no other people.** The profile page also carries the member's articles and posts and lists of other members ("Other similar profiles", "People also viewed"). None of it is read or written.
- **Guest-visible content only.** What you get is what LinkedIn itself publishes to a visitor with no account. A member who hides their About section, or whose experience LinkedIn truncates for guests, gives you less - and the row says so rather than pretending the profile is empty.
- **Fields LinkedIn leaves out are `null`, sections it does not have are `[]`.** Nothing is guessed from another field. `location.country` is the ISO code LinkedIn's own structured data carries, or `null` - never derived from the location words. `followersCount` is the exact figure when LinkedIn's structured data carries it; the page itself rounds ("41M followers"), and the rounded wording is kept in `followersText` instead of being turned into a made-up number.
- **`firstName` / `lastName` are the first word and the rest of the displayed name.** LinkedIn does not publish the two parts separately, so compound given names and some name orders split wrongly. `fullName` is always the name as LinkedIn displays it.
- **Dates are LinkedIn's own strings** (`"2000"`, `"Jan 2019"`), kept verbatim rather than reformatted, because LinkedIn itself only shows a year for older roles.
- **Some members have public profile visibility switched off.** LinkedIn then sends the sign-in wall for them, to everyone, from every IP. Those rows get `status: "not_public"` and **are not charged**. In our 22-profile test 10 were like this, and the figure says nothing about your proxy: a datacenter run and a residential run were refused the *same* 10 and read the *same* 12, and re-fetching those 10 from fresh IPs reproduced it exactly while readable profiles kept working. **Switching to the residential pool does not help** - it cost 2.8x more for an identical result - so the default datacenter pool is the right one.

### What it does

- Reads one page per profile: `linkedin.com/in/<identifier>`, logged out. No cookie is sent and none is kept - a cookie is not even accepted as input.
- Accepts the URL forms you actually have: full URLs, country subdomains (`uk.linkedin.com/in/…`, which LinkedIn redirects to `www` itself), URLs with tracking or `locale` parameters, `/in/<id>` paths, and bare identifiers. Everything is normalised to one canonical URL, so `/in/WilliamHGates` and `/in/williamhgates` are one profile.
- **One row per input line, in your order, always** - including a line that was invalid, refused, or never reached. You can join the output straight back onto your own list with no holes. The same profile listed twice is fetched once and charged once; the repeat row carries the same data and names the earlier entry in `duplicateOf`.
- Reads LinkedIn's own schema.org `Person` block first (the authoritative name, location, country code, photo and exact follower count) and the rendered page sections second (headline, About, each role's own job title, the month-level date ranges, the duration LinkedIn prints, and the secondary sections).
- Never reports an incomplete read as an empty profile. `status` is `ok`, `not_public` (the sign-in wall on every attempt), `invalid_url` (not a LinkedIn profile address - costs no request and is free), `upstream_error`, `not_collected` (the run stopped before this line) or `not_found` (HTTP 404). `statusText` on every row says which it is in a sentence. Note that LinkedIn gives a guest the same sign-in wall whether a profile is hidden or simply does not exist, so `not_public` covers both and says so; a typo in your list is not distinguishable from a private profile, by anyone.
- Flags a row that was read but is not the whole picture with `partial: true` and a `partialReason`: `experience_education_blurred` (LinkedIn paywalled the work and study history - see the warning above), `no_structured_data` (LinkedIn served the page without its `Person` block, so the exact follower count, country code and photo may be missing), `empty_profile` (a page with no name on it - written, flagged, not charged), `upstream_request_cap`, `charge_limit_reached`.
- **Never writes out a masked value.** Where LinkedIn substitutes asterisks for a company or school name, the field is `null` and the row is flagged - `"******* ******"` is not a company name and does not belong in your database.
- Keeps the source identity of every row: your `inputUrl`, the canonical `profileUrl`, LinkedIn's `publicIdentifier`, the `canonicalUrl` the page says it is (which differs when LinkedIn redirected the identifier), and our own `collectedAt`. LinkedIn publishes no "last updated" time for a profile, so `collectedAt` is the only time a row can carry.
- Caps itself: `maxUpstreamRequests` is a hard ceiling on requests to LinkedIn for the whole run, retries included, derived from your list when you leave it empty, so a run is never unbounded.

### Input

| Field | What it does |
| --- | --- |
| `profileUrls` | Public LinkedIn profiles, one per line. Full URL, country subdomain, `/in/<id>` path or bare identifier. Required. |
| `maxUpstreamRequests` | Optional hard ceiling on requests to LinkedIn for the whole run, retries included. Default: derived from your list. |
| `proxyConfiguration` | Where requests come from. Default: the Apify datacenter proxy, which measured identical results to residential at a third of the cost. |

### Output

- `recordType: "profile"` - one per input line. Charged only when `status` is `ok` and `partialReason` is not `empty_profile`, and only once per distinct profile.
- `recordType: "RUN_SUMMARY"` - the last row, also stored under the key `RUN_SUMMARY`: rows written and charged, a count per status, incomplete rows by reason, repeated and invalid lines, the success rate over the profiles a request was actually sent for, average requests per profile, requests and retries sent to LinkedIn, and why collection stopped. Free.

### Pricing

Pay per result: **$2.99 per 1,000 profiles**, no start fee. Rows with any status other than `ok` are free, an `empty_profile` row is free, a repeated profile is charged once, and the run summary is free. Set a maximum charge per run in Apify and the Actor stops at it, keeping what it already collected and marking the rest `not_collected`.

### Responsible use

This Actor reads only what LinkedIn publishes to visitors who are not logged in, and it keeps to professional information. It is still personal data, and using it is your responsibility, not ours:

- **You need a lawful basis.** Under the GDPR and comparable laws, scraping publicly available personal data is still processing personal data. Decide and document your lawful basis (usually legitimate interests, with the balancing test done) before you run this at scale, and give data subjects the information those laws require.
- **Do not use it for spam.** Unsolicited bulk outreach to scraped contacts is prohibited by this Actor's terms of use, and in most jurisdictions by law. This Actor deliberately returns no email address and no phone number.
- **Honour deletion and objection requests.** If a person asks you to delete their data or objects to your processing, do it - including in any copy you derived from this output. Keep the `collectedAt` timestamp so you can tell how old a record is, and refresh or drop stale records.
- **LinkedIn's terms are your responsibility.** LinkedIn's User Agreement restricts automated collection. This Actor does not log in and therefore does not accept that agreement on your behalf, but you must satisfy yourself that your own use is lawful and acceptable where you operate. Consider whether you need the data at all, and collect the minimum you do need.
- **Never use it for decisions that need accuracy you cannot verify.** A profile is self-reported and may be out of date. Do not use it as the sole basis for employment, credit, insurance or any other decision that materially affects a person.

# Actor input Schema

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

One public LinkedIn profile per line. A full URL (`https://www.linkedin.com/in/williamhgates`), a country subdomain (`uk.linkedin.com/in/...`), a URL with tracking or locale parameters, or just the identifier (`williamhgates`) all work. You get one row per line, in the order you give them, even for a line that could not be read. The same profile written twice is fetched once and charged once.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on the HTTP requests this run sends to LinkedIn, retries included. At the ceiling collection stops, the profiles not reached get status "not_collected", and the run ends normally. Leave empty and the Actor works out a ceiling from your list, so a run is never unbounded.

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

The proxy requests to LinkedIn go through. Every request goes out on a fresh proxy session. The default datacenter pool is the right choice: in a measured comparison the residential pool read exactly the same profiles and was refused exactly the same ones, at 2.8x the cost. A "not_public" row means the member has hidden their public profile, which no proxy can change.

## Actor input object example

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

# Actor output Schema

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

No description

## `runSummary` (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",
        "https://www.linkedin.com/in/satyanadella"
    ]
};

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

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

```

## MCP server setup

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

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/d2pNn9bDjFPgkMhUz/builds/i7xbXbaQw3UGic6n0/openapi.json
