# LinkedIn Jobs Scraper — Filters That Actually Work (`northbell/linkedin-jobs-filter-scraper`) Actor

Scrape public LinkedIn jobs where every filter is actually applied. LinkedIn silently ignores its own experience and workplace filters ("worktype is not working"). This Actor applies experience from LinkedIn's real values and offers an honest keyword filter instead of a fake workplace one. No login.

- **URL**: https://apify.com/northbell/linkedin-jobs-filter-scraper.md
- **Developed by:** [Northbell](https://apify.com/northbell) (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 — Filters That Actually Work

Scrape public LinkedIn jobs where **the filters you set are actually applied**. Not sent to LinkedIn and quietly ignored — applied, and proven, on every run. No login, no cookies.

### The complaint this Actor is built to fix

Read the reviews and issue trackers of the popular LinkedIn job scrapers and one word comes up more than any other: **filters**. You set an experience level, or a workplace type, and the results come back exactly as if you had set nothing at all.

That is not the scraper's fault. It is LinkedIn's. **LinkedIn's public job search accepts an experience filter (`f_E`) and a workplace filter (`f_WT`) and then ignores them.** We measured it: ask for *remote* jobs and ask for *on-site* jobs, and 88% of the results are the same postings. The filter changed nothing.

Most scrapers pass those parameters straight through, so they inherit LinkedIn's lie. This one does not pass a filter it knows to be dead. Instead:

- **Experience level is applied for real** — by reading LinkedIn's own seniority value off each job page and keeping only the jobs that match.
- **Workplace is replaced by an honest word filter** — because LinkedIn's public data genuinely cannot tell remote from on-site, this Actor does not pretend to. It offers a *"mentions this word"* filter instead, which is a fact you can check, not a guess.
- **Every run proves what it filtered** — a `summary` row shows what was requested, how many jobs matched, how many were dropped and exactly why.

### What each filter does — and what it honestly can't

| Filter | How it's applied | The honest catch |
|---|---|---|
| **keywords / location** | Sent to LinkedIn. Works. | — |
| **datePosted** | Sent to LinkedIn. Works — and every run re-checks the returned postings' real dates against it. | — |
| **titleIncludes / titleExcludes** | Keep (or drop) jobs by words in the **job title only** — "manager", "senior", "backend". The title is on the listing, so these are applied **without opening the job page — free**. | Title only, so a word buried in the description won't match. Use the description filters below for that. |
| **seniority** | LinkedIn ignores this at search. So we open each job and match on **LinkedIn's own value**. | Only ~4 in 10 public jobs carry a seniority value; the rest say *"Not Applicable"* and are dropped. A narrow filter returns fewer jobs — the summary tells you how many were dropped for having no value, so the count is never a mystery. |
| **employmentType** | Same — matched on LinkedIn's own value. | Almost every public posting is *Full-time*, so this rarely narrows much. |
| **mustMention** | Keeps jobs whose **title or description contains** one of your words — *remote*, *telework*, *visa*, *no agencies*. | It matches the **word**, not the fact. A post saying *"this role is not remote"* contains *"remote"* and will match. Use it to narrow, then read. |
| **mustNotMention** | The inverse — **drops** jobs whose title or description contains one of your words (*agency*, *recruiter*, *unpaid*, *clearance*). | Same honesty: it matches the word. The summary reports how many were dropped this way. |
| **excludeCompanies** | Drops jobs from the companies you name (matched on company name or LinkedIn slug). Applied on the listing, so it costs **no extra request**. | — |

There is deliberately **no "remote / hybrid / on-site" filter** in this Actor. LinkedIn's public pages do not carry that classification reliably — we could only guess it right about 30% of the time — and a filter that is wrong a third of the time is worse than no filter. The word filter is the honest version of the same wish.

### Why not a giant jobs database instead?

Some LinkedIn job scrapers are really a **pre-built database** — millions of jobs indexed once, then queried by API. It sounds better, and the filters are rich, but it comes with the problems buyers of those tools actually report:

- **Stale by construction.** A database is only as fresh as its last crawl. This Actor reads LinkedIn **live, at the moment you run it**, so a job that went up an hour ago is there and a job that closed is gone.
- **No filter that quietly only half-works.** Database tools carry footnotes like *"description search not supported with the 6-month range"* or *"seniority only for English-speaking countries"*. Here, a filter either works everywhere or is not offered.
- **No paid add-ons.** Every filter is included. You are charged for pages examined, not for unlocking a filter.
- **No silent cap.** `maxJobs` is your examine ceiling and the summary tells you exactly what was checked — you are never quietly cut off at 1,000 rows wondering where the rest went.

This Actor is not trying to be a database. It is trying to be the honest live scrape whose numbers you can trust on the day you pull them.

### What you get

Every run writes two kinds of row, tagged by `type`.

**`job`** — one row per job that passed **every** filter:

| field | meaning |
|---|---|
| `title`, `company`, `companySlug`, `location` | the posting |
| `postedOn` | the exact date from LinkedIn's own `datetime` attribute, not "3 days ago" |
| `seniority`, `employmentType`, `jobFunction`, `industries` | LinkedIn's own values, off the job page |
| `salaryMentioned` | whether the description states pay — never invented when it doesn't |
| `description` | the full text |
| `jobUrl` | link to the posting |

**`summary`** — one row per run, the proof:

```
checked: 200        matched: 74        dropped: 126
dropReasons: { seniority-no-value: 118, seniority-wrong-value: 8 }
seniorityFillRatePct: 39
datePostedRequested: r604800   datePostedOutOfRange: 0   datePostedHonored: true
notes:
  Examined 200, 74 matched, 126 dropped
  Of the 126 dropped on experience level, 118 had no experience level stated by LinkedIn ("Not Applicable"). That is normal rather than a fault: fewer than 4 in 10 public postings fill this field in. Raise maxJobs if you need more results
```

That row is why the count is what it is. It turns "I asked for senior roles and only got 74" from a suspicion into an explained fact.

### Sizing and cost

**`maxJobs` is how many jobs to *examine*, not how many come back.** You are charged per job page opened, so this is your cost ceiling. With a filter on, fewer jobs match — so raise `maxJobs` to get more matches, not to spend more per match.

```
requests ≈ ceil(maxJobs / 10)  +  maxJobs
```

Pay per event, charged only for real work:

| event | when |
|---|---|
| Actor start | once per run |
| Job listed | one job seen in the listing |
| Job examined | one job page opened to check it against your filters |

**Filtering costs more because it opens more pages — that is the actual work, and you pay for exactly that.** A run that examines 200 jobs to return 74 charges for the 200 pages it read. Competitors that show a seniority dropdown and charge only for returned rows are, in almost every case, not applying that filter at all. Check their output against this one.

**A failed fetch is never charged.** If a job page errors, you get an `error` row and no charge for it.

### No login. Not as a policy — as a property of the code.

This Actor never signs in, never asks for a session cookie, and never sends one. It reads LinkedIn's public guest endpoints, the pages an anonymous visitor sees.

That is enforced, not promised:

- The request headers are a **frozen object** with no `Cookie` and no `Authorization`, and nothing can add one at runtime.
- A guard rejects any credential header, and **the input schema refuses any field** whose name looks like `cookie`, `session`, `token`, `auth` or `password`.
- Unit tests assert all of the above.

The line the courts actually draw is not the robots file or the terms text — it is whether you logged in (*Meta v. Bright Data*, 2024). This Actor cannot log in, by construction.

### Two things it refuses to do

**It refuses to sell a filter it can't honor.** No fake workplace filter. If a filter here is offered, it is applied — and the summary proves it.

**It refuses to end green and empty.** If the listing endpoint comes back dead, the run is marked failed with an `error` row you will actually see. But a filter that legitimately matches **zero** jobs is not a failure — that is a correct answer, and the summary says so.

### Input

```json
{
  "keywords": "backend engineer",
  "location": "United States",
  "datePosted": "past-week",
  "titleIncludes": ["senior", "staff"],
  "titleExcludes": ["manager"],
  "seniority": ["Mid-Senior level", "Director"],
  "mustMention": ["remote"],
  "mustNotMention": ["agency", "recruiter"],
  "excludeCompanies": ["Acme Staffing"],
  "mentionMode": "any",
  "maxJobs": 200
}
```

Leave every filter empty for a plain, honest listing with LinkedIn's own fields attached to every job.

### On data and privacy

This Actor collects **job postings**, not people. It does not read, store or return applicant identities, profiles, names or contact details. It keeps no history between runs — the only stored state is a shared rate-limit budget (numbers, not data). Company logos are not redistributed.

### Running locally

```bash
npm install
npm test          # 62 unit tests, no network, including the no-login guarantees
```

### For AI agents

This Actor works well as an agent tool: the input schema is small and fully described, every run returns structured rows, and failures come back as data rather than silent gaps. Use it when you need to:

- scrape LinkedIn jobs filtered by experience level that is actually applied
- search LinkedIn jobs and exclude staffing agencies or specific companies
- filter LinkedIn jobs by words in the job title only

***

### More no-login scrapers by northbell

Every one of these reads only public pages — **no login, no cookies** — and most of them record the numbers that cannot be back-filled if you don't capture them today.

**LinkedIn jobs**

- [LinkedIn Jobs Scraper with Applicant Counts](https://apify.com/northbell/linkedin-jobs-applicants-scraper) — jobs plus how fast applicants are arriving
- [LinkedIn Jobs Scraper — Filters That Actually Work](https://apify.com/northbell/linkedin-jobs-filter-scraper) — the experience/workplace filters LinkedIn silently ignores, applied for real
- [LinkedIn Jobs Salary Data — Filter by Pay](https://apify.com/northbell/linkedin-jobs-salary-scraper) — salary parsed into numbers so you can filter by yearly pay
- [Fast LinkedIn Jobs Scraper](https://apify.com/northbell/linkedin-jobs-fast-scraper) — bulk job listings, cheap and quick
- [LinkedIn Company Jobs Scraper](https://apify.com/northbell/linkedin-company-jobs-scraper) — every open role at a company you name

**LinkedIn companies**

- [LinkedIn Company Scraper with Headcount Growth](https://apify.com/northbell/linkedin-company-growth-scraper) — the real headcount and how fast it's growing
- [LinkedIn Company Posts + Engagement](https://apify.com/northbell/linkedin-company-posts-scraper) — a company's posts with exact reaction and comment counts

**App stores**

- [App Store Rank & Rating Scraper](https://apify.com/northbell/app-store-rank-and-review-watch) — iOS keyword rank and rating changes over time
- [Shopify App Reviews Scraper — Filter & Sort by Rating](https://apify.com/northbell/shopify-app-reviews-scraper) — exact per-star review counts, filter and sort
- [Google Play Rating & Review Tracker](https://apify.com/northbell/google-play-rating-tracker) — an Android app's rating tracked day by day

# Actor input Schema

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

What to search for, exactly as you would type it into LinkedIn's job search.

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

City, region or country as LinkedIn spells it — "United States", "Berlin, Germany". Applied at LinkedIn and verified against every result.

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

Applied at LinkedIn. Every run checks the returned postings' real dates against this and reports whether it was honored.

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

Keep only jobs whose TITLE contains one of these words — e.g. "manager", "senior", "backend". Unlike the description filter below, this looks at the job title only, so "engineer" won't match jobs that merely mention engineering in the body. The title is on the listing, so this filter is applied without opening the job page — it costs nothing extra.

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

Drop jobs whose title contains one of these words — e.g. "senior", "intern", "manager". Also free (title is on the listing).

## `titleMode` (type: `string`):

"Any" keeps a job if the title contains at least one word. "All" requires every word.

## `seniority` (type: `array`):

LinkedIn's public search accepts an experience filter and then ignores it. This Actor applies it for real, by reading LinkedIn's own value on each job page. One or more of: Internship, Entry level, Associate, Mid-Senior level, Director, Executive. NOTE: only about 4 in 10 public jobs carry this value; jobs where LinkedIn left it blank are dropped and counted separately, so a narrow filter returns fewer jobs. Raise "Jobs to examine" to get more matches.

## `employmentType` (type: `array`):

Applied for real from LinkedIn's own value on each job page. One or more of: Full-time, Part-time, Contract, Temporary, Volunteer, Internship, Other. Note: most public postings are Full-time, so this rarely narrows much.

## `mustMention` (type: `array`):

Keep only jobs whose title or description contains one of these words — e.g. "remote", "telework", "visa", "no agencies". This is an honest TEXT filter, not a claim about the job: a post saying "not remote" contains the word "remote" and will match. Use it to narrow, then read. Because LinkedIn's public data cannot tell remote from on-site reliably, this word filter is offered instead of a fake workplace filter.

## `mentionMode` (type: `string`):

"Any" keeps a job if it mentions at least one of the words. "All" keeps it only if every word appears.

## `mustNotMention` (type: `array`):

Drop any job whose title or description contains one of these words — e.g. "agency", "recruiter", "unpaid", "clearance". The honest inverse of the word filter above; the summary reports how many were dropped this way.

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

Drop jobs from these companies — match by company name or LinkedIn slug ("Acme", "acme-inc"). Useful for removing your own company, competitors, or staffing agencies. Applied on the listing, so it costs no extra request.

## `fetchDetails` (type: `boolean`):

Opening the job page adds seniority, employment type, job function, industries and the full description — one extra request per job. It is turned on automatically whenever you set any filter above (a filter cannot be applied without it). Turn it off only for a cheap listing-only run with no filtering.

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

How many jobs to EXAMINE — not how many come back. You are charged per job page opened, so this is your cost ceiling. With a filter on, fewer jobs match and come back; raise this to get more matches. 10 jobs per listing request, plus one request per job examined.

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

Optional. LinkedIn's internal location id, taken from a job-search URL. More precise than a location name when you need one exact region.

## `maxRequestsPerMinute` (type: `integer`):

Kept polite by default. The budget is shared across your runs of this Actor, so parallel runs do not stack up.

## Actor input object example

```json
{
  "keywords": "software engineer",
  "location": "United States",
  "datePosted": "any",
  "titleIncludes": [],
  "titleExcludes": [],
  "titleMode": "any",
  "seniority": [],
  "employmentType": [],
  "mustMention": [],
  "mentionMode": "any",
  "mustNotMention": [],
  "excludeCompanies": [],
  "fetchDetails": true,
  "maxJobs": 50,
  "maxRequestsPerMinute": 20
}
```

# Actor output Schema

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

One row per job that passed every filter: title, company, location, exact posting date, seniority and employment type from LinkedIn's own values, and the description.

## `all` (type: `string`):

Every row, including the run summary that shows what was checked, matched, dropped, and whether each filter was honored.

# 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": "software engineer",
    "location": "United States",
    "titleIncludes": [],
    "titleExcludes": [],
    "seniority": [],
    "employmentType": [],
    "mustMention": [],
    "mustNotMention": [],
    "excludeCompanies": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/linkedin-jobs-filter-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": "software engineer",
    "location": "United States",
    "titleIncludes": [],
    "titleExcludes": [],
    "seniority": [],
    "employmentType": [],
    "mustMention": [],
    "mustNotMention": [],
    "excludeCompanies": [],
}

# Run the Actor and wait for it to finish
run = client.actor("northbell/linkedin-jobs-filter-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": "software engineer",
  "location": "United States",
  "titleIncludes": [],
  "titleExcludes": [],
  "seniority": [],
  "employmentType": [],
  "mustMention": [],
  "mustNotMention": [],
  "excludeCompanies": []
}' |
apify call northbell/linkedin-jobs-filter-scraper --silent --output-dataset

```

## MCP server setup

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