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

LinkedIn jobs in full detail: title, company, location, salary, description, seniority, contract type, function, industries and applicant count. No account or cookie needed. The city split over 199 countries passes LinkedIn's 1,000-job ceiling. Example: 'developer', United States, past week.

- **URL**: https://apify.com/badr\_xba/linkedin-jobs-scraper.md
- **Developed by:** [Badr Bouargan](https://apify.com/badr_xba) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.50 / 1,000 job scrapeds

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?

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 Jobs Scraper – Search Results & Full Job Details

![LinkedIn Jobs Scraper](https://api.apify.com/v2/key-value-stores/paDGjcmD1ZMsRJLZS/records/linkedin-banner.png?signature=46an9tQrYXbHOn1FsfoB)

Scrape **LinkedIn job search results** with the full detail of every posting: title, company, location, salary range, the complete description, seniority level, employment type, job function, industries and how many people have applied. No API key, no account, no cookie to paste.

Give it either a **LinkedIn jobs search URL** copied from your browser, or fill in the search fields. Both work; the URL wins when you provide one.

### Before you run it — read this

LinkedIn's `robots.txt` reads `Disallow: /` for ordinary crawlers, and states that *"the use of robots or other automated means to access LinkedIn without the express permission of LinkedIn is strictly prohibited"*. This Actor reads pages that any visitor can open without signing in, but that notice applies to it all the same. Running it is a decision about LinkedIn's User Agreement that only you can make — and LinkedIn runs a whitelist process at `whitelist-crawl@linkedin.com` for anyone who wants permission first.

Two further things worth knowing before you build on it:

- **Job poster fields are personal data.** `jobPosterName`, `jobPosterPhoto` and `jobPosterProfileUrl` identify a real person. Have a lawful basis before you store them, especially under the GDPR.
- **LinkedIn throttles.** Job pages come through reliably; company pages mostly do not (see below).

### What comes back, and how reliably

Measured on two real runs — 80 data-analyst jobs in New York and 60 developer jobs in Morocco:

| Field | Filled |
| --- | --- |
| `id`, `link`, `title`, `location`, `postedAt`, `companyLogo` | **100 %** |
| `descriptionHtml`, `descriptionText` | **100 %** |
| `seniorityLevel`, `employmentType`, `industries`, `applicantsCount` | **100 %** |
| `companyName`, `companyLinkedinUrl`, `jobFunction` | 97 % |
| `benefits` ("Actively Hiring", "Be an early applicant") | ~50 % |
| `salaryInfo` | 14 % — only when the employer publishes a range |
| `jobPosterName` and the other poster fields | 3–15 % — LinkedIn shows a poster on few jobs |
| `companyDescription`, `companyWebsite`, `companyEmployeesCount` | **near zero from datacenter addresses** |

**Company details are the weak spot.** LinkedIn answers most requests for a company page with its refusal code `999`. The Actor tries once per company, caches the result, and after fifteen refusals in a row it stops trying and says so in the log rather than billing you for requests that cannot succeed. The RESIDENTIAL proxy group improves this markedly. Job data is unaffected.

Three fields from other LinkedIn scrapers are **not** offered here, because the pages this Actor reads do not carry them: `expireAt`, `insights` and a per-component salary breakdown. An always-empty column is worse than no column.

### Example output

![Real output](https://api.apify.com/v2/key-value-stores/paDGjcmD1ZMsRJLZS/records/linkedin-output.png?signature=Q3ArhNpGpJmDUHxMNzJn)

### The August 2026 search change

LinkedIn replaced its job search with an AI-driven one and dropped most URL filters. Only these still work in a URL:

| Still a URL filter | |
| --- | --- |
| `f_TPR` / `f_TP` | Date posted |
| `f_C` | Company |
| `f_AL` | Easy Apply |
| `f_EA` | Under 10 applicants |
| `keywords`, `location`, `geoId`, `distance` | The search itself |

Experience level (`f_E`), job type (`f_JT`) and workplace (`f_WT`, `f_WRA`) are gone. With **Convert dropped filters into search words** switched on — the default — the Actor turns them into plain language and appends them to your keywords, whether they came from your input or from a pasted URL. That steers the search. It does not guarantee the filter, and no one can make it.

Filters whose value is an internal LinkedIn ID with no public label — `f_I` (industry), `f_F` (function), `f_T` (title) and similar — cannot be converted at all. The Actor names them in the log so you know they were dropped instead of silently ignoring them. Describe those in your keywords yourself.

### Getting past LinkedIn's 1,000-job limit

LinkedIn stops serving a search after about a thousand jobs, no matter how many match. Switch on **Split the search by city**, pick a country, and the same search runs city by city. Jobs seen twice are dropped on their ID.

Measured on Morocco: one search of 14 cities plus the country returned **400 unique jobs in 63 seconds**, with 343 duplicates discarded along the way.

**199 countries** ship with a city list — every country LinkedIn serves jobs in, from the United States (25 cities) to Tuvalu (1). **Morocco has 14, the United Arab Emirates 8, Saudi Arabia 12.** Every country in the dropdown has real cities behind it, so the split always happens; there is no entry that quietly does nothing.

Small markets behave honestly rather than impressively: a split across Mongolia's two cities costs two requests and returns what is actually there. Newly added Kazakhstan, for instance, returned 150 jobs across five locations in 21 requests, with 34 duplicates discarded.

### Collecting new jobs every day

Set **Date posted** to *Past 24 hours* — or copy a search URL that already has that filter — and schedule the Actor daily. LinkedIn reads the window from the request each time, so the same input keeps working without being regenerated.

### Step-by-step

1. Paste one or more **LinkedIn job search URLs**, or fill in **Keywords** and **Location**.
2. Set **Maximum jobs**. 100 is a good first run; 0 means no ceiling.
3. Leave **Open each job for its full details** on unless you only need the search cards — off, it uses about ten times fewer requests.
4. Optional: **Split the search by city** with one of the 199 countries, to go past 1,000.
5. Optional: **Also read each company** — expect it to come back mostly empty unless you pick the RESIDENTIAL proxy group.
6. Press **Start**, then export as JSON, CSV, Excel or HTML.

### Input example

```json
{
  "keywords": "data analyst",
  "location": "New York, United States",
  "datePosted": "pastWeek",
  "maxItems": 500,
  "scrapeJobDetails": true,
  "splitByCity": false
}
```

Or, to sweep a whole country:

```json
{
  "keywords": "developer",
  "splitByCity": true,
  "splitCountry": "MA",
  "maxItems": 2000
}
```

### Output example

```json
{
  "id": "4413987826",
  "link": "https://ma.linkedin.com/jobs/view/fullstack-engineer-kotlin-vue-at-a2mac1-4413987826",
  "title": "Fullstack Engineer (Kotlin / Vue)",
  "companyName": "A2MAC1 - Decode the future",
  "companyLinkedinUrl": "https://fr.linkedin.com/company/a2mac1",
  "companyLogo": "https://media.licdn.com/dms/image/…",
  "location": "Casablanca, Casablanca-Settat, Morocco",
  "postedAt": "2026-05-13",
  "postedAtTimestamp": 1778630400000,
  "salaryInfo": ["$175,000/yr", "$210,000/yr"],
  "benefits": ["Be an early applicant"],
  "applicantsCount": "Be among the first 25 applicants",
  "applicantsCountNumber": 25,
  "seniorityLevel": "Mid-Senior level",
  "employmentType": "Full-time",
  "jobFunction": "Other",
  "industries": "IT Services and IT Consulting",
  "workRemoteAllowed": false,
  "workplaceTypes": ["On-site"],
  "applyMethod": "Apply on the company site",
  "descriptionText": "We are seeking a skilled and passionate Fullstack Engineer…",
  "searchInput": "developer",
  "scrapedAt": "2026-09-23T10:25:11.000Z"
}
```

### Speed and cost

| Run | Jobs | Time | Requests | Platform cost |
| --- | --- | --- | --- | --- |
| Search cards only, 15 cities in Morocco | 400 | 1 min 3 s | 83 | $0.005 |
| Full details, New York | 80 | 1 min 32 s | 88 | $0.006 |
| Full details + company attempts, Morocco | 60 | 1 min 42 s | 105 | $0.006 |

LinkedIn's own latency varies: the same 80-job run took 92 seconds once and 198 another time. Budget for the slower figure.

Search cards cost one request per ten jobs. Opening each job costs one request per job — that is the whole difference between the first row of the table and the second.

### Frequently asked questions

**Do I need a LinkedIn account or an API key?**
No. Everything it reads is served to signed-out visitors.

**Why is `salaryInfo` empty on most jobs?**
Because most employers do not publish a range. The Actor reads only LinkedIn's own compensation block; pulling figures out of the description text would return numbers that are not salaries.

**Why did the company fields come back empty?**
LinkedIn refused those pages. See the section above — the Actor tells you in the log when it gives up, and the RESIDENTIAL proxy group is the fix.

**Can I filter by experience level or job type?**
You can ask for them, and they will be turned into words inside the search. LinkedIn no longer accepts them as real filters, so treat the result as steering rather than filtering.

**How do I collect more than 1,000 jobs?**
Split the search by city. See above.

### Legal note

This Actor reads pages LinkedIn serves to anyone, but LinkedIn's `robots.txt` prohibits automated access without its permission, and its User Agreement governs your use. Job poster names, photos and profile links are personal data in many jurisdictions. Decide on a lawful basis before you store them, and consider whether you need the poster fields at all.

# Actor input Schema

## `urls` (type: `array`):

Open LinkedIn jobs search in a private window, apply your filters, then copy the full address from the bar and paste it here. When this list has anything in it, the search filters below are ignored.

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

What the job is about: "data analyst", "chef de projet", "nurse". Used only when no URL is pasted above.

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

A city, region or country as LinkedIn spells it: "Casablanca, Morocco", "Dubai", "United Arab Emirates".

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

The numeric place identifier LinkedIn puts in its own search URL. More precise than a location name when you have it.

## `distance` (type: `integer`):

How far around the location to look. LinkedIn counts in miles.

## `datePosted` (type: `string`):

How recent the job must be. Set this to "Past 24 hours" and schedule the Actor daily to collect only new postings.

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

Numeric LinkedIn company identifiers, to follow named employers. They appear as f\_C in a LinkedIn search URL.

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

Keeps only jobs you can apply to without leaving LinkedIn.

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

Keeps only jobs with fewer than ten applicants so far — the ones worth applying to first.

## `experienceLevel` (type: `array`):

Since August 2026 LinkedIn dropped these from its search URLs. With the option below switched on, they are appended to your keywords in plain words instead. That steers the search; it does not guarantee the filter.

## `jobType` (type: `array`):

Same treatment: turned into words inside the search.

## `workplaceType` (type: `array`):

On-site, remote or hybrid — also turned into words.

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

On, the three filters above — and the same ones found in a pasted URL — are appended to the keywords in plain language. Off, they are ignored and the run says so in the log.

## `splitByCity` (type: `boolean`):

LinkedIn stops serving a search after about 1,000 jobs, however many match. Switch this on and the same search is run city by city across the country you pick, and duplicates are dropped on the job ID.

## `splitCountry` (type: `string`):

Which country's cities to spread the search over. Only countries with a built-in city list are offered, so the split always happens.

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

A ceiling for the whole run. There is no built-in maximum — set 0 to collect everything your searches return. 1,000 jobs from search cards take about two and a half minutes.

## `maxItemsPerSearch` (type: `integer`):

Applied to each pasted URL and to each city. LinkedIn itself will not go past 1,000.

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

Adds the description, seniority level, employment type, job function, industries and applicant count — at the cost of one request per job. Off, a search page gives ten jobs in one request, which is about ten times faster: 1,000 jobs in roughly two and a half minutes instead of twenty.

## `scrapeCompanyDetails` (type: `boolean`):

Adds the company description, website and employee count. One extra request per company, read once and reused.

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

How many requests run at the same time, across searches, job pages and companies. Connections are kept open and reused, so raising this is what makes a run faster. Lower it if LinkedIn starts refusing requests.

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

Keep Apify Proxy on. LinkedIn rate-limits an address that asks for too much, and rotating addresses is what keeps a long run alive.

## Actor input object example

```json
{
  "urls": [],
  "keywords": "developer",
  "location": "Morocco",
  "datePosted": "",
  "easyApply": false,
  "under10Applicants": false,
  "autoConvertToAiSearch": true,
  "splitByCity": false,
  "maxItems": 1000,
  "maxItemsPerSearch": 1000,
  "scrapeJobDetails": true,
  "scrapeCompanyDetails": false,
  "maxConcurrency": 12,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Title, company, location, date, applicants, contract and seniority.

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

Employer, website, employee count, industry and function.

## `remote` (type: `string`):

Remote arrangements, pay ranges and posting highlights.

## `allFields` (type: `string`):

Every field, including the full description in HTML and plain text.

## `runStats` (type: `string`):

Jobs saved, searches run, pages read, duplicates dropped, refusals.

# 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 = {
    "urls": [],
    "keywords": "developer",
    "location": "Morocco",
    "maxItems": 1000,
    "maxConcurrency": 12,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("badr_xba/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 = {
    "urls": [],
    "keywords": "developer",
    "location": "Morocco",
    "maxItems": 1000,
    "maxConcurrency": 12,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("badr_xba/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 '{
  "urls": [],
  "keywords": "developer",
  "location": "Morocco",
  "maxItems": 1000,
  "maxConcurrency": 12,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call badr_xba/linkedin-jobs-scraper --silent --output-dataset

```

## MCP server setup

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