# LinkedIn Job Listings with Salary & Company Data Scraper (`nimbryxo-studio/linkedin-jobs-scraper`) Actor

Collect LinkedIn job postings by keyword, location, or search URL, with parsed salary ranges, visa-sponsorship signals, and enriched employer profiles. No login required.

- **URL**: https://apify.com/nimbryxo-studio/linkedin-jobs-scraper.md
- **Developed by:** [Nimbryxo-studio](https://apify.com/nimbryxo-studio) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.49 / 1,000 results

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

## LinkedIn Job Listings with Salary & Company Data Scraper

Turn LinkedIn's public job board into a clean spreadsheet. Search by keywords, locations and employers, or paste job links, and get one row per posting with the full description, a yearly salary range with its source, applicant numbers, Easy Apply status, seniority, the recruiter when LinkedIn shows one, and the employer's LinkedIn profile. No LinkedIn account, login or cookies needed.

### What this actor does

- **Salary you can sort by.** Pay is parsed into minimum, maximum, currency and period, then converted to yearly figures. It comes from LinkedIn's own pay box or, when the employer only wrote it in the description, from that text. `salarySource` tells you which.
- **Many searches in one run.** Several keywords times several locations, company-only searches, pasted LinkedIn search links and individual job links, merged without duplicates.
- **Filters that really narrow the results.** Date posted (from the past hour to the past 3 months), distance, Easy Apply, fewer than 10 applicants, companies to include or exclude, experience level, job type, industry, staffing agencies, salary and recruiter.
- **Employer profile included.** Industry, size, headquarters, website, founding year, office addresses, follower count, tagline and description, looked up once per company.
- **Empty fields are omitted.** Fields LinkedIn doesn't show for a posting are left out instead of filled with blanks or guesses.

### Output per job

| Field | What it contains |
|---|---|
| `id`, `title`, `jobUrl` | LinkedIn job ID, job title and link |
| `companyName`, `companyUrl`, `companyId`, `companyNumericId`, `companyLogo` | Employer name, LinkedIn page, page name, numeric company ID and logo |
| `location` | Location as LinkedIn shows it |
| `postedDate`, `postedDateText`, `postedAt` | Date the job was posted or last reposted, LinkedIn's "2 days ago" text, and the exact UTC posting time when LinkedIn's page shows it (Extended job page depth asks again when it's missing) |
| `expiresAt` | LinkedIn's own expiry date and time for the posting, when the job page includes structured data (present for many, but not all, postings); omitted otherwise |
| `salary`, `salaryRange`, `salarySource` | Pay as written, parsed `{min, max, currency, interval, minYearly, maxYearly}`, and `employer` (LinkedIn's pay box) or `description` |
| `applicantCount`, `applicantCountNumeric`, `applicantCountKind`, `applicantInsights` | "Over 200 applicants" as shown, the number, whether it is `exact`, `moreThan` or `fewerThan`, and early-applicant notes |
| `listingBadge`, `activelyHiring` | Badge on the search result, such as "Actively Hiring" or "Be an early applicant" |
| `easyApply`, `applyUrl` | Whether you can apply on LinkedIn, and the LinkedIn apply page |
| `descriptionText`, `descriptionHtml` | Full job description as plain text and HTML |
| `responsibilities`, `qualifications`, `preferredQualifications` | Bullet lists from the description when it has those sections |
| `seniorityLevel`, `inferredSeniorityLevel`, `employmentType`, `industries`, `industriesList` | LinkedIn's job criteria, plus a seniority taken from the title when LinkedIn says "Not Applicable" |
| `experienceYearsMin`, `experienceYearsMax` | Years of experience asked for in the description |
| `visaSponsorshipMentioned`, `visaSponsorshipPhrase` | Whether the description offers or rules out visa sponsorship, with the sentence |
| `remoteAllowed` | `true` when the title or description clearly says the job is fully remote, or when the job page's structured data marks it `TELECOMMUTE`. It is never set from hybrid or on-site guesses |
| `recruiter` | `{name, title, profileUrl, photoUrl}` of the person who posted the job, when LinkedIn shows them |
| `contactEmails` | Email addresses written in the description |
| `atsPlatform` | Hiring system (Workday, Greenhouse, Lever…) when the description links to it |
| `companyIndustry`, `companySize`, `companyHeadquarters`, `companyType`, `companyWebsite`, `companyFounded`, `companySpecialties`, `companyLocations`, `companyFollowersCount`, `companySlogan`, `companyDescription` | Employer profile from LinkedIn |
| `companyAffiliatedPages`, `companySimilarPages` | `{name, url, industry}` of the employer's subsidiaries and of similar companies, as listed on its LinkedIn page |
| `companyRecentPosts` | The employer's latest public LinkedIn posts: `{url, postedAt, text, reactionsCount, commentsCount}` |
| `similarJobs`, `peopleAlsoViewed`, `similarSearches`, `industryIds`, `titleId`, `titleCategoryLabel`, `titleCategoryUrl` | Related jobs and LinkedIn's own categories (Extended job page depth only) |
| `searchQuery`, `searchLocation`, `searchUrl` | Which of your searches found the job |
| `scrapedAt` | When the row was collected |

The dataset has three ready-made views: **Jobs**, **Salaries** (yearly minimum and maximum side by side) and **Employers**.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `searchQueries` | array | – | Keywords to search, one per line. Each one is searched in every location |
| `searchUrls` | array | – | LinkedIn job search links copied from your browser |
| `jobUrls` | array | – | Job links or numeric job IDs to scrape directly |
| `locations` | array | – | Places in English, e.g. `Germany`, `Austin, Texas`. Empty means worldwide |
| `geoIds` | array | – | Numeric LinkedIn location IDs, for exact places |
| `distance` | string | `""` (LinkedIn default) | Radius around a city: 0, 5, 10, 25, 50 or 100 miles |
| `timePosted` | string | `""` (any time) | Past hour, 24 hours, 3 days, week, 2 weeks, month or 3 months |
| `companies` | array | – | Employer names, LinkedIn company or university page links, or company IDs |
| `excludeCompanies` | array | – | Employers to leave out |
| `easyApply` | boolean | `false` | Only Easy Apply jobs |
| `under10Applicants` | boolean | `false` | Only jobs with fewer than 10 applicants |
| `experienceLevel` | string | `""` (any) | Internship, Entry level, Associate, Mid-Senior level, Director or Executive |
| `jobType` | string | `""` (any) | Full-time, Part-time, Contract, Temporary, Volunteer, Internship or Other |
| `industry` | array | `[]` | LinkedIn industries of the employer |
| `companySizes` | array | `[]` | Only employers of these sizes (1-10 up to 10,001+ employees) |
| `excludeStaffingAgencies` | boolean | `false` | Skip staffing and recruiting firms |
| `requireSalary` | boolean | `false` | Only jobs that state their pay |
| `minYearlySalary` | integer | – | Skip jobs whose pay range tops out below this yearly amount |
| `salaryCurrency` | string | `""` (any) | Only jobs paying in this currency; also the currency of `minYearlySalary` |
| `requireRecruiter` | boolean | `false` | Only jobs that name the person who posted them |
| `includeTitleKeywords` | array | – | Title must contain one of these words |
| `excludeTitleKeywords` | array | – | Title must not contain any of these words |
| `excludeJobIds` | array | – | Job IDs from earlier runs to skip |
| `scrapeJobDetails` | boolean | `true` | Add description, salary, criteria, applicants and recruiter |
| `includeCompanyDetails` | boolean | `true` | Add the employer's LinkedIn profile |
| `enrichmentDepth` | string | `standard` | `full` adds similar jobs, related searches and LinkedIn's category IDs, and retries for the exact posting time |
| `language` | string | `en` | Language of LinkedIn's labels (English, Spanish, French, German, Portuguese) |
| `splitLargeSearches` | boolean | `true` | When a search reaches LinkedIn's 1,000-result limit, continue with narrower posting-date windows |
| `maxItems` | integer | `100` | Stop after this many jobs (up to 10,000) |

Use at least one of `searchQueries`, `searchUrls`, `jobUrls` or `companies`.

#### Example: newest data jobs in three countries

```json
{
    "searchQueries": ["data engineer", "analytics engineer"],
    "locations": ["Germany", "Netherlands", "United Kingdom"],
    "timePosted": "r604800",
    "maxItems": 300
}
```

#### Example: every open role at two employers, with pay only

```json
{
    "companies": ["General Motors", "https://www.linkedin.com/company/affirm"],
    "requireSalary": true,
    "maxItems": 200
}
```

#### Example: check a list of postings

```json
{
    "jobUrls": [
        "https://www.linkedin.com/jobs/view/4419969671",
        "4336853130"
    ]
}
```

### Use cases

- **Salary benchmarking:** compare yearly pay for a title across cities, countries and employers.
- **Recruiting and sourcing:** watch fresh postings in your niche, see who is hiring and who posted each role.
- **Sales prospecting:** spot companies that are hiring fast in a segment, with their size, website and offices.
- **Job hunting:** a daily run with "Past 24 hours", Easy Apply and fewer than 10 applicants, skipping jobs you've already seen.
- **Labour-market research:** track demand, seniority mix and pay transparency over time.

### FAQ

**Do I need a LinkedIn account or cookies?**
No. The actor reads the job pages LinkedIn shows to signed-out visitors.

**Can I filter for remote, hybrid or on-site jobs?**
Neither the public search nor the job page shows the remote/hybrid/on-site label to signed-out visitors, and the search ignores the workplace filter, so a remote filter can't be offered. `remoteAllowed` is `true` when the title or description clearly says the role is fully remote, or when the job page includes LinkedIn's structured data marking it `TELECOMMUTE`. That structured block only ever marks remote jobs, never hybrid or on-site, so neither is inferred; not every remote job carries it, so the field can be missing for a remote role. Adding "remote" to your keywords narrows results to jobs that mention it. Older saved inputs that still contain the remote filter or the job-function filter keep running; those two settings are ignored and the run's status message says so.

**Why do some jobs have no salary?**
Many employers don't publish pay. The actor reads LinkedIn's pay box and pay ranges written in the description, and never estimates. `salarySource` shows where each figure came from. Expect most US jobs in pay-transparency states to have a salary, and far fewer elsewhere.

**How do I get more than 1,000 jobs?**
LinkedIn returns at most 1,000 jobs for one search. When a search reaches that limit, the actor automatically continues with the same search restricted to the past month, week, 3 days and 24 hours. Each of those returns its own results, and duplicates are removed, so one keyword and location can yield well over 1,000 jobs. For even more, add locations, keywords or companies. Turn this off with `splitLargeSearches` if you only want LinkedIn's top 1,000.

**Why can't I get the employer's own application link?**
LinkedIn only shows the external "Apply on company website" link to signed-in members. `applyUrl` is the LinkedIn page where the application starts, and `easyApply` tells you whether you can apply on LinkedIn directly.

**My location returned the wrong country.**
LinkedIn quietly searches the whole United States when it doesn't recognise a place. The actor checks every location first: an unrecognised one is matched to LinkedIn's closest suggestion or skipped with a note in the status message. Type places in English ("Germany", not "Deutschland"), or use `geoIds`.

**Which details does LinkedIn hide from signed-out visitors?**
Remote/hybrid/on-site labels, the employer's own application link, whether a job was reposted, view counts and the job-function category are only shown to signed-in members, so this actor can't return them. Expiry dates are partly available: when the job page carries structured data, `expiresAt` is LinkedIn's own expiry time. Workarounds: add "remote" to your keywords or use `includeTitleKeywords` with "remote" or "hybrid"; use `remoteAllowed` for jobs that clearly state they're fully remote or whose structured data marks them `TELECOMMUTE`; use `postedDate` (the latest posting date) for freshness; to track reposts and closures yourself, run on a schedule and compare `id` values between runs. The exact posting time (`postedAt`) appears only on some versions of a job page. Extended job page depth asks again to fill it for most jobs, but not all; `postedDate` is always filled.

**How do I get only new jobs on a schedule?**
Use "Date posted: Past 24 hours", or keep the `id` values from earlier runs and pass them in `excludeJobIds`.

**Do I need to set up a proxy?**
No. The actor picks its connection automatically and switches when LinkedIn slows it down.

**Is this affiliated with LinkedIn?**
No. It is an independent tool that reads LinkedIn's public job pages.

# Actor input Schema

## `searchQueries` (type: `array`):

Job titles, skills or phrases to search for, one per line (e.g. 'data analyst', 'registered nurse'). Each keyword is searched in every location you list, and results are merged without duplicates.

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

Paste job search links copied from LinkedIn (linkedin.com/jobs/search/?...). The keywords, location and filters inside each link are kept.

## `jobUrls` (type: `array`):

Scrape specific job postings: paste job links (linkedin.com/jobs/view/...) or numeric job IDs. Postings that have been taken down are skipped.

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

Cities, regions or countries in English (e.g. 'Germany', 'Austin, Texas', 'London Area, United Kingdom'). Every keyword is searched in each location. A location LinkedIn doesn't recognise is reported and skipped rather than silently searched as the United States. Leave empty to search worldwide.

## `geoIds` (type: `array`):

Optional numeric LinkedIn location IDs (the geoId value in a LinkedIn search link, e.g. 101282230 for Germany). Use these for exact places in any language.

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

Search radius around a city-level location, in miles.

## `timePosted` (type: `string`):

Only jobs posted (or reposted) within this period.

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

Only jobs from these employers. Enter company names (e.g. 'General Motors'), LinkedIn company or university page links, or numeric company IDs. Leave the keywords empty to get all of a company's open jobs.

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

Skip jobs from these employers (exact company name or LinkedIn company page link).

## `easyApply` (type: `boolean`):

Only jobs you can apply to directly on LinkedIn.

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

Only jobs that have had fewer than 10 applicants so far.

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

Only jobs listed at this seniority level on LinkedIn. Jobs that LinkedIn lists as 'Not Applicable' are left out when a level is chosen.

## `jobType` (type: `string`):

Only jobs with this employment type.

## `industry` (type: `array`):

Only jobs from employers in these LinkedIn industries.

## `companySizes` (type: `array`):

Only jobs from employers of these sizes, as listed on their LinkedIn page. Each employer's page is looked up once. Jobs whose employer size isn't listed are kept.

## `excludeStaffingAgencies` (type: `boolean`):

Skip jobs posted by employers in LinkedIn's 'Staffing and Recruiting' industry.

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

Only jobs that state their pay, either in LinkedIn's pay range box or in the job description.

## `minYearlySalary` (type: `integer`):

Skip jobs whose stated pay range tops out below this yearly amount. Hourly, weekly and monthly pay is converted to a yearly figure. The amount is compared in the job's own currency, so set 'Salary currency' to compare like with like. Jobs without a stated salary are kept unless 'Only jobs with a salary' is on.

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

Only jobs whose stated pay is in this currency. Also sets the currency of 'Minimum yearly salary'. Jobs without a stated salary are kept unless 'Only jobs with a salary' is on.

## `requireRecruiter` (type: `boolean`):

Only jobs where LinkedIn shows the person who posted the job.

## `includeTitleKeywords` (type: `array`):

Keep only jobs whose title contains at least one of these words (not case-sensitive). Useful because LinkedIn's keyword matching is broad.

## `excludeTitleKeywords` (type: `array`):

Skip jobs whose title contains any of these words, e.g. 'Senior' or 'Intern'.

## `excludeJobIds` (type: `array`):

Job IDs (the 'id' field of an earlier run) to leave out, so scheduled runs return only new postings.

## `scrapeJobDetails` (type: `boolean`):

Open each job to add the full description, salary, seniority, job type, industry, applicant count, recruiter and Easy Apply status. Turn off for a fast list of titles, companies, locations and dates only. Filters that need these details switch it on automatically.

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

Add each employer's LinkedIn profile: industry, size, headquarters, website, founding year, office locations, followers, tagline and description. Fetched once per company.

## `enrichmentDepth` (type: `string`):

Standard returns each job's core details. Extended also adds similar jobs, 'people also viewed' jobs, related searches and LinkedIn's numeric industry and title IDs, and asks LinkedIn again when the exact posting time is missing, so it takes longer per job.

## `language` (type: `string`):

Language LinkedIn uses for labels such as job type and seniority. Job descriptions stay in the language they were written in. Locations must still be typed in English.

## `splitLargeSearches` (type: `boolean`):

LinkedIn returns at most 1,000 jobs for one search. When a search reaches that limit, continue with the same search limited to the past month, week, 3 days and 24 hours, which surfaces many more jobs (duplicates removed).

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

Stop after saving this many jobs. A single keyword + location search returns at most 1,000 jobs; add keywords or locations to collect more.

## Actor input object example

```json
{
  "searchQueries": [
    "software engineer"
  ],
  "locations": [
    "United States"
  ],
  "distance": "",
  "timePosted": "",
  "easyApply": false,
  "under10Applicants": false,
  "experienceLevel": "",
  "jobType": "",
  "industry": [],
  "companySizes": [],
  "excludeStaffingAgencies": false,
  "requireSalary": false,
  "salaryCurrency": "",
  "requireRecruiter": false,
  "scrapeJobDetails": true,
  "includeCompanyDetails": true,
  "enrichmentDepth": "standard",
  "language": "en",
  "splitLargeSearches": true,
  "maxItems": 3
}
```

# Actor output Schema

## `jobs` (type: `string`):

Dataset containing all scraped LinkedIn job listings.

# 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 = {
    "searchQueries": [
        "software engineer"
    ],
    "locations": [
        "United States"
    ],
    "easyApply": false,
    "under10Applicants": false,
    "excludeStaffingAgencies": false,
    "requireSalary": false,
    "requireRecruiter": false,
    "scrapeJobDetails": false,
    "includeCompanyDetails": false,
    "splitLargeSearches": true,
    "maxItems": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("nimbryxo-studio/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 = {
    "searchQueries": ["software engineer"],
    "locations": ["United States"],
    "easyApply": False,
    "under10Applicants": False,
    "excludeStaffingAgencies": False,
    "requireSalary": False,
    "requireRecruiter": False,
    "scrapeJobDetails": False,
    "includeCompanyDetails": False,
    "splitLargeSearches": True,
    "maxItems": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("nimbryxo-studio/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 '{
  "searchQueries": [
    "software engineer"
  ],
  "locations": [
    "United States"
  ],
  "easyApply": false,
  "under10Applicants": false,
  "excludeStaffingAgencies": false,
  "requireSalary": false,
  "requireRecruiter": false,
  "scrapeJobDetails": false,
  "includeCompanyDetails": false,
  "splitLargeSearches": true,
  "maxItems": 3
}' |
apify call nimbryxo-studio/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

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