# LinkedIn Profile Search Scraper - $0.50/1k, No Login (`dami_studio/linkedin-profile-search-scraper`) Actor

Search public LinkedIn profiles by keywords, location, job title, company or school and get name, headline, location, current role and profile URL. No login, no cookies, no LinkedIn account needed. The cheapest LinkedIn people search on the market at $0.50 per 1,000 profiles.

- **URL**: https://apify.com/dami\_studio/linkedin-profile-search-scraper.md
- **Developed by:** [Dami's Studio](https://apify.com/dami_studio) (community)
- **Categories:** Lead generation, Social media, Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 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.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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 Search Scraper — public people search, no login

Type a job title, a skill or a name, add a city or a company if you want to narrow it down, and get
back a list of **public LinkedIn profiles**: full name, headline, location, current role, current
company, education, connection count and the profile URL.

No LinkedIn account. No cookies. No session token to paste in. Nothing to install. You run it, you
get rows.

**Price: $0.50 per 1,000 profiles**, plus a $0.001 run start fee. That is the lowest price for a
LinkedIn people search on the Apify Store. Sample rows and error rows are never billed.

***

### What it actually does

LinkedIn's own people search sits behind the login wall — you cannot open
`linkedin.com/search/results/people/` logged out and you cannot script it without an account.

What *is* public is the profile pages themselves. LinkedIn publishes every `/in/<name>` page to the
open web so search engines can index it, complete with the member's name and headline in the page
title and a description that spells out their experience, education and location. That published
version is what this Actor searches. It builds a query from your keywords and filters, runs it
against the public web index, and turns the matching profile pages into structured rows.

That has two consequences worth knowing up front:

- **It is genuinely account-free.** There is no login step that can break, no cookie that expires,
  and no risk to a LinkedIn account of yours, because none is used.
- **It sees what the public web sees.** A profile that has opted out of public visibility, or one
  that has never been indexed, will not appear. See the limits section below.

***

### Input

Everything is optional. Give it a `searchQuery`, or filters, or both.

| Field | Type | What it does |
|---|---|---|
| `searchQuery` | string | Free-text people search: `"software engineer"`, `"head of growth fintech"`, `"Sarah Chen"`. |
| `locations` | string\[] | Keep profiles whose public page mentions one of these places. Several values are searched as OR. |
| `currentJobTitles` | string\[] | Keep profiles mentioning one of these titles. |
| `currentCompanies` | string\[] | Company name **or** a full LinkedIn company URL — both work. |
| `schools` | string\[] | School or university name. |
| `maxItems` | integer | How many profiles to return, 1–120. Default 20. |

A real input, exactly as you would paste it into the API:

```json
{
  "searchQuery": "software engineer",
  "locations": ["San Francisco"],
  "maxItems": 12
}
```

A wider one, using several values on a filter to reach a bigger list:

```json
{
  "searchQuery": "product manager",
  "locations": ["London", "Berlin", "Amsterdam"],
  "maxItems": 60
}
```

That second run finished in **10 seconds** and returned **60 profiles**.

If you start a run with completely empty input, you get one clearly labelled free sample row telling
you what to fill in. The run still succeeds, and you are not charged for any profiles — that is
deliberate, so a forgotten field never costs you a failed, billed run.

***

### Output

One row per profile. Here is a genuine row from a real run, copied out of the dataset unedited:

```json
{
  "ok": true,
  "_sample": false,
  "charged": true,
  "recordType": "profile",
  "profileUrl": "https://www.linkedin.com/in/swong415",
  "publicIdentifier": "swong415",
  "fullName": "Sharon Wong",
  "firstName": "Sharon",
  "lastName": "Wong",
  "headline": "Software Engineer",
  "location": "San Francisco",
  "currentPosition": "Software Engineer",
  "currentCompany": "Dropbox",
  "education": null,
  "connections": "475",
  "snippet": "Software Engineer · Experience: Dropbox · Location: San Francisco · 475 connections on LinkedIn.",
  "searchQuery": "site:linkedin.com/in software engineer \"San Francisco\"",
  "foundVia": "secondary-index",
  "scrapedAt": "2026-08-15T13:32:07.862Z"
}
```

#### Field reference

| Field | Notes |
|---|---|
| `profileUrl` | Canonical public profile URL. Country subdomains (`uk.linkedin.com`, `ug.linkedin.com`) are preserved because that is where the page actually lives. |
| `publicIdentifier` | The slug after `/in/`. Handy as a primary key when you de-duplicate across runs. |
| `fullName`, `firstName`, `lastName` | Name as published. `lastName` is `null` for single-word names. |
| `headline` | The member's own headline line, when the indexed title carries one. |
| `location` | From the published profile description first, the title second. |
| `currentPosition`, `currentCompany` | Split out of the headline on `at` / `@`, or taken from the published "Experience:" line. A headline written as a keyword list (`Engineer \| FinTech \| AI/ML`) is **not** guessed at — you get `null` rather than "FinTech" invented as an employer. |
| `education` | From the published "Education:" line when present. |
| `connections` | As a string, because LinkedIn publishes `"500+"` as often as a number. |
| `snippet` | The raw indexed description, so you can see what the match was based on. |
| `searchQuery` | The exact query the row came from — useful when you widen a search and want to know which variant found whom. |
| `recordType` | `profile`, `sample` or `diagnostic`. |
| `charged` | `true` only on rows you actually paid for. |

Every field that could not be read is `null`. Nothing is inferred, padded or filled with a
placeholder.

#### Rows that are not profiles

- **Sample row** (`recordType: "sample"`, `charged: false`) — returned when you run with empty
  input.
- **Diagnostic row** (`recordType: "diagnostic"`, `charged: false`) — returned when a search found
  nothing, or when the public indexes refused the run. It tells you which of the two happened and
  what to change:

```json
{
  "ok": false,
  "charged": false,
  "recordType": "diagnostic",
  "code": "NO_RESULTS",
  "requestsMade": 3,
  "droppedOffTarget": 40,
  "details": "The indexes answered, but all 40 profile(s) they returned failed your own filters, so none were kept or charged. Loosen a filter."
}
```

Neither of those is ever billed as a profile, and neither makes the run fail. A failed run still
costs you the start fee, so this Actor exits successfully and explains itself instead.

***

### Pricing

| Event | Price |
|---|---|
| Run start | $0.001 |
| `profile-found` — one matching public profile saved | $0.0005 |

That is **$0.50 per 1,000 profiles**, and it is the cheapest LinkedIn people search on the market.

What that means in practice:

- 20 profiles in one run → $0.011
- 120 profiles in one run (the per-run maximum) → $0.061
- 1,000 profiles → $0.50 of profile charges, plus one $0.001 start fee per run. A run tops out at
  120 profiles, so that is 9 runs and $0.509 in total.

You are charged once per profile row, right after it is written to the dataset. You are **not**
charged for:

- the sample row on an empty run,
- diagnostic rows,
- listings that came back from the index but failed your own filters (they are dropped before
  anything is billed — a real run dropped 15 off-target listings and charged for none of them),
- duplicate profiles across queries inside a run — the de-duplication happens before the charge.

There is no monthly fee and no minimum.

***

### How many profiles can I actually get?

Be realistic about this, because a lot of listings are not.

**Up to 120 per run, and that is a hard cap in the input box on purpose.** Measured on the cloud on
2026-08-17 on the build that is live right now, straight from the run logs:

| Search | `maxItems` | Profiles delivered | Time |
|---|---|---|---|
| `"marketing manager"` + London | 120 | **120 / 120** distinct | 18 s |
| `"software engineer"` + San Francisco | 20 | **20 / 20** distinct | 5 s |

This used to top out around 40 per search whatever you asked for. That cap is gone: it was never
LinkedIn's limit, it was one slice of the public index. The search now also walks LinkedIn's
international public-profile hostnames, which are indexed separately and hold genuinely different
people, and it still widens across your own multi-value filters on top of that (three cities means
three searches, run automatically).

The box stops at 120 rather than at some rounder, larger number because 120 is the figure that has
actually been delivered in full. An attempt at 200 came back with 169 and spent more than twice the
requests getting there, so 200 is not a number this listing is willing to print next to a price.

**Three honest caveats before you set `maxItems: 120`:**

1. **A narrow search returns what exists and then stops.** If only 40 public profiles anywhere match
   your terms, you get 40 and the run ends early. You are charged for the profiles actually
   delivered — never for the number you asked for. Asking for 120 and getting 40 costs you 40.
2. **Going deep trades some relevance for volume.** The first results come from the main index slice
   and are the closest matches. The deeper ones come from LinkedIn's international profile pages,
   where the index has verified your terms appear on the page but the person may not live in the
   city you typed — a Toronto or Milan profile that mentions London will show up in a London search.
   This has always been how the location filter works (it is a text match, not a geo radius), but at
   `maxItems: 120` you will see much more of it than at 20. If precision matters more than volume,
   ask for fewer and add a job-title filter.
3. **Large runs back to back will return less than a large run on its own.** The public index
   rate-limits by network address, and a 120-profile search reads far more of it than a 20-profile
   one. Several maximum-size runs fired within a few minutes of each other will start seeing
   refusals, and a run that is refused everywhere writes a free diagnostic row and charges you
   nothing for results. If you need volume, space the big runs out; if you need it now, prefer
   several smaller runs over one after another at 120.

***

### What this does not do

An honest list, because these are the things people ask after they buy:

- **No email addresses or phone numbers.** They are not on the public profile page, so they are not
  here. This is a people search, not a contact-enrichment tool.
- **No full work history, skills, certifications or recommendations.** You get the current role and
  headline as published, not the whole CV.
- **No profile photos.**
- **No connection-degree, no "who viewed you", no InMail** — all of that requires an account.
- **Private and unindexed profiles are invisible.** Members who turned off public profile visibility
  do not appear anywhere in the public web index, and there is no keyless way around that.
- **Filters match the whole published page, not just the snippet you see.** If you search for
  "Product Manager" in "London", a returned profile does contain both — but the short description in
  the `snippet` field may not repeat them. That is why the row is kept: the index verified the match
  against the full page.
- **Location is a text match, not a geo radius.** `"San Francisco"` will not automatically pull in
  Oakland, and `"Greater London Metropolitan Area"` matches far less than plain `"London"`. Prefer
  the shorter, more common form of a place name.
- **Fields can be `null`.** Roughly a third of profiles publish a headline with no company in it, so
  `currentCompany` is often empty. Nothing is invented to fill the gap.
- **Result ordering is relevance from the public index**, not recency, and not a ranking you can
  control. Within a run the closest matches come first and the widened, less precise ones come
  last — so if you only want the best matches, ask for fewer.
- **Nobody can guarantee a fixed number of profiles**, and this listing does not. A run returns what
  the public index holds for your terms on the day. What is guaranteed is the billing: one charge
  per profile row actually delivered to your dataset, and nothing at all for a run that finds none.

***

### Reliability

The public indexes rate-limit hard per address, so every request goes out through a different
rotating datacenter address with a real browser TLS fingerprint. Two independent indexes are read in
parallel and merged, so one of them being slow or unhappy does not empty your run.

If everything is refused at once — it happens, usually for a few minutes — you get a diagnostic row
saying so, the run succeeds, and no profiles are billed. Run it again shortly after.

Every request is capped at 8 seconds and the whole search at 90, so a stalled network turns into a
short run and an honest diagnostic, never a long expensive one.

***

### FAQ

**Do I need a LinkedIn account or cookies?**
No. Nothing is logged in, nothing is stored, and no cookie or session token is ever asked for. The
Actor reads the versions of LinkedIn profile pages that are published to the open web.

**Is this legal?**
It reads only publicly published pages, the same ones a search engine indexes and anyone can open in
a browser without logging in. No login wall is bypassed and no private data is touched. You are
responsible for how you use the results — if you are in the EU or UK and processing personal data,
GDPR applies to you regardless of where the data came from.

**How do I search for a specific person by name?**
Put the name in `searchQuery`, for example `{"searchQuery": "Sarah Chen", "currentCompanies": ["Stripe"]}`. Adding a company or a city cuts down the namesakes a lot.

**Can I filter by company using a LinkedIn company URL?**
Yes. `"currentCompanies": ["https://www.linkedin.com/company/airbnb/"]` works exactly like
`"currentCompanies": ["Airbnb"]`.

**Why did I get fewer profiles than `maxItems`?**
Because that many public profiles matched. `maxItems` is a ceiling, not a target, and you are only
charged for what you actually receive. Widen the search by adding more values to a filter.

**Why is `currentCompany` sometimes null?**
Because the member's published headline does not name an employer. It is left empty rather than
guessed at from a keyword list.

**Do I pay for a run that finds nothing?**
Only the $0.001 start fee. Zero profiles means zero profile charges, and the run still ends
successfully with a row explaining why.

**Does it work outside the US?**
Yes. Country subdomains come back as they are — `uk.linkedin.com`, `ug.linkedin.com`,
`in.linkedin.com` — and non-English profiles are parsed the same way.

**How fresh is the data?**
It reflects the public index, which lags a profile edit by days to weeks. `scrapedAt` records when
the row was collected. For a live read of one specific person, this is the wrong tool; for building
a list, it is the cheap one.

**Can I run it on a schedule?**
Yes, like any Actor — schedule it, use `publicIdentifier` as the key, and diff against your last
run.

**How do I call it from code?**
It is a standard Apify Actor, so the Apify API, the JavaScript and Python clients, the MCP server
and the integrations all work with it. Start a run with the JSON input above and read the default
dataset.

# Actor input Schema

## `searchQuery` (type: `string`):

Free-text people search, e.g. "software engineer" or "head of growth fintech". Combine it with the filters below or use it on its own.

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

Keep profiles whose public page mentions one of these places, e.g. "San Francisco". Several values are searched as OR, and are also used to widen the search when you ask for a lot of profiles. This is a text match against the whole public page, not a geo radius.

## `currentJobTitles` (type: `array`):

Keep profiles whose public page mentions one of these job titles, e.g. "Senior Software Engineer". Several values are searched as OR.

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

Keep profiles that mention one of these companies. A company name or a full LinkedIn company URL both work.

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

Keep profiles that mention one of these schools or universities, e.g. "Stanford University".

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

How many profiles to return in total. Maximum 120 — the box stops there because 120 is the figure that has actually been delivered in full on the cloud (measured 2026-08-17: "marketing manager" + London, 120 requested -> 120 distinct profiles in 18 seconds). A narrow or unusual search returns everything the public index holds for it and then stops early — you are charged per profile actually delivered, never for the number you asked for, so asking for 120 and getting 40 costs you 40.

## Actor input object example

```json
{
  "searchQuery": "software engineer",
  "locations": [
    "San Francisco",
    "New York"
  ],
  "currentJobTitles": [
    "Senior Software Engineer",
    "Staff Engineer"
  ],
  "currentCompanies": [
    "Stripe",
    "https://www.linkedin.com/company/airbnb/"
  ],
  "schools": [
    "Stanford University"
  ],
  "maxItems": 20
}
```

# Actor output Schema

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

One row per matching public LinkedIn profile in the default dataset: fullName, firstName, lastName, headline, location, currentPosition, currentCompany, education, connections, profileUrl, publicIdentifier, snippet, searchQuery, foundVia, scrapedAt. Empty or blocked runs return a single uncharged row instead.

# 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 = {
    "searchQuery": "software engineer",
    "locations": [
        "San Francisco"
    ],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("dami_studio/linkedin-profile-search-scraper").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 = {
    "searchQuery": "software engineer",
    "locations": ["San Francisco"],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("dami_studio/linkedin-profile-search-scraper").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 '{
  "searchQuery": "software engineer",
  "locations": [
    "San Francisco"
  ],
  "maxItems": 20
}' |
apify call dami_studio/linkedin-profile-search-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/zTzet69rohe0AlN9L/builds/tKcHUCTabZ6cJ2qMC/openapi.json
