# LinkedIn Jobs Salary Data — Filter by Pay (`northbell/linkedin-jobs-salary-scraper`) Actor

Scrape public LinkedIn jobs and get the salary as structured data — min, max, currency, period — normalized to a yearly figure, so you can filter to jobs paying over a threshold and see the real pay distribution. Honest about the ~60% of jobs that state pay. No login, no cookies.

- **URL**: https://apify.com/northbell/linkedin-jobs-salary-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 Salary Data — Filter by Pay

Scrape public LinkedIn jobs and get **the pay as structured data** — minimum, maximum, currency, and whether it is hourly or yearly — normalized to a yearly figure so you can **filter to jobs paying over a threshold** and see the real pay distribution. No login, no cookies.

### What every other jobs scraper leaves you to do

The popular LinkedIn job scrapers hand you the description as a wall of text and let *you* dig the salary out of it. None of them return the pay as numbers you can sort, filter, or average. So "show me jobs paying over $150k" is a thing you cannot ask — you scrape everything and grep by hand.

This Actor does that work, honestly:

- **Pay as fields.** `$150K – $220K` in the text becomes `annualSalaryMin: 150000`, `annualSalaryMax: 220000`, `currency: USD`, `salaryPeriod: year`.
- **Hourly and monthly normalized.** `$46 – $50 per hour` becomes a yearly figure (× 2080 hours) so a warehouse job and a director job sit on the same axis. The original is kept too.
- **Filter by pay.** Set a minimum yearly salary and get back only the jobs that reach it.
- **The real distribution.** Every run reports the median and range of pay across the jobs that stated it.

### Honest about what isn't there

**About 6 in 10 US jobs state their pay** (we measured it live — pay-transparency laws made US postings the most forthcoming; elsewhere it is lower). The rest simply do not write a number down. This Actor does not invent one. Each job is tagged `salaryFound: true/false`, and the summary tells you the fill rate every run.

And it does not mistake other numbers for salary. Job descriptions are full of dollar figures that are **not** pay:

| Looks like money | What it really is | Handled by |
|---|---|---|
| `$20M+ raised from investors` | funding | rejected — near "raised/funding/series" |
| `$800 billion market` | market size | rejected — implausible as a yearly wage |
| `$5,000 signing bonus` | one-off bonus | rejected — near "bonus/signing" |
| `Annual maximum out of pocket $200,000` | insurance benefit | rejected — near "deductible/insurance/out-of-pocket" |
| `on-target earnings of $175,000` | OTE/commission | skipped in favour of the base salary |

The rule that catches most of them: **normalize to a yearly figure first, and if it lands outside $8,000–$2,000,000, it is not a wage.** On top of that, a number is only taken as pay when its own label says so ("salary", "pay range", "per hour"), never a bare figure sitting in a sentence.

### What you get

Two kinds of row, tagged by `type`.

**`job`** — one per returned job:

| field | meaning |
|---|---|
| `salaryFound` | whether the posting stated pay |
| `currency` | `USD`, `GBP`, `CAD`, … from the symbol used |
| `salaryPeriod` | `year` / `hour` / `month` — how it was originally written |
| `salaryMin`, `salaryMax` | the range in that original period |
| `annualSalaryMin`, `annualSalaryMax` | normalized to a yearly figure for comparison |
| `salaryText` | the exact phrase from the posting, so you can check us |
| `title`, `company`, `location`, `postedOn`, `seniority`, `employmentType`, `jobUrl` | the posting |

**`summary`** — one per run: how many jobs stated pay (`salaryFillRatePct`), how many were returned or dropped, and the pay distribution (`annualSalaryMedian`, min, max) across the jobs that stated it, plus a breakdown by currency and period.

### Sizing and cost

**`maxJobs` is how many jobs to *examine*, not how many come back.** Pay lives on the job page, so every job is opened and read. You are charged per job page opened — that is your cost ceiling. With a minimum-salary filter on, fewer jobs match, so raise `maxJobs` to get more matches.

```
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 and read for pay |

**A failed fetch is never charged.**

### 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. 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 that looks like `cookie`, `token` or `password`. Unit tests assert all of it. The line the courts draw is whether you logged in (*Meta v. Bright Data*, 2024) — this Actor cannot, by construction.

### Input

```json
{
  "keywords": "software engineer",
  "location": "United States",
  "datePosted": "past-week",
  "minSalary": 150000,
  "maxJobs": 200
}
```

Leave `minSalary` empty (and `onlyWithSalary` off) to get every job, each tagged with whether pay was found and, where it was, parsed into fields.

### 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. Company logos are not redistributed.

### Running locally

```bash
npm install
npm test          # 31 unit tests, no network, including the no-login guarantees and the salary parser's traps
```

### 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 with the salary parsed into numbers
- find LinkedIn jobs paying over a yearly salary threshold
- get the salary distribution (median, range) for a job search

***

### 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. US postings state pay most often (pay-transparency laws), so a US location gives the highest fill rate.

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

Applied at LinkedIn.

## `minSalary` (type: `integer`):

Keep only jobs paying at least this much per year. Hourly and monthly pay is normalized to a yearly figure first (hourly × 2080). A job whose stated top pay reaches the threshold is kept. Jobs with no stated pay cannot be checked, so they are dropped when this is set — the summary reports how many.

## `onlyWithSalary` (type: `boolean`):

Drop jobs whose posting does not state a salary. Turned on automatically when you set a minimum salary. Leave off to get every job, each tagged with whether pay was found.

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

How many jobs to open and read for pay — not how many come back. You are charged per job page opened, so this is your cost ceiling. About 6 in 10 US jobs state pay; filtering returns fewer, so raise this to get more matches. 10 jobs per listing request, plus one request per job.

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

Optional. LinkedIn's internal location id, taken from a job-search URL. More precise than a location name.

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

Kept polite by default. The budget is shared across your runs of this Actor.

## Actor input object example

```json
{
  "keywords": "software engineer",
  "location": "United States",
  "datePosted": "any",
  "onlyWithSalary": false,
  "maxJobs": 50,
  "maxRequestsPerMinute": 20
}
```

# Actor output Schema

## `salaries` (type: `string`):

One row per returned job: title, company, location, and pay as currency + yearly min/max + how it was originally stated.

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

Every row, including the run summary with the salary fill rate and distribution.

# 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"
};

// Run the Actor and wait for it to finish
const run = await client.actor("northbell/linkedin-jobs-salary-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",
}

# Run the Actor and wait for it to finish
run = client.actor("northbell/linkedin-jobs-salary-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"
}' |
apify call northbell/linkedin-jobs-salary-scraper --silent --output-dataset

```

## MCP server setup

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