# LinkedIn Jobs Scraper | Experience Filters & Company Data (`corvuslab/linkedin-jobs-scraper`) Actor

Scrape LinkedIn job listings worldwide by keyword and place or from search URLs, no login. Experience-level and job-type filters that really work, company website, size, industry, followers and HQ address, parsed pay range, applicant count, Easy Apply, plus incremental alerts.

- **URL**: https://apify.com/corvuslab/linkedin-jobs-scraper.md
- **Developed by:** [Corvuslab](https://apify.com/corvuslab) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.25 / 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?

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

### What does LinkedIn Jobs Scraper do?

LinkedIn Jobs Scraper extracts public job listings from **[linkedin.com/jobs](https://www.linkedin.com/jobs)** in **137 countries**. Its headline is **experience-level and job-type filters that actually work**: LinkedIn's public search quietly ignores both, so this Actor checks each posting's own seniority and employment type, and jobs that don't match are skipped and cost nothing. Pay, company size, industry, keyword and agency filters work the same way. Each job comes with up to **70 fields**: full description, seniority, job type, applicants, Easy Apply or external, matched **skills**, a one-line **AI summary**, pay when the employer lists it, and an optional **company profile** (website, industry, size, followers, HQ address) at no extra cost.

Search by keyword and place, a whole country, a region group like DACH or the EU-27, or a company name, or paste LinkedIn search and job URLs. With no input at all you get the newest US jobs. **Incremental mode** returns and charges only new and changed jobs, with alerts to Telegram, Slack, Discord, WhatsApp or a webhook. Export to JSON, CSV or Excel, use the API, or feed an AI agent. **No LinkedIn login or cookies needed from you.**

> New to Apify? Sign up free and use the included monthly platform credit to try this Actor. No credit card needed.

**Why this scraper**

- 🎯 **Filters you can trust**: experience level and job type are checked on each posting, so "Entry level, Full-time" means exactly that.
- 🏢 **Company data included**: website, size, industry, followers and HQ address at the same per-job price.
- 💰 **Pay you can filter on**: employer-stated ranges parsed into numbers, currency and period, plus a yearly figure for comparing and filtering.
- ♻️ **Cheap to monitor**: incremental mode skips unchanged jobs before their posting is read, and skipped jobs cost nothing.

***

### What data can you extract from LinkedIn?

Every record carries up to **70 fields**, grouped like this:

| Group | What you get |
|---|---|
| **Job** | `title`, `url`, `id`, `location`, `country`, `countryCode`, `postedDate`, `postedAgo`, `listingBadge` (e.g. "Actively Hiring"), `isClosed` |
| **Job criteria & skills** | `seniorityLevel`, `employmentType`, `industries`, `applyType` (`EASY_APPLY` / `EXTERNAL`), `skills` (matched from the description), `language` (e.g. `en`, `de`), `isAgency` (posted by a staffing or recruiting firm) |
| **Pay** (when the employer lists it) | `salaryText`, `salaryMin`, `salaryMax`, `salaryCurrency` (ISO code), `salaryPeriod` (`YEAR`, `MONTH`, `HOUR`…), `salarySource`, plus `salaryYearlyMin` / `salaryYearlyMax` |
| **Demand & hiring team** | `applicantsCount` as a number, `applicantsText`, plus `jobPosterName`, `jobPosterTitle` and `jobPosterProfileUrl` when LinkedIn shows the hiring team |
| **Description, summary & contacts** | `description` as plain text, `descriptionHtml`, `descriptionMarkdown`, `aiSummary` (one line for AI agents and ATS imports), plus `extractedEmails`, `extractedPhones`, `extractedUrls` and `socialProfiles` when the employer writes them into the posting |
| **Company** | `company`, `companyUrl`, `companySlug`, `companyLogo`, and with company details on: `companyId`, `companyWebsite`, `companyIndustry`, `companySize`, `companyEmployeesOnLinkedIn`, `companyFollowers`, `companyHeadquarters`, `companyStreetAddress`, `companyPostalCode`, `companyCountryCode`, `companyType`, `companySpecialties`, `companySlogan`, `companyDescription` |
| **Monitoring** | `changeType`, `firstSeenAt`, `lastSeenAt`, `previousSeenAt`, `expiredAt`, `isRepost`, `repostOfId`, `repostDetectedAt`, `contentHash`, plus `searchKeyword` / `searchLocation` showing which search found the job |

***

### Key features

- 🎯 **Experience-level and job-type filters that actually work**: LinkedIn's public search ignores both and returns every level and type. This Actor reads each posting's own seniority (Internship to Executive) and employment type (Full-time, Contract…) and keeps only the matches. **Non-matching jobs are not charged.**
- 🏢 **Company profiles in the same price**: website, industry, size band, employees on LinkedIn, followers, HQ street address, postal and country code, type and specialties. No add-on event; each company is read once per run.
- 💰 **Pay as numbers, and a pay filter**: `CA$55,000.00/yr - CA$60,000.00/yr` becomes min 55000, max 60000, CAD, YEAR, plus yearly equivalents for hourly, daily, weekly and monthly pay. Filter by a yearly minimum or maximum in 15 currencies.
- 🌍 **Countries and region groups**: 137 countries, or one click for DACH, Nordics, Benelux, UK & Ireland, EU-27, GCC, MENA, ASEAN, Latin America or the Anglosphere. Each country is its own search with LinkedIn's full 1,000-job view.
- 🏢 **Companies by name**: type "Google" or paste a company URL; it's matched to the right LinkedIn company and searched exactly.
- 🚫 **Skip staffing agencies**: one switch drops staffing, recruiting and headhunting firms, by the employer's LinkedIn industry and name. Every record carries `isAgency`.
- 🎚️ **More filters, none charged when they drop a job**: description must contain (e.g. "visa sponsorship"), exclude keywords, industry, company size, hiring team shown, title words, excluded companies, known job IDs. Plus LinkedIn's Easy Apply, under 10 applicants and a custom **last N hours** window.
- 🧠 **Skills, language and an AI summary**: `skills` are matched against a curated skill list (most postings get at least one), `language` is the description's language, and `aiSummary` puts the whole job in one line.
- ♻️ **Incremental monitoring**: schedule it and get only NEW / UPDATED / REAPPEARED / EXPIRED jobs. Unchanged jobs are never charged; re-posts can be flagged or skipped.
- 🔔 **Notifications**: Telegram, Slack, Discord, WhatsApp or any webhook (n8n / Make / Zapier).

***

### 🚀 How to scrape LinkedIn jobs

1. Enter one or more **keywords** (e.g. `data analyst`) and **places** like `London, England, United Kingdom`, or pick **countries** or a **region group**. Or paste a LinkedIn job-search URL.
2. Pick your **filters**: experience level, job type, posted within, pay, company size, industry, description words, agencies.
3. Set **Max results** and decide whether to **add company details**.
4. (Optional) Turn on **incremental mode** and a **notification** channel, then **Schedule** it.
5. Click **Start** and download the results as **JSON, CSV or Excel**, or pull them from the **API**.

No LinkedIn account, no login, no cookies: it runs out of the box.

#### Quick-start example input

```json
{
  "searchTerms": ["data analyst"],
  "locations": ["London, England, United Kingdom"],
  "experienceLevels": ["entryLevel", "associate"],
  "employmentTypes": ["fullTime"],
  "postedWithin": "pastWeek",
  "includeCompanyDetails": true,
  "maxResults": 100
}
```

This returns up to 100 full-time, entry-level or associate data analyst jobs posted in London in the past week. Each one really carries that seniority and job type, plus the full description, applicants and the hiring company's profile.

***

### Input

Use the visual editor or pass JSON via the API. **58 inputs**, grouped as in the editor.

#### 🔎 Search

| Field | Type | What it does |
|---|---|---|
| `searchTerms` | array | Job titles or skills, as typed on LinkedIn; each keyword is its own search in every place. Empty = newest jobs. |
| `locations` | array | Countries, regions or cities (`"Berlin"`) or a LinkedIn geoId. Empty = United States. |
| `countries` | array (select) | 137 countries, one search each. |
| `regionPresets` | array (select) | `dach`, `nordics`, `benelux`, `ukIreland`, `eu27`, `gcc`, `mena`, `asean`, `latam`, `anglosphere`; one search per member country. |
| `distance` | string (select) | Radius around a city: 5 to 100 miles. |
| `startUrls` | array | LinkedIn job-search URLs (filters kept) or job URLs; replaces the search form. |
| `maxResults` | integer | Maximum jobs across all searches (default 25, 0 = no limit). |
| `maxResultsPerSearch` | integer | Cap each keyword × place search (up to 1,000). |

#### 🎚️ Filters

| Field | Type | What it does |
|---|---|---|
| `postedWithin` | enum | Any time, `past24Hours`, `pastWeek` or `pastMonth`. |
| `postedWithinHours` | integer | Custom window in hours (1–720), e.g. 1 or 6 for frequent schedules. Overrides `postedWithin`. |
| `experienceLevels` | array (enum) | `internship`, `entryLevel`, `associate`, `midSenior`, `director`, `executive`, `notApplicable`, checked on each posting. |
| `employmentTypes` | array (enum) | `fullTime`, `partTime`, `contract`, `temporary`, `internship`, `volunteer`, `other`, checked on each posting. |
| `easyApplyOnly` | boolean | Only jobs you can apply to directly on LinkedIn. |
| `under10Applicants` | boolean | Only jobs fewer than 10 people have applied to so far. |
| `companies` | array | Only jobs at these companies, by name (`"Google"`) or LinkedIn company URL. |
| `companyIds` | array | The same by LinkedIn company ID (the number after `f_C=` in a company's job-search URL, e.g. `1441`). |
| `titleIncludes` / `titleExcludes` | array | Title must / must not contain these words. |
| `excludeCompanies` | array | Drop jobs from companies whose name contains any of these. |
| `excludeAgencies` | boolean | Drop jobs from staffing, recruitment and headhunting firms. |
| `excludeKeywords` | array | Drop jobs whose title or description mentions any of these, e.g. `"commission only"`. |
| `descriptionIncludes` | array | Keep only jobs whose description mentions one of these, e.g. `"visa sponsorship"`. |
| `industryIncludes` | array | Keep only jobs whose LinkedIn industry contains one of these, e.g. `"Software Development"`. |
| `requireSalary` | boolean | Keep only jobs where the employer lists pay. |
| `salaryMin` / `salaryMax` | integer | Yearly pay bounds (hourly × 2080, daily × 260, weekly × 52, monthly × 12). |
| `salaryCurrency` | enum | Currency of the bounds: `USD` (default), `EUR`, `GBP`, `CAD`, `AUD`, `INR`, `CHF` and 8 more. |
| `includeUnknownSalary` | boolean | Also keep jobs without comparable pay. |
| `requireJobPoster` | boolean | Keep only jobs that show the hiring team. |
| `companySizeMin` / `companySizeMax` | integer | Company size band bounds, e.g. 51 to 200 (reads company details automatically). |
| `skipJobIds` | array | Job IDs or URLs you already have. |

Every filter from `experienceLevels` down is checked by the Actor itself, and a job it drops is never charged.

#### 📦 Output

| Field | Type | What it does |
|---|---|---|
| `includeDetails` | boolean | Read the full posting (default on). Off = a faster, card-only run. |
| `includeCompanyDetails` | boolean | Add the hiring company's profile. |
| `descriptionFormat` | enum | `all`, `text`, `html` or `markdown`. |
| `descriptionMaxLength` | integer | Cut descriptions to this many characters (0 = full length). |
| `compact` | boolean | Core fields only, for AI agents and MCP clients. |
| `excludeEmptyFields` | boolean | Drop null, empty-string and empty-list fields. |

#### ♻️ Incremental monitoring

| Field | Type | What it does |
|---|---|---|
| `incrementalMode` | boolean | Tag jobs NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED; unchanged ones aren't charged. |
| `stateKey` | string | Stable name for the tracked search (auto-derived from your settings if blank). |
| `emitUnchanged` | boolean | Also return jobs that haven't changed. |
| `emitExpired` | boolean | Return jobs that were listed last run but are gone now. |
| `skipReposts` | boolean | Leave out jobs re-posted under a new ID. |

#### 🔔 Notifications

Channels are off unless filled in. Credentials are encrypted, masked and never logged.

| Field | Type | What it does |
|---|---|---|
| `telegramToken` + `telegramChatId` | string | Telegram bot token (from @BotFather) + chat or channel ID. |
| `slackWebhookUrl` / `discordWebhookUrl` | string | Incoming webhook URL. |
| `whatsappAccessToken` + `whatsappPhoneNumberId` + `whatsappTo` | string / array | WhatsApp Cloud API token, your business phone-number ID and recipient numbers. |
| `webhookUrl` + `webhookHeaders` | string / object | POST every matched job (up to 1,000 per run) as JSON. |
| `notificationLimit` | integer | Jobs listed per chat message (1–20, default 5). |
| `notifyOnlyChanges` | boolean | In incremental mode, alert only on new, updated and reappeared jobs. |

#### ⚙️ Advanced

| Field | Type | What it does |
|---|---|---|
| `proxyConfiguration` | object | Connection settings. Keep the default for reliable runs. |
| `detailConcurrency` | integer | How many postings to read at the same time (1–16, default 10). |
| `ignoreUrlFailures` | boolean | Skip unreadable start URLs instead of failing. |
| `maxRequestRetries` | integer | Retries for a failed request (0–10, default 6). |

#### More example inputs

Paste a LinkedIn search URL (its filters are kept) and a single job URL:

```json
{
  "startUrls": [
    { "url": "https://www.linkedin.com/jobs/search?keywords=product%20manager&location=Germany&f_TPR=r86400" },
    { "url": "https://www.linkedin.com/jobs/view/4480525670" }
  ],
  "includeCompanyDetails": true
}
```

Watch two searches in two cities every few hours, skip agencies and get a Telegram alert for each new job:

```json
{
  "searchTerms": ["python developer", "backend engineer"],
  "locations": ["Berlin", "Munich"],
  "postedWithinHours": 6,
  "excludeAgencies": true,
  "incrementalMode": true,
  "notifyOnlyChanges": true,
  "telegramToken": "<your bot token>",
  "telegramChatId": "@yourchannel",
  "maxResults": 0
}
```

Data engineering jobs across the Nordics that state at least 60,000 a year in euros, at companies of 51+ people:

```json
{
  "searchTerms": ["data engineer"],
  "regionPresets": ["nordics"],
  "salaryMin": 60000,
  "salaryCurrency": "EUR",
  "companySizeMin": 51,
  "maxResultsPerSearch": 200
}
```

Senior roles at named companies that still have few applicants:

```json
{
  "companies": ["Google", "https://www.linkedin.com/company/microsoft"],
  "countries": ["US", "GB"],
  "titleIncludes": ["senior", "staff"],
  "under10Applicants": true
}
```

***

### Output

Each dataset item is one LinkedIn job. A real record (descriptions shortened here):

```json
{
  "id": "4480525670",
  "url": "https://www.linkedin.com/jobs/view/4480525670",
  "title": "Junior Accountant",
  "company": "Peak Associates Limited",
  "companyUrl": "https://ca.linkedin.com/company/peak-associates-limited",
  "location": "North York, Ontario, Canada",
  "country": "Canada",
  "countryCode": "CA",
  "postedDate": "2026-10-16",
  "postedAgo": "6 days ago",
  "listingBadge": "Actively Hiring",
  "salaryText": "CA$55,000.00/yr - CA$60,000.00/yr",
  "salaryMin": 55000.0,
  "salaryMax": 60000.0,
  "salaryCurrency": "CAD",
  "salaryPeriod": "YEAR",
  "salarySource": "EMPLOYER",
  "salaryYearlyMin": 55000.0,
  "salaryYearlyMax": 60000.0,
  "applicantsCount": 200,
  "applicantsText": "Over 200 applicants",
  "seniorityLevel": "Associate",
  "employmentType": "Full-time",
  "industries": "Staffing and Recruiting",
  "applyType": "EASY_APPLY",
  "isAgency": true,
  "isClosed": false,
  "skills": ["Excel", "Accounting", "GAAP", "Accounts Payable", "Accounts Receivable", "CPA", "Time Management"],
  "language": "en",
  "aiSummary": "Junior Accountant at Peak Associates Limited (North York, Ontario, Canada) · Full-time · Associate · pay CA$55,000.00/yr - CA$60,000.00/yr · Easy Apply · Over 200 applicants · posted 2026-10-16 · industry: Staffing and Recruiting · skills: Excel, Accounting, GAAP, Accounts Payable, Accounts Receivable, CPA, Time Management",
  "description": "Our client is seeking a Junior Accountant reporting to the Director of Accounting to join their team in Toronto. …",
  "companyWebsite": "http://www.peakassociates.com",
  "companyIndustry": "Staffing and Recruiting",
  "companySize": "11-50 employees",
  "companyEmployeesOnLinkedIn": 10,
  "companyFollowers": 58622,
  "companyHeadquarters": "Toronto, ON",
  "companyStreetAddress": "2345 Yonge Street, Suite 505",
  "companyPostalCode": "M4P 2E5",
  "companyCountryCode": "CA",
  "companyType": "Privately Held",
  "companySpecialties": ["Recruitment", "Temporary Staffing Services", "Talent Acquisition", "Executive Search"],
  "source": "linkedin.com",
  "searchKeyword": "accountant",
  "searchLocation": "Toronto",
  "scrapedAt": "2026-10-23T12:34:26.021163+00:00",
  "detailFetched": true
}
```

Descriptions also come as `descriptionHtml` and `descriptionMarkdown`. This posting comes from a recruitment firm, so it has `isAgency: true`, and **Exclude staffing & recruiting agencies** would have dropped it at no cost. In incremental mode each record carries a `changeType` (NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED) with `firstSeenAt` / `lastSeenAt`, and EXPIRED jobs get `expiredAt`. A job read by its URL after it stopped taking applications comes back with `isClosed: true`.

#### Data fields

Card fields are always there; **Fetch the full job posting** adds description, criteria, skills, language, AI summary, agency flag, applicants, apply type, pay and hiring team; **Add company details** adds the company profile including `companyId`.

Every field is present in standard mode (missing values are `null`); **compact mode** returns the core fields only, for lean AI/MCP payloads; **`excludeEmptyFields`** drops empties entirely. The Output tab ships two ready-made views: **Overview** (one row per job) and **Companies** (website, industry, size, HQ per open role).

***

### ♻️ How to monitor LinkedIn jobs with incremental mode

Schedule the Actor and turn on **incremental mode**: each run compares against the last and returns only **NEW / UPDATED / REAPPEARED / EXPIRED** jobs. Unchanged jobs are recognised from the search results *before* their posting is read, so you skip both the work and the cost.

| | First run (baseline) | Next run (nothing changed) |
|---|---|---|
| Jobs found in the search | all | all |
| Postings read | all | **none** |
| Jobs returned & charged | all | **none** |

Your cost after the baseline scales with how much the search changes, not with how much you watch:

| Daily churn | of 1,000 tracked | billable jobs | you save |
|---|---|---|---|
| 5 % | 1,000 | 50 | **95 %** |
| 15 % | 1,000 | 150 | **85 %** |
| 30 % | 1,000 | 300 | **70 %** |

Re-posts under a new ID are flagged with `isRepost` / `repostOfId`, or left out with **Skip re-posted jobs**. **EXPIRED** needs a run that reads the whole search: Max results 0, and each search under 1,000 jobs.

#### Setting up scheduled monitoring

1. Configure your search and filters; a **last N hours** window matching your schedule keeps runs small.
2. Turn on **Incremental mode**.
3. Run it once to seed the baseline.
4. Open the **Schedules** tab and set a recurring run (e.g. every hour or every morning).
5. Add a notification channel with **Alert only on changes** on.

***

### 🔔 How to set up LinkedIn job alerts

Fill in only the channels you want:

| Channel | What to configure |
|---|---|
| ✈️ **Telegram** | `telegramToken` (from @BotFather) + `telegramChatId` |
| 💬 **Slack** | `slackWebhookUrl` (Incoming Webhook URL) |
| 🎮 **Discord** | `discordWebhookUrl` (channel webhook URL) |
| 🟢 **WhatsApp** | `whatsappAccessToken` + `whatsappPhoneNumberId` (WhatsApp Cloud API) + `whatsappTo` numbers |
| 🪝 **Webhook** | `webhookUrl` receives every matched job as structured JSON, ideal for n8n / Make / Zapier |

Each message lists the title, company, place, pay and link for up to `notificationLimit` jobs. A broken channel can't stop the scrape or the others. WhatsApp follows Meta's 24-hour rule: a free-form message only reaches a number that has written to your business number in the last 24 hours, so have each recipient message it first. Pair incremental mode with **Alert only on changes** to hear only about genuinely new jobs.

***

### 💡 What can you do with LinkedIn data?

#### Get job alerts that match your level

Set your level, Full-time and a 1-hour window, schedule it hourly and get a ping for every fresh posting. Add fewer than 10 applicants to apply early.

#### Recruiting and sales lead generation

Every company posting a job is hiring. Get its website, size, industry and HQ with each job, filter by size, and drop agencies for a clean list of direct employers.

#### Salary benchmarking

Collect employer-stated pay ranges with currency and period, and compare roles, levels and countries on the yearly figures. Turn on **Only jobs with a stated pay range** to skip the rest at no cost.

#### Labour-market and competitor research

Track how many roles a competitor opens, where, at which levels and with which skills, week by week. Region groups cover a whole market in one run.

#### Feed AI agents and LLM pipelines

`aiSummary` gives an agent or ATS the whole job in one line, and `skills` is ready for CV matching. Compact mode and `excludeEmptyFields` keep token costs low.

***

### 💰 How much does it cost to scrape LinkedIn?

This Actor uses Apify's **pay-per-event** model: a tiny fee when a run starts, plus one fee for each job you receive. See the **Pricing** tab for the live numbers.

The full posting and the company profile are part of that one price: there is no separate charge for company data. Three things cut your bill further:

- **Filters that skip before charging**: jobs that fail the experience-level, job-type, pay, company-size, industry, keyword, agency or title filters are dropped and never charged.
- **Incremental mode**: after the baseline you pay only for what changed (the table above).

***

### 🔌 Integrations & export

Export to **JSON, CSV, Excel** or an HTML table, or pull from the **REST API** and the **JavaScript / Python** clients. Runs on a **schedule**, connects to **Google Sheets, Slack, Make, Zapier and n8n**, and works as an **MCP tool** for AI agents. Compact mode keeps token usage small.

Also covering jobs and employers: our [XING Jobs Scraper](https://apify.com/corvuslab/xing-jobs-scraper) (DACH), [Glassdoor Job Scraper](https://apify.com/corvuslab/glassdoor-scraper) and [Greenhouse, Lever & Ashby Jobs Scraper](https://apify.com/corvuslab/greenhouse-jobs-scraper).

***

### 🔗 Using the API

Run this Actor from your own code. Example with the Apify Python client:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_API_TOKEN>")

run_input = {
    "searchTerms": ["data engineer"],
    "locations": ["Canada"],
    "experienceLevels": ["midSenior"],
    "postedWithin": "pastWeek",
    "includeCompanyDetails": True,
    "maxResults": 50,
}

run = client.actor("corvuslab/linkedin-jobs-scraper").call(run_input=run_input)

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["title"], item["company"], item["salaryText"])
```

It also works with the JavaScript/TypeScript client, the Apify CLI and the REST API.

***

### ❓ FAQ

**Do I need a LinkedIn account, login or cookies?** No. It reads LinkedIn's public job listings, the ones anyone can open without signing in.

**Why do the experience-level and job-type filters work differently here?** LinkedIn's public search ignores both, so the Actor reads each posting and keeps only those whose own seniority and employment type match. Jobs that don't match are not charged. These filters always read the full posting, even with **Fetch the full job posting** off. Filters that keep only a sliver of jobs stop the run early; the log suggests a keyword instead.

**How many jobs can I get from one search?** LinkedIn shows at most 1,000 jobs per search. Go further with more places (cities, or a region group) or more keywords: each keyword × place is its own search with its own 1,000-job view, de-duplicated.

**Why is the pay range empty on most jobs?** LinkedIn shows pay only when the employer states it, a minority of postings that varies by country. Otherwise the pay fields are `null`; pay is never guessed. So a pay filter returns fewer jobs: jobs without stated pay, or in another currency, are left out unless you turn on **Keep jobs without comparable pay**.

**Do I get the external apply link?** No. LinkedIn doesn't show the employer's own application link to signed-out visitors. Every posting carries `applyType` instead: `EASY_APPLY` (apply on LinkedIn) or `EXTERNAL` (apply on the employer's site).

**Can I filter remote or hybrid jobs?** Not reliably. Public postings don't say whether a role is remote, hybrid or on-site, and "Remote" as a place doesn't filter for it. **Title must contain** `remote` keeps jobs whose title says so.

**Why is the hiring team empty on most jobs?** `jobPosterName`, `jobPosterTitle` and `jobPosterProfileUrl` are filled only when LinkedIn shows a "Meet the hiring team" box, which is a small share of jobs. **Only jobs that show the hiring team** keeps just those.

**How do I scrape all jobs from one company?** Type its name or paste its LinkedIn company URL into **Companies**, or use its numeric ID in **Company IDs**. Leave keywords empty for all its roles in your places. The log shows which company each name matched; if none can be matched, the run searches nothing, so you're never charged for unrelated jobs.

**How are skills found?** By matching the title and description against a curated list of skills and tools (Excel, Python, GAAP, Kubernetes…). A skill appears only when the posting names it.

***

### ⚖️ Is it legal to scrape LinkedIn?

This Actor collects only **publicly available job listings**: the postings and company pages LinkedIn shows to anyone signed out. It does not scrape member profiles; the only personal data is what a posting displays, such as a hiring-team contact's name and title. You're responsible for how you use the data, especially personal information, and for complying with LinkedIn's terms and applicable law (including the GDPR/LGPD). Not affiliated with, endorsed by or sponsored by LinkedIn Corporation.

***

**Keywords:** linkedin jobs scraper · linkedin job scraper · linkedin scraper · linkedin jobs api · scrape linkedin jobs · linkedin job postings · linkedin job listings export · linkedin jobs without login · linkedin company data · linkedin salary data · linkedin jobs by country · job market research · recruitment lead generation · job monitoring · linkedin job alerts · export to CSV · export to Excel · no-code scraper · MCP tool for AI agents

# Changelog

This Actor's version history is a separate document: https://apify.com/corvuslab/linkedin-jobs-scraper/changelog.md

# Actor input Schema

## `searchTerms` (type: `array`):

Job titles, skills or companies, as you would type them in LinkedIn's job search (e.g. "data analyst", "python developer"). Each keyword runs as its own search in every place below. Leave empty to list the newest jobs in each place.

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

Countries, regions or cities, as you would type them on LinkedIn: "United States", "Germany", "London, England, United Kingdom", "Berlin". A LinkedIn geoId (e.g. 101282230 for Germany) also works. Leave empty for LinkedIn's default market (United States).

## `countries` (type: `array`):

Pick whole countries instead of typing them. Each country runs as its own search (added to Places).

## `regionPresets` (type: `array`):

Search a group of countries at once. Each country runs as its own search, so every one gets LinkedIn's full 1,000 jobs.

## `distance` (type: `string`):

Widen each city to everything within this distance. Has no effect on country-wide searches.

## `startUrls` (type: `array`):

Optional. Paste LinkedIn job-search URLs (linkedin.com/jobs/search?keywords=…\&location=…, their filters are kept) or single job URLs (linkedin.com/jobs/view/…). Any start URL replaces the search form above.

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

Maximum number of jobs to return across all searches. Set 0 for no limit.

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

Cap each keyword × place search separately, so one big search can't use up the whole run. 0 = no per-search cap (LinkedIn shows at most 1,000 jobs per search).

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

Only jobs posted (or re-posted) in this window.

## `postedWithinHours` (type: `integer`):

A custom window, e.g. 1 for the last hour or 6 for the last six hours — handy for frequent scheduled runs. Overrides Posted within. 0 = off.

## `experienceLevels` (type: `array`):

Keep only jobs whose posting shows one of these seniority levels. Jobs that don't match are skipped and not charged.

## `employmentTypes` (type: `array`):

Keep only jobs whose posting shows one of these employment types. Jobs that don't match are skipped and not charged.

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

Only jobs you can apply to directly on LinkedIn.

## `under10Applicants` (type: `boolean`):

Only jobs that fewer than 10 people have applied to so far.

## `companyIds` (type: `array`):

Only jobs at these companies, by LinkedIn company ID: the number after f_C= in a company's job-search URL (e.g. 1441 for Google). Or use Companies (names or URLs) below.

## `companies` (type: `array`):

Only jobs at these companies, by name ("Google", "General Motors") or LinkedIn company URL (linkedin.com/company/…). Each is matched to LinkedIn's company and searched exactly.

## `titleIncludes` (type: `array`):

Keep only jobs whose title contains at least one of these words (case-insensitive), e.g. "senior", "lead".

## `titleExcludes` (type: `array`):

Drop jobs whose title contains any of these words, e.g. "intern", "sales". Dropped jobs are not charged.

## `excludeCompanies` (type: `array`):

Drop jobs from companies whose name contains any of these, e.g. staffing agencies you never want to see. Dropped jobs are not charged.

## `excludeAgencies` (type: `boolean`):

Drop jobs posted by staffing, recruitment and headhunting firms (by the employer's LinkedIn industry and name). Dropped jobs are not charged.

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

Drop jobs whose title or description mentions any of these, e.g. "security clearance", "commission only". Dropped jobs are not charged.

## `descriptionIncludes` (type: `array`):

Keep only jobs whose description mentions at least one of these (case-insensitive), e.g. "visa sponsorship", "Kubernetes".

## `industryIncludes` (type: `array`):

Keep only jobs whose LinkedIn industry contains one of these, e.g. "Software Development", "Hospitals", "Financial Services".

## `requireSalary` (type: `boolean`):

Keep only jobs where the employer lists pay. Many postings don't, so expect fewer results.

## `salaryMin` (type: `integer`):

Keep jobs whose stated pay reaches this yearly amount (hourly × 2080, daily × 260, weekly × 52, monthly × 12), in the currency below.

## `salaryMax` (type: `integer`):

Keep jobs whose stated pay starts at or below this yearly amount, in the currency below.

## `salaryCurrency` (type: `string`):

Currency of the pay bounds above. Jobs paid in another currency count as unknown pay. Default USD.

## `includeUnknownSalary` (type: `boolean`):

With a pay bound set, also keep jobs that don't state pay (or state it in another currency).

## `requireJobPoster` (type: `boolean`):

Keep only jobs where LinkedIn shows who posted the job (name, title, profile link).

## `companySizeMin` (type: `integer`):

Keep jobs at companies of at least this size (LinkedIn's size band, e.g. 51-200). Reads company details automatically.

## `companySizeMax` (type: `integer`):

Keep jobs at companies of at most this size (LinkedIn's size band). Reads company details automatically.

## `skipJobIds` (type: `array`):

Job IDs (or job URLs) you already have. They are skipped and not charged.

## `includeDetails` (type: `boolean`):

Add the full description, seniority level, employment type, industries, applicant count, employer-stated pay range and Easy Apply / external apply. Turn off for a faster, card-only run.

## `includeCompanyDetails` (type: `boolean`):

Add the hiring company's website, industry, size, employees on LinkedIn, followers, headquarters address, type, specialties and description. Each company is read once per run.

## `descriptionFormat` (type: `string`):

Which representation(s) of the job description to include.

## `descriptionMaxLength` (type: `integer`):

Cut descriptions to this many characters. 0 = full length.

## `compact` (type: `boolean`):

Emit only the core fields (title, company, place, date, pay, job type, seniority, link). Ideal for AI agents and MCP clients.

## `excludeEmptyFields` (type: `boolean`):

Remove null, empty-string and empty-list fields from each record.

## `incrementalMode` (type: `boolean`):

Track jobs between runs and tag every record NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED. Unchanged jobs are skipped and not charged.

## `stateKey` (type: `string`):

Stable name for the tracked search. Leave empty to derive one automatically from your search settings.

## `emitUnchanged` (type: `boolean`):

Also emit jobs that have not changed since the previous run.

## `emitExpired` (type: `boolean`):

Emit jobs that were listed last run but are gone now. Needs a run that reads the whole search (Max results 0 or above the number of matches, and under 1,000 matches per search).

## `skipReposts` (type: `boolean`):

Leave out jobs an employer took down and posted again under a new ID (same title, company and place as a job already tracked). They are not charged.

## `telegramToken` (type: `string`):

Bot token from @BotFather.

## `telegramChatId` (type: `string`):

Chat or channel ID, e.g. "-100123456789" or "@yourchannel".

## `slackWebhookUrl` (type: `string`):

Slack incoming-webhook URL.

## `discordWebhookUrl` (type: `string`):

Discord incoming-webhook URL.

## `whatsappAccessToken` (type: `string`):

WhatsApp Cloud API access token (Meta for Developers). Messages reach a number that has written to your business number in the last 24 hours.

## `whatsappPhoneNumberId` (type: `string`):

The Cloud API phone-number ID of your business number.

## `whatsappTo` (type: `array`):

Recipient numbers in international format, e.g. "4512345678".

## `webhookUrl` (type: `string`):

Any HTTPS endpoint. Receives a JSON POST with every matched job (up to 1,000 per run) — works with n8n, Make and Zapier.

## `webhookHeaders` (type: `object`):

Extra headers for the webhook request, e.g. {"Authorization": "Bearer xyz"}. Stored encrypted, like the other credentials.

## `notificationLimit` (type: `integer`):

How many jobs to list in each Telegram, Slack or Discord message. The webhook always gets every matched job.

## `notifyOnlyChanges` (type: `boolean`):

In incremental mode, alerts list only new, updated and reappeared jobs, even when Include unchanged jobs is on.

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

Keep the default proxy settings for reliable runs — LinkedIn slows down visitors who send many requests from one address.

## `detailConcurrency` (type: `integer`):

How many job postings to read at the same time.

## `ignoreUrlFailures` (type: `boolean`):

Skip start URLs that cannot be interpreted instead of failing the whole run.

## `maxRequestRetries` (type: `integer`):

How many times to retry a refused or failed request (each retry uses a fresh connection).

## Actor input object example

```json
{
  "searchTerms": [
    "data analyst"
  ],
  "locations": [
    "United States"
  ],
  "maxResults": 25,
  "easyApplyOnly": false,
  "under10Applicants": false,
  "excludeAgencies": false,
  "requireSalary": false,
  "includeUnknownSalary": false,
  "requireJobPoster": false,
  "includeDetails": true,
  "includeCompanyDetails": false,
  "descriptionFormat": "all",
  "descriptionMaxLength": 0,
  "compact": false,
  "excludeEmptyFields": false,
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "skipReposts": false,
  "notificationLimit": 5,
  "notifyOnlyChanges": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "detailConcurrency": 10,
  "ignoreUrlFailures": true,
  "maxRequestRetries": 6
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `allItems` (type: `string`):

No description

## `companies` (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 = {
    "searchTerms": [
        "data analyst"
    ],
    "locations": [
        "United States"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("corvuslab/linkedin-jobs-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 = {
    "searchTerms": ["data analyst"],
    "locations": ["United States"],
}

# Run the Actor and wait for it to finish
run = client.actor("corvuslab/linkedin-jobs-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 '{
  "searchTerms": [
    "data analyst"
  ],
  "locations": [
    "United States"
  ]
}' |
apify call corvuslab/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

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