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

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

## Pricing

Pay per event

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 Jobs Scraper

A LinkedIn jobs scraper that takes a keyword and a location and returns full job postings, with no login, no cookies and no session token anywhere in the input. It reads LinkedIn's public guest surface, the same pages a search engine crawls, so there is no account to connect and nothing to keep warm between runs.

You get the search card plus the whole posting behind it: description in both HTML and plain text, seniority, employment type, industry, applicant count, and the advertised salary whenever the company published one.

### What you get

- **Up to 500 postings per run**, walked ten at a time through LinkedIn's guest search until the query runs dry
- **The full job description**, returned twice: `description_html` with LinkedIn's own markup intact, and `description_text` flattened for anything that feeds a model
- **35 populated fields per posting** on a detailed run, including `applicants_count`, `seniority_level`, `job_function`, `industries`, `employment_type`, `valid_through` and `company_linkedin_url`
- **Advertised salary** as `salary_min`, `salary_max`, `salary_currency` and `salary_unit`, parsed out of the posting's structured data when the company published a range
- **Five filters that map to LinkedIn's own**: posted within, workplace type, job type, experience level, and sort by relevance or most recent
- **A listings-only mode** that returns title, company, location, posted date and URL at roughly seven times the speed and a seventh of the cost, for when you only need to know what is out there
- **`job_id`**, LinkedIn's own numeric identifier, so consecutive runs deduplicate cleanly and you can diff yesterday against today
- **No actor-start fee.** You pay per posting returned, and a run that finds nothing costs nothing

### Why scrape LinkedIn jobs

LinkedIn does not sell a jobs API to the people who need one. The official Talent Solutions API is gated behind a partner agreement aimed at applicant tracking vendors, and it answers questions about your own postings, not about the market. If you want to know what a competitor is hiring for, how fast a city's demand for a skill is moving, or which companies posted a role this morning, the public job pages are the only surface that will tell you.

Those pages are open. LinkedIn publishes every posting to search engines with a schema.org `JobPosting` block attached, because a job nobody can find is a job nobody applies to. That is the surface this actor reads. It never touches an account, never asks you for a session cookie, and never sends a request that a logged-out visitor could not have sent.

The practical consequence is that nothing here expires. Scrapers built on a session cookie stop the day the cookie does, which is why so many LinkedIn actors carry a support thread about re-authentication. There is no credential in this actor's input schema, so there is nothing to rotate and nothing that goes stale on a Sunday night while your schedule is running.

### Input

| Field | Type | Required | Default | What it does |
|---|---|---|---|---|
| `keywords` | string | yes | `data engineer` | Job title, skill or company. The same box as LinkedIn's own search |
| `location` | string | no | `Netherlands` | City, region or country as LinkedIn spells it. `Remote` searches remote-tagged postings across markets. Empty searches worldwide |
| `limit` | integer | no | `25` | Postings to return, 1 to 500 |
| `detail` | boolean | no | `true` | Open every posting for the full description and criteria. Turn off for a fast, cheap listing sweep |
| `posted_within` | select | no | `any` | `any`, `24h`, `week`, `month` |
| `workplace` | select | no | `any` | `any`, `on_site`, `remote`, `hybrid` |
| `job_type` | select | no | `any` | `any`, `full_time`, `part_time`, `contract`, `temporary`, `internship`, `volunteer` |
| `experience` | select | no | `any` | `any`, `internship`, `entry_level`, `associate`, `mid_senior`, `director`, `executive` |
| `sort_by` | select | no | `relevance` | `relevance` or `date` |
| `retries` | integer | no | `3` | Retry attempts per request |

### Output

One row per posting. Detail fields appear only when `detail` is on.

```json
{
  "position": 1,
  "job_id": "4446807685",
  "url": "https://nl.linkedin.com/jobs/view/marketing-manager-at-rise-resourcing-4446807685",
  "title": "Marketing Manager",
  "company": "Rise Resourcing",
  "company_name": "Rise Resourcing",
  "company_linkedin_url": "https://www.linkedin.com/company/rise-resourcing",
  "company_logo": "https://media.licdn.com/dms/image/...",
  "location": "London Area, United Kingdom",
  "country": "GB",
  "city": "London Area",
  "posted_at": "2026-08-21",
  "posted_label": "1 week ago",
  "date_posted": "2026-08-21T09:14:02.000Z",
  "valid_through": "2027-02-17T09:14:02.000Z",
  "employment_type": "FULL_TIME",
  "employment_type_label": "Full-time",
  "seniority_level": "Mid-Senior level",
  "job_function": "Marketing",
  "industries": "Staffing and Recruiting",
  "description_text": "Marketing Manager\nLondon\n£65,000 - £75,000\n...",
  "description_html": "<p>Marketing Manager<br>London<br>...</p>",
  "applicants_count": 60,
  "applicants_label": "60 applicants",
  "salary_min": 65000,
  "salary_max": 75000,
  "salary_currency": "GBP",
  "salary_unit": "YEAR",
  "detail_fetched": true,
  "detail_source": "json_ld",
  "keywords": "marketing manager",
  "search_location": "United Kingdom"
}
```

### Use cases

**Recruiters mapping a market before a pitch.** Run the keyword and city you are about to sell into, sort by most recent, and you have every competing vacancy with its applicant count. `applicants_count` is the number nobody else in this category returns, and it is the one that tells you whether a role is contested or quietly starving. Thirty seconds of runtime replaces an afternoon of clicking, and the output drops straight into a spreadsheet a hiring manager will actually read.

**Job boards and aggregators filling a niche index.** Set `detail` on, pick your vertical keywords, and schedule the run hourly with `posted_within: 24h`. Deduplicate on `job_id`, which is LinkedIn's own identifier and stable forever, so a repeated run costs you nothing in duplicate rows. `description_html` keeps the original formatting, so postings render on your own site the way the employer wrote them.

**Salary and skills research.** `salary_min`, `salary_max` and `salary_currency` come out of the posting's structured data rather than a regex over prose, so the numbers are the ones the employer published. Combine with `seniority_level` and `job_function` and you can build a defensible compensation band for a role in a city, from a sample size that a survey vendor would charge five figures for.

**Feeding a model with real job text.** `description_text` is the full posting flattened to plain text with the markup removed, which is what an embedding pipeline or a resume-matching model wants. A LinkedIn jobs scraper API that hands you truncated snippets forces you to fetch the page again yourself; this one returns the whole description on the first pass.

### How it compares

| | this actor | `cheap_scraper/linkedin-job-scraper` | `crawlworks/linkedin-jobs-scraper` |
|---|---|---|---|
| Per 1.000 listing rows | **$0,60** | $0,70 | $5,00 |
| Per 1.000 detailed rows | **$4,00** | not offered separately | $5,00 |
| Actor-start fee | **none** | $0,005 per run | $0,00005 per run |
| Filters exposed | **5** | fewer | fewer |
| Applicant count | **yes** | no | no |
| Salary fields | **yes, when published** | no | no |
| Description as HTML and text | **both** | text | text |
| Login or cookie needed | **no** | no | no |

Honest about the other side: `cheap_scraper` has 7.061 monthly users against our zero, and `curious_coder/linkedin-jobs-scraper` has 14.783 with 135 reviews. They are proven at a scale this actor is not. If all you need is a title-and-company list and you already run one of them, the reason to move is the start fee and the filters, not raw capability.

### Pricing

Two events. `job_listing` costs $0,0006 per posting, which covers title, company, location, posted date and URL. `job_detail` costs $0,0040 per posting and adds the full description, criteria, applicant count and salary. A run with `detail` off is charged entirely at the listing rate. All pricing is pay-per-event, so you only pay for postings you actually receive. No actor-start fee, no per-compute-unit charges, and a run that returns nothing costs nothing.

### Limits and gotchas

- **LinkedIn's guest search runs dry past roughly 500 results** for most queries. Measured: ten ids per page through `start=300`, an empty body from around 600, and an HTTP 400 at 1000. Narrow the keywords or add a filter rather than raising `limit` and hoping.
- **`seniority_level`, `job_function` and `industries` come back in the posting's own market language.** A Dutch posting returns `Senior medewerker`, a British one `Mid-Senior level`. This is not a bug we can fix: LinkedIn only serves the structured data block on the country subdomain, and that same subdomain localizes the criteria labels. `employment_type` and `country` are normalized and safe to filter on.
- **Salary is present only when the employer published a range.** Most postings do not. Treat `salary_min` as optional in anything downstream.
- **A small share of postings render without a structured-data block.** The actor falls back to reading the visible page for those and sets `detail_source` to `dom`, so you can tell them apart. Those rows carry the description and company but not `employment_type` or `country`.
- **`detail: true` costs one page fetch per posting**, roughly 300 KB each. A 500-job detailed run is real bandwidth and takes a few minutes. Use listings-only mode for wide sweeps and turn detail on for the shortlist.
- **Postings older than a few months disappear from guest search** even when the URL still resolves. `sort_by: date` plus `posted_within` is the reliable way to catch new listings.
- **Cold start adds about two seconds** before the first request goes out, which matters if you are scheduling many small runs.

### FAQ

**Can I scrape LinkedIn jobs without login or cookies?**
Yes, and this actor does exactly that. There is no credential field in the input schema. It reads the guest job pages LinkedIn publishes for search engines, so nothing expires and there is no session to keep alive.

**Is there a LinkedIn jobs scraper API I can call from my own code?**
Every Apify actor is an API. Start a run with a POST to the Apify API, poll or wait for it to finish, and read the dataset as JSON, CSV or Excel. The input schema on this page is the request body.

**Can I use this LinkedIn jobs scraper for free?**
Apify's free plan includes a monthly platform credit, which covers a few thousand listing rows or a few hundred detailed ones on this actor. There is no separate free tier and no trial to activate, and there is no actor-start fee eating your credit before results land.

**How does this compare to a LinkedIn jobs scraper in Python I write myself?**
The scraping itself is not the hard part. The parts that cost you a weekend are the fingerprint rotation LinkedIn's edge requires, the retry behaviour when it answers 429 or 999, and the discovery that the structured-data block only appears on the country subdomain with the full slug URL. All three are already handled here.

**Can I run this on a schedule and only get new jobs?**
Yes. Set `sort_by` to `date` and `posted_within` to `24h`, then deduplicate on `job_id` against what you already stored. LinkedIn's job ids are stable, so a repeat posting never reaches your database twice.

### Related Actors

- [LinkedIn Company Finder](https://apify.com/s-r/free-linkedin-company-finder---linkedin-address-from-any-site) — resolve any website to its LinkedIn company page
- [LinkedIn Ads Library](https://apify.com/s-r/linkedin-ads-library) — what a company is advertising on LinkedIn
- [Google Search Results SERP](https://apify.com/s-r/free-google-search-results-serp---only-0-25-per-1-000-results) — organic rankings at $0,25 per 1.000 results

# Actor input Schema

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

Job title, skill or company to search for. Same box as the one on LinkedIn's own job search.

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

City, region or country as LinkedIn spells it, for example Netherlands, London, or United States. Use Remote to search remote-tagged postings across markets. Leave empty to search worldwide.

## `limit` (type: `integer`):

How many postings to return, 1 to 500. LinkedIn's guest search runs dry somewhere past 500 for most queries, so narrow the keywords or add filters if you need more than one run's worth.

## `detail` (type: `boolean`):

On: open every posting and return the full description, seniority, employment type, industry, applicant count and salary when shown. Off: return only what the search card carries (title, company, location, posted date, url), which is roughly seven times faster and much cheaper.

## `posted_within` (type: `string`):

Only return postings published inside this window.

## `workplace` (type: `string`):

Filter by how the work is done.

## `job_type` (type: `string`):

Filter by contract type.

## `experience` (type: `string`):

Filter by the seniority LinkedIn assigned to the posting.

## `sort_by` (type: `string`):

Relevance is LinkedIn's default ranking. Most recent puts the newest postings first, which is what you want for a monitoring schedule.

## `retries` (type: `integer`):

Retry attempts per page, each with a rotated browser fingerprint and exponential backoff.

## Actor input object example

```json
{
  "keywords": "data engineer",
  "location": "Netherlands",
  "limit": 25,
  "detail": true,
  "posted_within": "any",
  "workplace": "any",
  "job_type": "any",
  "experience": "any",
  "sort_by": "relevance",
  "retries": 3
}
```

# Actor output Schema

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

One row per job posting, in search-result order.

## `summary` (type: `string`):

itemCount, errorCount, pagesFetched, listingsFound and detailsFetched.

## `errors` (type: `string`):

Per-request failures with a code and a redacted message. Absent when the run had none.

# 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": "data engineer",
    "location": "Netherlands",
    "limit": 25,
    "retries": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/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 = {
    "keywords": "data engineer",
    "location": "Netherlands",
    "limit": 25,
    "retries": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/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 '{
  "keywords": "data engineer",
  "location": "Netherlands",
  "limit": 25,
  "retries": 3
}' |
apify call s-r/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

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