# LinkedIn Public Scraper (Jobs, Companies, Posters) (`lovely_radiologist/linkedin-scraper`) Actor

Scrape public LinkedIn job listings and company profiles — no login, no cookies, no account risk. Get titles, companies, salaries, descriptions, employment type, and full company data. Features incremental updates, adaptive rate-limiting, and drift detection. Fast, lightweight, and compliant.

- **URL**: https://apify.com/lovely\_radiologist/linkedin-scraper.md
- **Developed by:** [Vivek Gaur](https://apify.com/lovely_radiologist) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.35 / 1,000 results

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?

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 Public Scraper — Jobs, Companies & More

**Extract public LinkedIn job listings and company pages — no login, no cookies, no account risk.**

![Output](https://img.shields.io/badge/Output-JSON%20%7C%20CSV%20%7C%20Excel%20%7C%20XML-blue?style=for-the-badge)
![No Login](https://img.shields.io/badge/Auth-None%20required-brightgreen?style=for-the-badge)
![Reliability](https://img.shields.io/badge/Adaptive-Rate--Limiting%20%2B%20Drift%20Detection-orange?style=for-the-badge)

![Hero Image](https://drive.google.com/uc?export=view\&id=1Q9G180SNPNcqIuWHK-85UNT-20OdMi12)

***

### 📑 Table of Contents

- [Why Use This Scraper?](#-why-use-this-scraper)
- [Quick Start](#-quick-start)
- [Programmatic Integration (API)](#-programmatic-integration-api)
- [AI & Agent Integrations (MCP)](#-ai--agent-integrations-mcp)
- [Input Parameters](#-input-parameters)
- [Output Format](#-output-format)
- [Incremental Mode](#-incremental-mode)
- [Adaptive Rate-Limiting](#-adaptive-rate-limiting)
- [Drift Detection (Canary)](#-drift-detection-canary)
- [Pricing](#-pricing)
- [Frequently Asked Questions](#-frequently-asked-questions-faq)
- [Legal & Disclaimer](#-legal--disclaimer)

***

### 🌟 Why Use This Scraper?

✅ **No Login, No Cookies** – Works entirely on LinkedIn's public, logged-out surface. No credentials, no CAPTCHAs, no account-ban risk.

✅ **Jobs + Companies in One Actor** – Scrape job listings *and* public company profiles in a single run. Most scrapers do only one.

✅ **Incremental Updates** – Only emit *new*, *changed*, or *expired* items versus a previous run. Keep scheduled scrapes cheap and fresh.

✅ **Adaptive Rate-Limiting** – Automatically slows down on 429/5xx and handles blocks gracefully, so it's resilient at scale — no hardcoded "safe" limits.

✅ **Drift Detection** – A canary mode alerts you the moment LinkedIn changes its page structure, **before** your users hit breakage.

✅ **Rich, Structured Data** – Full job details (salary, seniority, employment type, function, industries, benefits, applicants, Easy Apply, remote) + full company profiles (industry, size, employees, headquarters, website, founded, specialties).

✅ **Lightweight & Fast** – Plain HTTP + parsing (no heavy browser), so it's low-cost and quick to run.

![Screenshot: search results](https://drive.google.com/uc?export=view\&id=1GWz54x9Jl7IoW13oRt3ADngndR8bKqsF)
![Screenshot: job detail](https://drive.google.com/uc?export=view\&id=1vo6lwOE3seMpWn7NedZsxv1NaCNoaFkD)
![Screenshot: company profile](https://drive.google.com/uc?export=view\&id=1VVKxox2gb0KVYZ1thX6LsjGTAslqFGa2)
![Screenshot: output dataset](https://drive.google.com/uc?export=view\&id=1g8ADp3T2D29X9oF2grix6lYghvG_3uYU)

***

### ⚡ Quick Start

Get your data in three simple steps on the Apify platform:

1. **Open the Actor page.**
2. **Fill in the inputs** (e.g. Job Titles: `["Software Engineer"]`, Location: `United States`).
3. **Click "Start"** and wait for the run to finish.
4. **Download the data** in JSON, CSV, XML, Excel, or JSONL.

Or run it right from the Apify API (below).

***

### 💻 Programmatic Integration (API)

Integrate the scraper into your database, app, or automation pipeline using the Apify API.

#### Node.js Example

```javascript
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_APIFY_API_TOKEN' });

const input = {
    mode: 'jobs',
    jobsTitles: ['Software Engineer', 'Data Scientist'],
    location: 'United States',
    postedWithin: 'r604800',
    maxItems: 500,
};

(async () => {
    const run = await client.actor('YOUR_ACTOR_ID').call(input);
    const { items } = await client.dataset(run.defaultDatasetId).listItems();
    console.log(items);
})();
```

#### Python Example

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_API_TOKEN")

run_input = {
    "mode": "jobs",
    "jobsTitles": ["Software Engineer", "Data Scientist"],
    "location": "United States",
    "postedWithin": "r604800",
    "maxItems": 500,
}

run = client.actor("YOUR_ACTOR_ID").call(run_input=run_input)
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)
```

***

### 🤖 AI & Agent Integrations (MCP)

Feed real-time LinkedIn job data to your AI agents. The actor integrates with the Apify Model Context Protocol (MCP) server, so you can query it directly from Claude, ChatGPT, Cursor, and other MCP-compatible clients — great for job-market analysis, salary benchmarking, and hiring research pipelines.

***

### 📥 Input Parameters

See `.actor/input_schema.json` for the full form. Highlights:

| Field | Description |
|---|---|
| `mode` | `jobs` · `companies` · `both` |
| `searchUrls` | Paste LinkedIn jobs search URLs (from incognito). Overrides filters when set. |
| `jobsTitles` / `keywords` | Job titles to search. Each runs its own search; results are merged & de-duplicated. |
| `location` / `geoId` / `cities` | Location targeting. `cities` splits a search across cities to bypass LinkedIn's ~1000-per-search cap. |
| `companyNames` | Restrict job searches, or (in `companies`/`both` mode) the companies to profile. |
| `experienceLevel` / `employmentType` / `workArrangement` / `postedWithin` | Standard LinkedIn filters. |
| `easyApplyOnly` | Restrict results to Easy Apply jobs. |
| `extractContactEmail` | **Opt-in.** Extract the first email from a public job description. See GDPR note. |
| `enableDifferential` + `previousDatasetId` | Incremental mode — only emit NEW / CHANGED / EXPIRED rows. |
| `canaryMode` | Drift-detection canary against a fixed sample + stored known-good schema. |
| `proxyConfiguration` | Datacenter by default. Switch to residential if you hit authwalls. |
| `minDelayMs` / `maxDelayMs` / `maxConcurrency` | Throttling knobs. Adaptive backoff overrides these on errors. |

#### Example input

```json
{
  "mode": "jobs",
  "jobsTitles": ["Software Engineer", "Data Scientist"],
  "location": "United States",
  "postedWithin": "r604800",
  "extractContactEmail": false,
  "maxResultsPerSearch": 100,
  "maxItems": 500
}
```

***

### 📤 Output Format

#### Job row

`id`, `jobUrl`, `title`, `companyName`, `companyUrl`, `companyLogoUrl`, `location`, `postedAt`, `applicantsCount`, `salaryInfo`, `jobDescription`, `jobDescriptionHtml`, `seniorityLevel`, `employmentType`, `jobFunction`, `industries`, `benefits`, `workplaceTypes`, `workRemoteAllowed`, `contactEmail` (opt-in), `source`, `scrapedAt`.

#### Company row

`companyName`, `linkedinUrl`, `universalName`, `tagline`, `industry`, `description`, `website`, `employeesCount`, `companySize`, `organizationType`, `followersCount`, `foundedYear`, `headquarters`, `specialties`, `source`, `scrapedAt`.

Every row includes `source` and `scrapedAt` so you can trace where each record came from and when it was captured.

***

### 🔄 Incremental Mode

Enable `enableDifferential` and pass a `previousDatasetId` to compare against a prior run. The actor then emits **only**:

- **`new`** items not seen before,
- **`changed`** items whose data shifted,
- **`expired`** control rows (`{ type: 'expired', key, kind }`) for jobs/companies that disappeared.

This keeps scheduled runs cheap for you and the data fresh — you get changes, not a full re-scrape every time.

***

### 🛡️ Adaptive Rate-Limiting

No hardcoded "safe" numbers. The actor:

- Starts at your `minDelayMs` / `maxDelayMs`, paced with normally-distributed delays.
- On **429 / 5xx** → exponentially backs off and effectively reduces concurrency.
- On **403** or an **authwall redirect** → treats it as a hard/blocked error, surfaces it clearly, and doesn't hammer the endpoint.
- Recovers gradually after a run of clean requests.
- Logs its live mean delay so you can calibrate.

***

### 🚨 Drift Detection (Canary)

Run with `canaryMode: true` on a schedule (optionally set the `DRIFT_WEBHOOK_URL` env var for alerts). It re-scrapes a small, fixed sample of job/company URLs and compares the observed structure against a stored "known-good" snapshot in the key-value store. On a mismatch it logs, emits a `drift-alert` row, and pings the webhook — so you learn about a LinkedIn change **before** your users do.

***

### 💰 Pricing

Pay-per-event: `actor-start` once, `result-item` per output row.

**Limited-time launch price:** **$0.35 / 1,000 results** — undercutting the category while you get started. Configure the pricing model in the Apify Console → Pricing tab using the two events this actor emits (`actor-start`, `result-item`). Pricing is re-evaluated after measuring real unit economics.

***

### ❓ Frequently Asked Questions (FAQ)

**Q: Do I need a LinkedIn account or cookies?**
A: No. This actor uses LinkedIn's public, logged-out endpoints only. No account, no login, no cookies, no account-ban risk.

**Q: Is scraping public LinkedIn data legal?**
A: Scraping publicly available data is generally defensible under US law (*hiQ Labs v. LinkedIn*, 9th Cir. 2022). However, automated access still technically violates LinkedIn's Terms of Service, and bulk **resale** of scraped datasets is the risky edge. Use this for lawful research, market analysis, and job aggregation — do **not** resell the scraped datasets as a data product.

**Q: Can I scrape profiles, employees, or posts?**
A: No. Those require an authenticated session (Voyager) and are deliberately out of scope for this actor, which stays within public data only.

**Q: How many jobs can I scrape in one run?**
A: Up to ~1000 per search (LinkedIn's cap). By running multiple keywords/locations/cities you can exceed that in a single run, subject to your `maxItems` setting and proxy/rate-limit behavior.

**Q: How do I keep getting fresh jobs every day?**
A: Use the date filter (e.g. `postedWithin: "r86400"`) and schedule the actor daily. Combine with **incremental mode** to only see what's new or changed.

***

### ⚖️ Legal & Disclaimer

- This tool is intended for lawful research, market analysis, and job-aggregation use cases.
- It only touches data visible without authentication and does not bypass any access controls.
- The operator acts as a **data processor** on the requester's behalf; the requester is responsible for lawful basis. Data lives in the requester's dataset, not the operator's stores.
- Automated access technically violates LinkedIn's ToS; use responsibly and respect rate limits.
- The author is not responsible for misuse of this tool.

***

### 🛠️ Development

```bash
## from the monorepo root
npm run build --filter=linkedin-scraper
npm run typecheck --filter=linkedin-scraper

## local run against storage/INPUT.json
cd actors/linkedin-scraper && npm run start
```

Uses `apify`, `crawlee`, `got-scraping`, and `cheerio`. No browser is required, so it runs on the lightweight `apify/actor-node:20` image.

# Actor input Schema

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

What to scrape. All modes use LinkedIn's public (logged-out) endpoints only — no login, no cookies.

## `searchUrls` (type: `array`):

Optional. Paste full LinkedIn jobs search URLs (from incognito). When provided, the structured filters below are ignored.

## `autoConvertToAiSearch` (type: `boolean`):

LinkedIn's 2026 AI job search removed classic filters. When enabled, legacy filter params are converted to natural-language appended to keywords. Date/company/easy-apply/under-10 stay as URL filters.

## `jobsTitles` (type: `array`):

List of job titles to search (e.g. 'Software Engineer', 'Data Scientist'). Each runs its own search; results are merged and deduplicated.

## `keywords` (type: `string`):

Optional single free-text search term (e.g. 'software engineer remote'). Combined with Job titles above.

## `location` (type: `string`):

Country, city, region, or 'Remote' (e.g. 'United States', 'London, United Kingdom').

## `geoId` (type: `string`):

Optional precise LinkedIn geo id (found as geoId= in a search URL). Overrides location.

## `cities` (type: `array`):

Optional. Split a location search across cities to bypass the ~1000-per-search cap. Deduplicates results.

## `companyNames` (type: `array`):

Optional. Restrict job searches to these companies.

## `experienceLevel` (type: `string`):

Filter jobs by required experience level.

## `employmentType` (type: `string`):

Filter jobs by employment type.

## `workArrangement` (type: `string`):

Filter jobs by work location type (on-site, remote, or hybrid).

## `postedWithin` (type: `string`):

Only return jobs posted within this time window.

## `easyApplyOnly` (type: `boolean`):

Only scrape jobs that support LinkedIn Easy Apply.

## `extractContactEmail` (type: `boolean`):

OPT-IN. Extracts the first email found in a public job description. Contact email is personal data — see README for the lawful-basis and per-individual opt-out notes before enabling.

## `fetchFullDetail` (type: `boolean`):

When OFF (default), returns search-card data (title, company, location, salary, URL) — fast and works from datacenter IPs. When ON, also fetches the full description and criteria — but LinkedIn's detail endpoint rate-limits heavily, so this realistically requires RESIDENTIAL proxies at scale.

## `maxResultsPerSearch` (type: `integer`):

Cap jobs per search URL. Default is 25 to run quickly.

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

Hard cap on total output rows across all modes. Default 50 for fast runs without hitting timeouts.

## `enableDifferential` (type: `boolean`):

Only emit items that are NEW or CHANGED versus a previous dataset. Pass the previous dataset ID below.

## `previousDatasetId` (type: `string`):

Dataset ID of a prior run to diff against when differential mode is enabled.

## `canaryMode` (type: `boolean`):

Re-scrape a small fixed sample and compare to a stored 'known-good' schema snapshot, alerting on schema drift before users hit breakage.

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

Datacenter proxies recommended (cheapest) for the public surface. Residential only if you see authwall/blocking; public jobs/company endpoints rarely need it.

## `minDelayMs` (type: `integer`):

Minimum random delay between requests. The adaptive rate-limiter raises this automatically on errors.

## `maxDelayMs` (type: `integer`):

Maximum random delay between requests.

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

Maximum number of parallel searches/requests at once.

## Actor input object example

```json
{
  "mode": "jobs",
  "autoConvertToAiSearch": true,
  "keywords": "software engineer",
  "location": "United States",
  "experienceLevel": "",
  "employmentType": "",
  "workArrangement": "",
  "postedWithin": "",
  "easyApplyOnly": false,
  "extractContactEmail": false,
  "fetchFullDetail": false,
  "maxResultsPerSearch": 25,
  "maxItems": 50,
  "enableDifferential": false,
  "canaryMode": false,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "DATACENTER"
    ]
  },
  "minDelayMs": 800,
  "maxDelayMs": 2500,
  "maxConcurrency": 4
}
```

# Actor output Schema

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

The scraped LinkedIn jobs and companies stored in the default dataset. Use the Jobs and Companies views to browse the data.

## `jobsView` (type: `string`):

Scraped LinkedIn job listings: titles, companies, locations, salaries, descriptions, seniority, employment type, and more.

## `companiesView` (type: `string`):

Scraped public LinkedIn company profiles: industry, size, employees, headquarters, website, founded year, and specialties.

# 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 = {
    "keywords": "software engineer",
    "location": "United States",
    "maxResultsPerSearch": 25,
    "maxItems": 50,
    "minDelayMs": 800,
    "maxDelayMs": 2500,
    "maxConcurrency": 4
};

// Run the Actor and wait for it to finish
const run = await client.actor("lovely_radiologist/linkedin-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 = {
    "keywords": "software engineer",
    "location": "United States",
    "maxResultsPerSearch": 25,
    "maxItems": 50,
    "minDelayMs": 800,
    "maxDelayMs": 2500,
    "maxConcurrency": 4,
}

# Run the Actor and wait for it to finish
run = client.actor("lovely_radiologist/linkedin-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 '{
  "keywords": "software engineer",
  "location": "United States",
  "maxResultsPerSearch": 25,
  "maxItems": 50,
  "minDelayMs": 800,
  "maxDelayMs": 2500,
  "maxConcurrency": 4
}' |
apify call lovely_radiologist/linkedin-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,lovely_radiologist/linkedin-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/OibvoWvk1jWYlYe93/builds/g4rhfoumZR9xKfW09/openapi.json
