# LinkedIn Jobs Scraper (`lightmoon/linkedin-jobs-scraper`) Actor

Scrape LinkedIn job postings to CSV, Excel or JSON: title, company, location, exact posting date and link. From $0.60 per 1,000 jobs. Descriptions, employment type, seniority and salary on request. Monitor and track new postings, or find remote roles and who is hiring in your city. No account.

- **URL**: https://apify.com/lightmoon/linkedin-jobs-scraper.md
- **Developed by:** [Stable](https://apify.com/lightmoon) (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.60 / 1,000 jobs

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

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

Turn a LinkedIn job search into a spreadsheet. Type what you would type into the
search box — or paste the URL of a search you already built on LinkedIn — and
every posting comes back as a row.

No LinkedIn account. No cookie, session or password. Nothing to install.

### One row, from a real run

```json
{
  "jobId": "4441090127",
  "title": "Senior Python Developer",
  "company": "Leidos",
  "companyUrl": "https://www.linkedin.com/company/leidos",
  "location": "San Antonio, TX",
  "postedDate": "2026-08-04",
  "postedAgo": "2 weeks ago",
  "isNew": false,
  "isActivelyHiring": true,
  "isEarlyApplicant": false,
  "url": "https://www.linkedin.com/jobs/view/4441090127",
  "searchQuery": "python developer",
  "searchLocation": "United States",
  "scrapedAt": "2026-08-21T09:14:00+00:00"
}
```

**16 fields on every job.** Switch on descriptions and each row carries **27**,
or up to 35 when the employer published a pay range:

```json
{
  "descriptionText": "Description\n\nPython Developer\n\nWho We Are ...",
  "descriptionLength": 6250,
  "employmentType": "Full-time",
  "seniorityLevel": "Mid-Senior level",
  "jobFunction": "Engineering and Information Technology",
  "industries": "IT Services and IT Consulting",
  "applicants": 25,
  "applicantsText": "Be among the first 25 applicants",
  "applicantsAreExact": false,
  "salaryText": "$85,000.00/yr - $120,000.00/yr",
  "salaryMin": 85000,
  "salaryMax": 120000,
  "salaryPeriod": "year",
  "salaryCurrency": "$",
  "hasSalary": true
}
```

### How complete the data is

Measured over **730 job cards** from twelve keyword and location pairs across
three countries, and **26 job pages**:

| Field | Present on |
|---|---|
| Job title, company, company page, location | **100%** |
| Exact posting date — `2026-08-04`, not "2 weeks ago" | **100%** |
| Company logo | **100%** |
| "Actively hiring" / "Be an early applicant" | 61% |
| Marked new by LinkedIn | 17% |
| Description, employment type, seniority, job function, industries | **100%** of job pages |
| Applicant count | **100%** of job pages |
| Pay range | **about 1 posting in 10** — it is there when the employer published one |

Numbers that are not exact say so. LinkedIn stops counting applicants at 200 and
writes "Over 200 applicants"; when it does, `applicants` is 200 and
`applicantsAreExact` is `false`.

### Filters, and which ones are real

LinkedIn's public job search — the one it serves without a login — **accepts
several of its own filters and does not apply them.** Measured 21 August 2026:
workplace type, job type, experience level, salary band, Easy Apply and sort
order all come back as an unfiltered result set. Four mutually exclusive filters
returned one identical page of sixty jobs, and not one of the sixty "remote"
jobs had a remote location.

So this actor divides its filters into what LinkedIn really does and what it
does itself.

**Applied by LinkedIn, before anything is fetched — these cost you nothing:**

- keywords, including an exact phrase in quotes
- location — a city, region or country
- posted within the past 24 hours, week or month
- a specific company, when your pasted URL carries its id

**Applied by this actor, from each job's own page:**

- employment type — Full-time, Part-time, Contract, Temporary, Internship…
- seniority level — Internship, Entry level, Associate, Mid-Senior, Director…
- job title must contain / must not contain
- posted on or after / on or before an exact date

Choosing employment type or seniority turns descriptions on, because those two
values exist nowhere else. **A job dropped by any filter is never charged for.**

**Not offered, because the data is not there:** workplace type — remote, hybrid
or on-site. It appears nowhere on LinkedIn's logged-out job search: the location
field said "Remote" on 0 of 730 measured cards, and none of 26 job pages carried
a workplace field. A filter for it here would be a switch that changes nothing.

### Recipes

**A weekly market report.** Searches: `python developer`, `data engineer`,
`platform engineer`. Location: `Berlin, Germany`. Posted within: Past week.
Schedule it for Monday morning.

**Only what is new.** Posted within: Past 24 hours, on a daily schedule.
LinkedIn applies that window itself, so the run is small and so is the bill.

**Hiring signals for sales.** Leave descriptions off, switch on **Report how
many jobs match in total**, and run one search per target city. The `company`,
`companyUrl` and `isActivelyHiring` columns are the list; the total tells you
how hot each market is.

**A shortlist you can read.** Turn descriptions on, set *Job title must contain*
to `senior`, and *Skip job titles containing* to `intern`, `sales`. Everything
filtered out is filtered before it is billed.

**Use LinkedIn's own filter UI.** Build the search on LinkedIn, copy the URL
from the address bar, paste it into **Searches**. The run tells you if a filter
in it is one LinkedIn ignores when logged out.

### What it costs

Two charges, and the second one only if you ask for it:

| | per 1,000 |
|---|---|
| a job row | **$0.60** |
| its full description, employment type and seniority | **+$0.30** |

A typical run — 1,000 jobs with no descriptions — is **$0.60**. The same 1,000
with descriptions is **$0.90**.

**You are not billed for:** a job that arrives twice (LinkedIn's own result list
shifts between requests, so this happens on roughly one job in five), a job
dropped by any filter, or a description that could not be fetched because the
posting was taken down.

### Limits, stated plainly

- **About 400–500 results per search.** LinkedIn's public job search stops
  serving results past that depth — reached on nine of twelve measured
  searches, earlier on the other three. To cover a whole market, split it by
  city or by job title rather than asking one search for more.
- **The same job can appear on two pages.** Duplicates are removed by job id and
  never billed twice.
- **Locations are matched literally.** `Berlin, Germany` works; a misspelt city
  returns nothing rather than something close.
- **No workplace type.** See above.
- **Public postings only.** Anything behind a LinkedIn login — recruiter tools,
  saved searches, applicant data — is out of scope by design.
- **Free Apify plan:** works. Large runs are limited by your own Apify usage,
  not by this actor. Start with `maxJobs` at 100.

### Every run explains itself

Each run writes a `RUN_SUMMARY` record next to the dataset: jobs stored and
charged, job pages fetched and how many had been taken down, searches that
returned nothing, which filter dropped what and how many, duplicates skipped,
how many jobs LinkedIn says match in total, and any filter in a pasted URL that
LinkedIn ignores.

An empty dataset always says why it is empty.

### Questions

**Do I need a LinkedIn account or cookies?** No. This reads the public job
search LinkedIn serves to a visitor who is not signed in.

**Can it get me applicants, recruiter details or InMail?** No. That is behind a
login and is not what this does.

**Why is the posting date better than "2 weeks ago"?** Because you can sort and
filter by it. LinkedIn shows the words and carries the date; you get both.

**Can I collect an entire country's job market?** In slices. One search runs out
at 400–500 results, so a country is covered by several searches — by city or by
job title — which is also how you keep each run small and cheap.

**How fresh is the data?** It is fetched when you press Start. Nothing is
cached.

# Actor input Schema

## `searches` (type: `array`):

One per line. Either a phrase (`python developer`) or a LinkedIn jobs search URL copied from your browser (`https://www.linkedin.com/jobs/search?keywords=...&location=...`). A pasted URL keeps its own location and date filter.

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

A city, region or country, written the way LinkedIn writes it: `Berlin, Germany`, `United States`, `Greater London`. Applied to every search that does not carry its own. LinkedIn matches this literally, so a misspelt city returns nothing rather than a near match.

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

Applied by LinkedIn before anything is fetched, so a narrow window costs a few requests instead of the whole market. Leave at Any time to read everything the search returns.

## `maxJobs` (type: `integer`):

Across all searches. 0 means no limit. Free-plan runs are capped by your own Apify usage, so start small.

## `maxJobsPerSearch` (type: `integer`):

How deep to read one search before moving to the next. 0 means no limit. LinkedIn's public job search runs out somewhere past 400–500 results per search, so a whole market is collected by splitting it across cities or job titles rather than by asking for more here.

## `includeDescriptions` (type: `boolean`):

Fetches each job's own page and adds the description, employment type, seniority level, job function, industries and how many people have applied. This is a second request per job and about ten times the data, so it is charged as its own event — leave it off for a plain job list.

## `includeDescriptionHtml` (type: `boolean`):

Keeps the original formatting — headings, bullet lists, links — alongside the plain text. Roughly doubles the size of each row.

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

Filtered by this actor from each job's own page, because LinkedIn's logged-out search accepts this filter and does not apply it. Selecting anything here turns on job descriptions. Jobs that do not match are never charged for.

## `seniorityLevels` (type: `array`):

Also read from each job's own page, and also not applied by LinkedIn's logged-out search. Selecting anything here turns on job descriptions.

## `titleMustContain` (type: `string`):

Checked against the job title before anything else is fetched, so a job it drops costs you nothing at all.

## `excludeTitles` (type: `array`):

One word or phrase per line. Useful for cutting out the roles a broad keyword drags in — `intern`, `sales`, `manager`.

## `postedAfter` (type: `string`):

`2026-08-01`, or a full timestamp. Every job carries an exact posting date, so this window is precise rather than approximate.

## `postedBefore` (type: `string`):

`2026-08-15`, or a full timestamp. Use with the field above to take one week out of the middle of a market.

## `includeTotalCount` (type: `boolean`):

Asks LinkedIn how large the whole result set is (`11,000+`) and records it in the run summary, so you can tell a search that is too broad from one that is too narrow. One extra request per search, never charged.

## Actor input object example

```json
{
  "searches": [
    "python developer",
    "data engineer"
  ],
  "location": "Berlin, Germany",
  "postedWithin": "any",
  "maxJobs": 100,
  "maxJobsPerSearch": 1000,
  "includeDescriptions": false,
  "includeDescriptionHtml": false,
  "titleMustContain": "senior",
  "excludeTitles": [
    "intern",
    "sales"
  ],
  "postedAfter": "2026-08-01",
  "postedBefore": "2026-08-15",
  "includeTotalCount": false
}
```

# Actor output Schema

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

No description

## `details` (type: `string`):

No description

## `descriptions` (type: `string`):

No description

## `companies` (type: `string`):

No description

## `all` (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 = {
    "searches": [
        "python developer"
    ],
    "location": "United States"
};

// Run the Actor and wait for it to finish
const run = await client.actor("lightmoon/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 = {
    "searches": ["python developer"],
    "location": "United States",
}

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

```

## MCP server setup

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