# Bulk LinkedIn Profile Enrichment | No Cookies | from $2.996/1k (`b2bdata/bulk-linkedin-profile-enrichment`) Actor

Bulk LinkedIn profile enrichment: give profile URLs or paste a CSV, get one fresh row per person: name, headline, title, company, seniority, location. No login or cookies. Pay per profile found. Does not search by name or company; use linkedin-people-search. Free plan: 100 per run.

- **URL**: https://apify.com/b2bdata/bulk-linkedin-profile-enrichment.md
- **Developed by:** [Open Enrich](https://apify.com/b2bdata) (community)
- **Categories:** Lead generation, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 profile founds

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
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?

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

## Bulk LinkedIn Profile Enrichment | No Cookies | from $2.996/1k

Bulk LinkedIn profile enrichment from a list or a pasted CSV: give profile links, get one fresh row per person with name, headline, current title, company, seniority, location and tenure. Up to 10,000 profiles per run, no LinkedIn account, no cookies, works through the API, MCP and schedules.

**No login | Paste a CSV or a list | Profile URL, short URL or bare slug | One row per profile | Up to 10,000 per run | JSON/CSV export**

> **Not a scraper of linkedin.com.** The actor never opens a LinkedIn page. Each row is read from a profile database by the profile address you supply. Not affiliated with LinkedIn.

[See all actors by b2bdata](https://apify.com/b2bdata)

***

### 💳 Pricing and plans

Pay per result, see the Pricing tab. Free Apify accounts can try it: up to USD 1 of results in the 14 days after their first run of this actor, then a paid Apify plan is needed. Normal Apify platform usage applies.

| Event | When it is charged | Free plan, per 1,000 | Gold plan, per 1,000 |
|---|---|---|---|
| `profile` | One profile found and delivered as a row | $3.963 | $3.052 |

Bronze and Silver prices sit in between and are on the Pricing tab. Higher Apify plans only lower the price. The free plan's $5 credit covers about 1,261 profiles at the free-plan price.

#### Success-only billing

You pay for delivered rows only.

- A profile that is not found is free.
- A failed lookup is free.
- An input that is not a LinkedIn profile link is skipped and is free.
- A notice row (for example `free_plan_limit`) is free.
- An empty run is free, and there is no start fee.

Each delivered profile is one `profile` event, charged right before the row is saved.

#### The free plan

Free Apify accounts get up to 100 inputs looked up per run, and as many runs as their Apify credit covers. Nothing is stored on our side. When a list is longer, the first 100 are looked up and one free notice row (`reason: "free_plan_limit"`) says how many inputs were not looked up. Paid Apify plans have no per-run limit.

#### Keep a run small while you test

Use `limit` to cap the number of profiles. The default is 100. Run 3 to 5 profiles first, read the rows, then scale up. You can also set a spend cap for a run with the hidden `maxChargeUsd` field in the API input.

***

### ⚖️ This vs. the alternatives

| | **This actor** | Cookie-based extension tools | Other Store profile scrapers |
|---|---|---|---|
| LinkedIn account needed | **No** | Yes (your own session) | Varies by actor |
| Your account at risk | **No, it is never used** | Yes | Varies by actor |
| Input | **List or pasted CSV, any link form** | Open page or URL list | URL list |
| Per call to the data source | **25 profiles** | n/a | Varies |
| Output | **One flat row per profile, fixed keys** | Varies | Varies |
| Misses | **Free and listed in `MISSES`** | n/a | Varies |
| Duplicates in your list | **Collapsed, charged once** | Not handled | Varies |

Public Store prices per 1,000 profiles, FREE tier, read from the public Store pages on 2026-10-13. Check the pages for current prices.

| Actor | Per 1,000 profiles |
|---|---|
| This actor | $3.963 |
| harvestapi profile scraper | $4 |
| apimaestro profile batch scraper | $5 |
| anchor profile enrichment | $6, plus a start fee and a runtime fee per run |
| dev_fusion profile scraper | $10 |

We compare on price only and make no claim about the other actors beyond that. Field lists differ between actors, so check that the columns you need are in the row before you compare cost.

***

### 🧩 Richer data when you need it

The default row is compact and fast: name, headline, current title, company, location and tenure. Nothing changes for existing runs. Add the `include` input to get more per profile, at the same price (`profile` event, unchanged). The groups are never a condition for charging a row.

| `include` value | Adds to each row |
|---|---|
| `career` | `experience[]` (work history), `volunteerExperience[]`, `education[]`, `skills[]`, `organizations[]` |
| `details` | `about`, `industry`, `followersCount`, `profileImageUrl`, `backgroundImageUrl` |
| `credentials` | `certifications[]`, `languages`, `honors`, `projects`, `courses`, `testScores`, `publications`, `patents` |
| `social` | `recommendations[]`, `interests`, `articles[]`, `groups[]` |
| `location` | `city`, `state`, `stateFullName`, `countryFullName`, `countryIso3` |
| `links` | `linkedinUrl`, `githubUrl`, `twitterUrl`, `facebookUrl`, `websiteUrl`, `hasPhone` (true/false only) |
| `currentJob` | `currentCompanyLinkedinId`, `currentCompanyLogoUrl` (expiring link), `currentJobStartDate`, `currentJobDescription`, `currentJobLocation`, `currentEmploymentType[]`, `currentJobFunction[]`, `currentSeniority[]`, `jobTitleNormalized` |
| `analytics` | `seniorityLevel`, `isDecisionMaker`, `averageTenureYears`, `skillsCount`, `educationCount`, `experienceCount`, `certificationsCount`, `languagesCount`, `companiesCount`, `highestDegree` |
| `network` | `relatedProfiles[]` (up to 10), `similarNames[]` (up to 10), `activities[]` |
| `quality` | `dataQualityPublicExperienceAvailable`, `sectionsIncluded[]` |
| `companyDetails` | Fields about the current employer, prefixed `company` (see Company details below) |
| `all` | Every group above at once, including `companyDetails` |

Example input: `{"items": ["https://www.linkedin.com/in/jane-example"], "include": ["career", "details"]}`. Fields a profile does not have are left out; empty lists are omitted.

Only fields the source actually holds are returned; nothing is padded or duplicated. Measured on 2026-10-13 on three public profiles (non-empty top-level fields per row, empty values, zeros and false flags not counted): deep mode with `include: ["all"]` returned 67, 62 and 72; the compact fast mode with `include: ["all"]` returned 37, 38 and 40; the rival `data-slayer/linkedin-profile-scraper` returned 51, 52 and 49 for the same three profiles. Three profiles is a small test, not a statistic.

Image links (`profileImageUrl`, `backgroundImageUrl`, `currentCompanyLogoUrl`) are time-limited and expire. Download the pictures soon after the run if you need them.

Image links (`profileImageUrl`, `backgroundImageUrl`, `currentCompanyLogoUrl`, `companyLogoUrl`) expire; see above.

\---|---|---|
| Experience and education lists | Yes, when `include` has `career` | Yes |
| Profile pictures | Yes, when `include` has `details` | Yes |
| Default row | Compact and fast | Full |
| Price per profile | $0.003963 (free plan) | about $0.004 |

***

### 🔬 Deep mode: the most data per LinkedIn profile

Set `depth` to `deep` to get the richest rows. Two routes, same price (`profile` event, same charge rules):

| | `compact` (default) | `deep` |
|---|---|---|
| Route | Fast bulk route, 25 profiles per upstream call | One upstream call per profile |
| Speed (upstream pacing; estimate, unverified) | about 1,500 profiles per minute | about 60 profiles per minute |
| Data | The sections the bulk route returns | Everything the single-profile route returns, for example more education and related-profile detail |
| Misses | Free, listed in `MISSES` | Free; a profile that does not exist (404) is `not_found` |

Use `compact` for big CRM refreshes and weekly re-checks of thousands of profiles. Use `deep` for smaller lists (a few hundred) where you want every field, for example before outreach or account research. `depth` and `include` are independent: `deep` does not turn on any group by itself, and `include: ["all"]` does not switch the route. For the most data per row, send `"depth": "deep"` together with `"include": ["all"]`.

Example input: `{"items": ["https://www.linkedin.com/in/jane-example"], "depth": "deep", "include": ["all"]}`. Time estimates come from the upstream rate limits (4 requests every 4 seconds), not from a measured run; a 1,000-profile deep run takes about 17 minutes by that limit.

***

### 🏢 Company details: employer data on every LinkedIn profile row

Add `companyDetails` to `include` (or use `all`) to attach data about the person's current employer. The actor takes the current job (the one with no end date), looks the company up once per run (cached, so 500 people at the same company cost one company lookup), and adds flat fields prefixed `company`:

`companyName`, `companyWebsite`, `companyIndustry`, `companyType`, `companyDescription`, `companyTagline`, `companySpecialities[]`, `companyEmployeeRange` (`[min, max]`, `null` max = open-ended), `companyEmployeesOnLinkedin`, `companyFollowers`, `companyFoundedYear`, `companyHqLine1`, `companyHqCity`, `companyHqState`, `companyHqCountry`, `companyHqPostalCode`, `companyLocationsCount`, `companyLogoUrl` (expiring link), `companyLinkedinUrl`, `companyCrunchbaseUrl`, `companyTotalFundingUsd`, `companyFundingRounds`, `companyInvestors`, `companyStockSymbol`, `companyIpoStatus`, and `companyFundingData[]` / `companyAcquisitions[]` when present.

- Real values only: a field the company record does not hold is left out.
- If the person has no current job, no company id, or the company cannot be found or looked up, the company fields are simply missing. The profile row is still delivered and charged as usual.
- The company lookup is part of the same `profile` price in this actor's pricing. Company lookups share the upstream rate budget, so a list with many different employers runs slower.
- Works with both `compact` and `deep`. `companyLogoUrl` expires, download it soon.

***

### ⚡ Why this bulk LinkedIn profile scraper?

- **Built for lists.** Paste links one per line or paste a whole CSV. Profile links in any column are picked up, the other columns are ignored.
- **Batched lookups.** Profiles go to the data source 25 per call, four calls at a time, paced under its rate limit.
- **No login, no cookies.** Nothing of yours is exposed and nothing can get banned.
- **Forgiving input.** `https://www.linkedin.com/in/slug`, `linkedin.com/in/slug` and plain `slug` all work. Query strings and trailing slashes are stripped.
- **Order kept, duplicates collapsed.** The same person written two ways is looked up once and billed once.
- **Each row says when it was read.** `scrapedAt` and `inputQuery` are on every row, so you can match rows back to your sheet.
- **Honest misses.** Every input that did not turn into a row is listed with a reason in the `MISSES` record, and none of them cost anything.
- **Strict input.** An unknown field fails the run at once with "remove unsupported fields", before anything is charged.

***

### 🎯 Use cases: refresh contact list, follow up, map accounts

- **CRM refresh.** Re-run the LinkedIn links in your CRM export every quarter and update title and company by `profileUrl`.
- **Event follow-up.** Turn a scanned-badge or attendee CSV into current roles and seniority before the sales team writes.
- **Account mapping.** Check who in a buying group is a VP or above across hundreds of accounts in one run.
- **Recruiting lists.** Convert a shortlist of candidate links into a table with current role, tenure and total experience.
- **Lead scoring inputs.** Add `seniority`, `tenureYears` and `department` as features to a scoring model.
- **Agent tool.** Let an AI agent resolve a whole list of profile links to structured facts with one call.

***

### 🔗 Pipeline guide: from a list of links to a contact

```
  Names or a target list, no URLs yet
        |
        v
 [1] linkedin-people-search / linkedin-person-search   -> profile URLs
        |
        v
 [2] bulk-linkedin-profile-enrichment (this actor)               -> one row per profile
        |
        v
 [3] email-phone-finder                                -> phone numbers and emails from name + company
```

Step 1 is only needed when you have no links. For one or two profiles with emails on file, use [linkedin-profile-by-url](https://apify.com/b2bdata/linkedin-profile-by-url) instead. All actors are on the publisher page: [See all actors by b2bdata](https://apify.com/b2bdata).

***

### 🚀 Quick start

Open the actor, paste your links or your CSV, press Start. Or send JSON. Three examples:

#### 1. A short list

```json
{
  "items": [
    "https://www.linkedin.com/in/jane-example",
    "linkedin.com/in/john-sample",
    "casey-demo"
  ],
  "limit": 3
}
```

#### 2. LinkedIn CSV enrichment: a pasted CSV

```json
{
  "csvText": "name,company,linkedin\nJane Example,Example Group,https://www.linkedin.com/in/jane-example\nJohn Sample,Sample Inc,https://www.linkedin.com/in/john-sample",
  "limit": 0
}
```

#### 3. A big batch with the audit view

```json
{
  "items": ["https://www.linkedin.com/in/jane-example", "https://www.linkedin.com/in/john-sample"],
  "limit": 10000,
  "includeAudit": true
}
```

In a real run, `items` holds your own links and `csvText` holds your own file. The slugs above are placeholders.

***

### ⚙️ Inputs

Step 1: give links in `items`, in `csvText`, or both. Step 2: set `limit`. Any field not listed here is rejected.

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `items` | string list | One of `items` or `csvText` | prefilled with an example link | LinkedIn profile links, one per entry. Accepted: `https://www.linkedin.com/in/slug`, `linkedin.com/in/slug`, or just the slug. |
| `csvText` | text | One of `items` or `csvText` | none | CSV text. Every LinkedIn profile link in any column is used. Bare words and other columns are ignored. Added after `items`. |
| `limit` | integer | No | `100` | How many profiles to look up, 0 to 10,000. `0` means all of them, up to 10,000. A value above 10,000 is rejected with a message, never silently cut. |
| `include` | list of text | No | `[]` | Extra data groups per row: `career`, `details`, `credentials`, `social`, `location`, `links`, `currentJob`, `analytics`, `network`, `quality`, `companyDetails`, `all` (see Richer data when you need it). Empty = the compact default row. Any other value fails the run, nothing charged. |
| `depth` | text | No | `compact` | `compact` = fast bulk route (about 1,500 profiles/min, estimate). `deep` = one call per profile (about 60 profiles/min, estimate), the most data per row. Same price. Independent of `include`. See Deep mode. |
| `includeAudit` | boolean | No | `false` | Adds an `audit` object to each row: `requestId`, `batchUnitsCharged`, `latencyMs`, `inputRaw`, `inputCanonical`. |
| `maxChargeUsd` | number | No | none | Hidden field for API use. Stops the run once this much has been charged. |
| `sourceTag` | text | No | none | Hidden field for API use. Free text echoed in every row and in the run summary. |

Tips:

- Entries that are not profile links (a company page, a search URL, free text with spaces) are skipped and cost nothing.
- Duplicates are collapsed after the link is cleaned up, in the order you sent them. Entries past `limit` are not looked up.
- Over 10,000 profiles: split the list into several runs.

***

### 📤 Output

One dataset row per profile found. Illustrative values, not a real person:

```json
{
  "profileUrl": "https://www.linkedin.com/in/jane-example",
  "slug": "jane-example",
  "firstName": "Jane",
  "lastName": "Example",
  "fullName": "Jane Example",
  "headline": "VP Sales at Example Group",
  "jobTitle": "VP Sales",
  "company": "Example Group",
  "location": "Example City, Example Region",
  "country": "US",
  "startYear": 2021,
  "tenureYears": 4.5,
  "totalExperienceYears": 15,
  "connections": 500,
  "updatedAt": "2026-01-01 00:00:00.000Z",
  "scrapedAt": "2026-10-13T08:00:00.000Z",
  "inputQuery": "https://www.linkedin.com/in/jane-example"
}
```

Fields you might see, when present:

| Key | Type | When it is missing |
|---|---|---|
| `profileUrl`, `slug` | string | Always present on a row |
| `firstName`, `lastName`, `fullName` | string | Not public |
| `headline` | string | None set |
| `jobTitle`, `company` | string | No current role, or not available |
| `department` | string | Conditional: not seen in our test rows; appears only when available |
| `seniority` | `cxo`, `vp`, `director`, `manager`, `founder`, `other` | Conditional: not in the 18 keys of our test rows; appears only when the source holds a role level |
| `location` | string | Not public |
| `country` | ISO-2 code | Not public |
| `startYear` | integer | No current role |
| `tenureYears`, `totalExperienceYears` | number | Not computable |
| `connections` | integer | Hidden |
| `hasWorkEmail`, `hasPersonalEmail` | boolean | Conditional: not seen in our test rows; appears only when the source knows |
| `updatedAt` | datetime string, like `2026-04-02 23:03:37.529Z` | Not available |
| `scrapedAt`, `inputQuery` | datetime, string | Always present |
| `sourceTag` | string | Only when you set it |
| `experience[]`, `volunteerExperience[]`, `education[]`, `skills[]` | arrays | Only when requested (`include: career`) and present |
| `about`, `industry`, `followersCount`, `profileImageUrl`, `backgroundImageUrl` | string, integer | Only when requested (`include: details`) and present; image links expire |
| `certifications[]`, `languages`, `honors`, `projects`, `courses`, `testScores`, `publications`, `patents` | arrays | Only when requested (`include: credentials`) and present |
| `recommendations[]`, `interests`, `articles[]`, `groups[]` | array, string | Only when requested (`include: social`) and present |
| `city`, `state`, `stateFullName`, `countryFullName`, `countryIso3` | strings | Only when requested (`include: location`) and present |
| `linkedinUrl`, `githubUrl`, `twitterUrl`, `facebookUrl`, `websiteUrl` | strings | Only when requested (`include: links`) and present |
| `hasPhone` | boolean | Only when requested (`include: links`); true or false, no number |
| `currentCompanyLinkedinId`, `currentCompanyLogoUrl`, `currentJobStartDate`, `currentJobDescription`, `currentJobLocation`, `jobTitleNormalized` | strings | Only when requested (`include: currentJob`) and present; logo link expires |
| `currentEmploymentType[]`, `currentJobFunction[]`, `currentSeniority[]` | arrays | Only when requested (`include: currentJob`) and present |
| `seniorityLevel`, `isDecisionMaker`, `averageTenureYears`, `highestDegree` | string, boolean, number | Only when requested (`include: analytics`) and present |
| `skillsCount`, `educationCount`, `experienceCount`, `certificationsCount`, `languagesCount`, `companiesCount` | integers | Only when requested (`include: analytics`) and present |
| `relatedProfiles[]`, `similarNames[]`, `activities[]` | arrays | Only when requested (`include: network`) and present |
| `dataQualityPublicExperienceAvailable`, `sectionsIncluded[]` | boolean, array | Only when requested (`include: quality`) and present |
| `organizations[]` | array | Only when requested (`include: career`) and present |
| `companyName`, `companyWebsite`, `companyIndustry`, `companyType`, `companyDescription`, `companyTagline`, `companyHqLine1`, `companyHqCity`, `companyHqState`, `companyHqCountry`, `companyHqPostalCode`, `companyLogoUrl`, `companyLinkedinUrl`, `companyCrunchbaseUrl`, `companyStockSymbol`, `companyIpoStatus` | strings | Only when requested (`include: companyDetails`) and present; logo link expires |
| `companyEmployeeRange[]`, `companySpecialities[]`, `companyFundingData[]`, `companyAcquisitions[]` | arrays | Only when requested (`include: companyDetails`) and present |
| `companyEmployeesOnLinkedin`, `companyFollowers`, `companyFoundedYear`, `companyLocationsCount`, `companyTotalFundingUsd`, `companyFundingRounds`, `companyInvestors` | numbers | Only when requested (`include: companyDetails`) and present |
| `audit` | object | Only with `includeAudit` (compact: `batchUnitsCharged`; deep: `unitsCharged`) |
| `type`, `reason`, `message` | string | Only on a notice row, never on a profile row |

A field with no value is left out of the row, so the columns that appear are the ones with data. `false` and `0` are real values and are kept. Export as JSON, CSV, Excel, XML or HTML from the Output tab, or read the dataset over the API.

#### Other records in the run

- `MISSES` (key-value store): one entry per input that did not become a row, with the `input` and a `reason`: `invalid_input`, `not_found`, or `Upstream HTTP <code>` / `Upstream unavailable` when a batch failed.
- `errors-bulk-linkedin-profile-enrichment` (key-value store): only the inputs whose batch failed at the data source, so you can run just those again.
- `RUN_SUMMARY` (key-value store): counts for the run (`requested`, `valid`, `attempted`, `depth`, `pushed`, `missed`, `upstreamFailed`, `units`, `stoppedEarly`, `sourceTag`, `build`).
- A notice row (`type: "notice"`) is added when the free plan's 100-per-run limit skips inputs, or when a run stops on your `maxChargeUsd`. It is not a profile and is not charged.

#### Measured in a test run (2026-10-13)

- A list of 25 well-known public profile links returned 12 profiles; the 13 not found were free and listed in `MISSES`. Real rows carried 18 keys.
- A run with a tiny `maxChargeUsd` delivered 1 profile and 1 notice row (`max_charge_reached`), and charged for the 1 profile only.
- An unknown input field failed the run (`remove unsupported fields: foo`), and empty input failed it (`No valid items in input`). Nothing was charged in either case.
- Profiles go to the data source 25 per call; the maximum is 10,000 profiles per run (`limit` 0 = all, up to that maximum).

#### Column notes

- `seniority` is derived from the current role. A missing value means no current role was found, not that the person is junior.
- `updatedAt` is the date of the source record, not the time of your run. `scrapedAt` is the time of your run.
- Company domain is not returned. Use [company-firmographics-by-domain](https://apify.com/b2bdata/company-firmographics-by-domain) when you have a domain from another source.

#### What is NOT returned

- ❌ Email addresses (only `hasWorkEmail`, `hasPersonalEmail` and `hasPhone` flags; no phone numbers; use [linkedin-profile-by-url](https://apify.com/b2bdata/linkedin-profile-by-url) with `includeEmails` for addresses)
- ❌ Posts, comments, reactions or job listings
- ❌ Full employment history, education or skills lists, unless you set `include` to `career` (they are not in the default row)
- ❌ Profile photos, unless you set `include` to `details` (picture links expire)
- ❌ Private or non-public profile data
- ❌ Profiles found by name or keyword (this actor only takes links, use [linkedin-people-search](https://apify.com/b2bdata/linkedin-people-search) for discovery)

***

### 🛡️ Reliable results

**Our data promise.** We work to keep every contact current, and we stand behind it. If a record is outdated, an email bounces or a person has moved on, open the Issues tab with your run link. We review every report and sort it out with you, case by case.

- **Misses are free and visible.** A profile that is not found or fails to load is listed in `MISSES` with a reason and is never charged.
- **Duplicates are collapsed.** The same profile written as a full URL, a short URL and a slug is looked up once and charged once.
- **A failed batch loses nothing.** If a call fails after retries, those inputs go to `errors-bulk-linkedin-profile-enrichment` and the rest of the run carries on. The run still succeeds with the data it has.
- **No mix-ups.** If the data source ever answers with a different number of results than you asked for, that batch is discarded and its inputs are reported, never matched to the wrong link.
- **Each row is saved as it is delivered.** A run that stops keeps what it already produced.
- **Typos fail early.** An unsupported input field stops the run before any charge.

***

### 🤖 Use with AI agents (MCP)

This section is written for an LLM agent that calls this actor as a tool, through the Apify MCP server or the API. Prompt to copy:

> "Use b2bdata/bulk-linkedin-profile-enrichment with items=\["https://www.linkedin.com/in/jane-example", "https://www.linkedin.com/in/john-sample"] and limit=2. Return fullName, jobTitle, company, seniority and location for each row."

Output is schema-stable: the same keys every run, with absent fields left out.

#### When to use this actor, and when to use a sister actor

- **Use `b2bdata/bulk-linkedin-profile-enrichment`** when you hold a list of profile links (tens to thousands) and need structured facts about each person.
- [linkedin-profile-by-url](https://apify.com/b2bdata/linkedin-profile-by-url): one or a few profiles, or you want the email addresses on file.
- [linkedin-person-search](https://apify.com/b2bdata/linkedin-person-search): you know a name and a company but have no link.
- [linkedin-people-search](https://apify.com/b2bdata/linkedin-people-search): you want to find people by title or company when you have no links yet.
- [email-phone-finder](https://apify.com/b2bdata/email-phone-finder): you have a work email and need the person and a phone.

#### Input contract

| Field | Type | Default | Notes |
|-------|------|---------|-------|
| `items` | string\[] | none | Profile URL, `linkedin.com/in/slug` or bare slug. Duplicates collapsed, order kept. |
| `csvText` | string | none | CSV text, links in any column. Use `items` or `csvText` or both. |
| `limit` | integer 0-10000 | `100` | `0` = all, up to 10,000. Above 10,000 is rejected. |
| `depth` | `compact` or `deep` | `compact` | `deep` = one call per profile, the most data, about 60 profiles/min (estimate); compact about 1,500/min. Same price. |
| `include` | string\[] of the groups above (incl. `companyDetails`), or `all` | `[]` | Adds richer fields (lists of jobs, education, skills, about, picture links, certifications, recommendations ...). Omit for the compact row. Same price. |
| `includeAudit` | boolean | `false` | Adds `audit` to each row. |
| `maxChargeUsd` | number | none | Spend cap for the run. |
| `sourceTag` | string | none | Echoed in rows and summary. |

Send only the fields you need. Any other field fails the run with `Invalid input: remove unsupported fields: x`, and nothing is charged.

#### Cost per call

You pay per profile found plus Apify platform usage. Misses, failed lookups, invalid inputs and notices are free. Start with a small `limit`, then scale. Free Apify accounts can spend up to USD 1 in the 14 days after their first run; paid plans have no limit.

#### Error cases and what to do

| Situation | What you see | What to do |
|-----------|--------------|------------|
| Unknown input field | Run fails: `remove unsupported fields: x`, nothing charged | Remove the field and retry once. |
| `limit` above 10,000 | Run fails with the maximum named | Split the list. |
| No valid items | Run fails: `No valid items in input` | Pass at least one profile link or slug. |
| Profile not found | No row; `MISSES` entry `not_found`, free | Check the link. Do not retry more than once. |
| Batch failed | No rows for those inputs; they are in `errors-bulk-linkedin-profile-enrichment`, free | Run just those inputs again, later. |
| Free Apify plan, list longer than 100 | Run SUCCEEDED, first 100 looked up, one free `free_plan_limit` notice row | Not an error. Run again with the rest; a paid Apify plan removes the limit. Do not loop. |
| Your `maxChargeUsd` reached | Run SUCCEEDED, one free `max_charge_reached` notice row | Raise the cap or run the rest separately. |

***

### 🔌 Integrations: send results anywhere

Rows are plain JSON with fixed keys, so any tool that reads an Apify dataset can use them.

- **API.** Start the run and read the dataset with the Apify API, or call the synchronous endpoint for short lists.
- **Webhook.** Add a webhook on run success and update your CRM by `profileUrl`.
- **n8n, Make, Zapier.** Use the Apify node or app, run this actor, then map the dataset rows.
- **Google Sheets, Airtable.** Export CSV from the Output tab or sync the dataset on a schedule.
- **HubSpot, Salesforce, Clay.** Match on `profileUrl` and write `jobTitle`, `company` and `seniority` to your own fields.

***

### 🕐 Schedule examples

Use Apify Schedules (cron) on a saved task.

- **Quarterly CRM refresh:** `0 6 1 */3 *`, your link export in `csvText`, `limit` 0, send rows to the CRM through a webhook.
- **Weekly key-account check:** `0 6 * * 1`, a list of key-account contacts in `items`, `limit` 200, compare `jobTitle` and `company` against last week.

A schedule you forget is still billed for every profile found. Set `maxChargeUsd` on scheduled runs.

***

### 🔗 Related actors

More from the publisher: [See all actors by b2bdata](https://apify.com/b2bdata)

- `b2bdata/linkedin-profile-by-url` ([link](https://apify.com/b2bdata/linkedin-profile-by-url)): one or a few profiles, with optional emails.
- `b2bdata/linkedin-person-search` ([link](https://apify.com/b2bdata/linkedin-person-search)): find a named person when you have no link.
- `b2bdata/linkedin-people-search` ([link](https://apify.com/b2bdata/linkedin-people-search)): find people by title or company.
- `b2bdata/email-phone-finder` ([link](https://apify.com/b2bdata/email-phone-finder)): person and phone from a work email.
- `b2bdata/company-firmographics-by-domain` ([link](https://apify.com/b2bdata/company-firmographics-by-domain)): company facts for a domain.

***

### ❓ FAQ

**How do I do bulk LinkedIn profile enrichment from a CSV without logging in?**
Paste the CSV text into `csvText` (or the links into `items`) and press Start. Every LinkedIn profile link in any column is used. No account and no cookies are involved.

**How do I enrich CSV of LinkedIn URLs and get the result back as CSV?**
Yes. The run produces a dataset with one row per profile. Export it as CSV, JSON or Excel from the Output tab, and match rows to your sheet with `profileUrl` or `inputQuery`.

**How do I refresh LinkedIn contacts in bulk, no login, for my CRM?**
Run your CRM's LinkedIn links through the actor on a schedule, then update `jobTitle`, `company` and `seniority` by `profileUrl`. Compare against the previous run to spot job changes.

**LinkedIn profile batch scraper, 10000 URLs: is that one run?**
Yes, up to 10,000 profiles per run. Set `limit` to 0 to look up everything you gave, or to a number to cap it. A value above 10,000 is rejected with a message, so split bigger lists.

**How do I get bulk LinkedIn profile data from URL list rows that have extra columns?**
Paste the whole CSV into `csvText`. Profile links in any column are picked up and the other columns are ignored.

**How do I update CRM contacts from LinkedIn profile URLs?**
Export the contacts with their LinkedIn links, run them through the actor, then match rows back to your CRM by `profileUrl`.

**What happens to links that are not found or are not profiles?**
They are skipped and never charged. Each one is listed with a reason in the `MISSES` record of the run.

**What is deep mode for LinkedIn profile enrichment?**
Set `depth` to `deep` to fetch each profile through the single-profile route, which returns the most data (about 60 profiles per minute, an unverified estimate from upstream limits). Compact is the default and handles about 1,500 profiles per minute. Same price per profile.

**How do I add company data (industry, size, funding) to each LinkedIn profile?**
Add `companyDetails` to `include`. The current employer is looked up once per run and its fields are added with a `company` prefix. If the company cannot be found, the profile row is still delivered.

**Do I need to remove duplicate links first?**
No. Duplicates are collapsed automatically, including the same profile written two ways, and charged once.

**Am I charged for failed lookups?**
No. You pay one `profile` event per delivered row. Misses, failed lookups, invalid inputs, notices and empty runs cost nothing, and there is no start fee.

**What does the free plan allow?**
Up to 100 inputs looked up per run, as many runs as your Apify credit covers. Longer lists get one free `free_plan_limit` notice row. A paid Apify plan removes the limit.

***

### 🛟 Support

Use the **Issues** tab on this actor's Apify page for bugs, questions and feature requests. We answer within 48 hours on working days. Please include the run ID and the input you used.

***

### 📜 Legal

**Use for B2B prospecting only. Comply with all applicable laws including GDPR, CCPA, and CAN-SPAM.**

This actor is not affiliated with, endorsed by, or sponsored by LinkedIn. The LinkedIn name belongs to its respective owner. It returns profile facts from a profile database. It does not log in, use cookies, access private pages or bypass authentication. Published on Apify by b2bdata as proprietary software.

# Changelog

This Actor's version history is a separate document: https://apify.com/b2bdata/bulk-linkedin-profile-enrichment/changelog.md

# Actor input Schema

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

Step 1. Accepted forms: https://www.linkedin.com/in/slug, linkedin.com/in/slug, or just the slug. Example: https://www.linkedin.com/in/satyanadella. Duplicates are collapsed and results keep input order. Leave empty if you paste a CSV below.

## `csvText` (type: `string`):

Step 1, alternative. Paste CSV text. Every LinkedIn profile link in any column is used; other columns are ignored. Links here are added after the list above. Omit if you use the list.

## `limit` (type: `integer`):

Step 2. How many profiles from your list to look up. 0 means all of them, up to 10,000 per run. A value above 10,000 is rejected. Omit for 100.

## `depth` (type: `string`):

Step 2b, optional. compact (default) = fast bulk route, 25 profiles per upstream call, about 1,500 profiles/min (estimate from upstream rate limits, unverified). deep = one upstream call per profile, about 60 profiles/min (estimate, unverified), and the richest rows: more sections such as education and related profiles. Same price per profile in both. Use compact for big CRM refreshes, deep for smaller lists where you want everything. Independent of `include`: include picks the field groups, depth picks the route; deep with include \["all"] returns the most. A profile that does not exist is a free miss in both modes.

## `include` (type: `array`):

Step 3, optional. Pick the extra groups to add to each row; the price per profile does not change. Format: a list of group names, for example \["career","details"]. career = experience\[] (work history), volunteerExperience\[], education\[], skills\[]. details = about, industry, followersCount, profileImageUrl, backgroundImageUrl (image links expire, download them soon). credentials = certifications\[], languages, honors, projects, courses, testScores, publications, patents. social = recommendations\[], interests, articles\[], groups\[]. location = city, state, stateFullName, countryFullName, countryIso3. links = linkedinUrl, githubUrl, twitterUrl, facebookUrl, websiteUrl (URLs only) and hasPhone (true/false; no phone numbers, no email addresses are ever returned). currentJob = the current role as flat fields: currentCompanyLinkedinId, currentCompanyLogoUrl (expiring link), currentJobStartDate, currentJobDescription, currentJobLocation, currentEmploymentType, currentJobFunction, currentSeniority, jobTitleNormalized. analytics = seniorityLevel, isDecisionMaker, averageTenureYears, and counts (skillsCount, educationCount, experienceCount, certificationsCount, languagesCount, companiesCount) plus highestDegree. network = relatedProfiles (up to 10), similarNames (up to 10), activities. quality = dataQualityPublicExperienceAvailable, sectionsIncluded. career also adds organizations. companyDetails = fields about the person's CURRENT employer, one company lookup per distinct employer per run, prefixed company: companyName, companyWebsite, companyIndustry, companyType, companyDescription, companyTagline, companySpecialities, companyEmployeeRange, companyEmployeesOnLinkedin, companyFollowers, companyFoundedYear, companyHq\* (line, city, state, country, postal code), companyLocationsCount, companyLogoUrl (expiring link), companyLinkedinUrl, companyCrunchbaseUrl, companyTotalFundingUsd, companyFundingRounds, companyInvestors, companyStockSymbol, companyIpoStatus, companyFundingData, companyAcquisitions. A company that cannot be found is skipped, the profile row is still delivered and charged. For the most data per row combine include \["all"] with depth deep. all = every group at once (use \["all"]). Only fields the profile actually has are added; empty ones are left out. Omit or leave empty for the default compact row (name, headline, title, company, location, tenure): it is the fastest and smallest. Any other value fails the run and nothing is charged.

## `includeAudit` (type: `boolean`):

Adds an audit object to each row: requestId, batchUnitsCharged, latencyMs, inputRaw, inputCanonical. Omit for a clean row.

## `maxChargeUsd` (type: `number`):

Stops the run once this much has been charged. Omit for no cap.

## `sourceTag` (type: `string`):

Free text echoed in every row and in the run summary, to tag where a run came from.

## Actor input object example

```json
{
  "items": [
    "https://www.linkedin.com/in/satyanadella"
  ],
  "limit": 100,
  "depth": "compact",
  "include": [],
  "includeAudit": false
}
```

# Actor output Schema

## `profiles` (type: `string`):

One row per profile found: person, current role, company, location

# 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/satyanadella"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("b2bdata/bulk-linkedin-profile-enrichment").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/satyanadella"] }

# Run the Actor and wait for it to finish
run = client.actor("b2bdata/bulk-linkedin-profile-enrichment").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/satyanadella"
  ]
}' |
apify call b2bdata/bulk-linkedin-profile-enrichment --silent --output-dataset

```

## MCP server setup

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

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/9490gbC0oqh5P03ZL/builds/Hn2yAKpKULKN5TuXA/openapi.json
