LinkedIn Salary Scraper — Jobs with Salary, Filter by Pay avatar

LinkedIn Salary Scraper — Jobs with Salary, Filter by Pay

Pricing

from $1.20 / 1,000 salary checks

Go to Apify Store
LinkedIn Salary Scraper — Jobs with Salary, Filter by Pay

LinkedIn Salary Scraper — Jobs with Salary, Filter by Pay

Scrape public LinkedIn jobs (search or job URLs) and get pay as numbers - min, max, currency, period, yearly. US jobs also get the official wage levels (DOL, from BLS) for the same job and metro, and which level the pay reaches. Pay not stated is marked, never guessed. Empty runs cost $0. No login.

Pricing

from $1.20 / 1,000 salary checks

Rating

0.0

(0)

Developer

Northbell

Northbell

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

1

Monthly active users

3 days ago

Last modified

Security

Limited permissions

Share

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.

$1.90 per 1,000 jobs read — 100 jobs cost $0.20 including the run start, so a scheduled run that reads 20 jobs costs about 5 cents. Measured on October 8, 2026: 30 US software-engineer jobs, 28 of them (93%) with pay parsed into numbers — the pay is read from the description and from LinkedIn's own "Base pay range" box; 30 Canadian ones, 24 with pay, all in CAD.

Running it on a schedule? Don't pay for the same job twice. Every job read is charged, and with Posted within left on Any time each run reads the jobs it already gave you. Set Posted within to the shortest window that still covers the time between runs — Past 24 hours for a daily run, Past week for a weekly one — and each run reads mostly new jobs, with nothing posted between runs skipped. Running several times a day, Past 24 hours is the shortest choice, so the same day's jobs are still read again.

Then: paste the jobUrl column into LinkedIn Jobs Scraper with Applicant Counts (input jobs) to see how many people applied to each of those jobs, and how fast.

Call it from a script or a schedule

curl -X POST "https://api.apify.com/v2/acts/northbell~linkedin-jobs-salary-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"keywords":"software engineer","location":"United States","maxJobs":20,"datePosted":"past-24-hours"}'

The response is the rows themselves (a JSON array). To run it on a timetable instead, save the input as a task in Apify Console and add a Schedule; n8n, Make and Zapier run it through their Apify app with the same input.

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. This one parses the pay into numbers you can sort, filter and average — annualSalaryMin / annualSalaryMax, currency, period, and the original phrase in salaryText — and lets you ask "show me jobs paying over $150k" as an input, with the fill rate reported on every run.

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.

Run it again and again

  • On a schedule — save your input as a task and add a Schedule: the same search every morning, for example at 07:00. Send each run to Google Sheets, Slack or email with an integration on the task.
  • Watch for changes — From the second run of the same search, every row says isNewSinceLastRun and the summary row counts newSinceLastRun. Tick Only jobs new since my last run and known postings are skipped before they are opened, so you pay only for the new ones.
  • In bulk — Raise Max jobs (up to 1,000) to read a whole search in one run.

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 moneyWhat it really isHandled by
$20M+ raised from investorsfundingrejected — near "raised/funding/series"
$800 billion marketmarket sizerejected — implausible as a yearly wage
$5,000 signing bonusone-off bonusrejected — near "bonus/signing"
Annual maximum out of pocket $200,000insurance benefitrejected — near "deductible/insurance/out-of-pocket"
on-target earnings of $175,000OTE/commissionskipped 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:

fieldmeaning
salaryFoundwhether the posting stated pay
currencyUSD, GBP, CAD, … from the symbol or code used. A bare $ takes the currency of the job's location (a Toronto job is CAD, a Sydney job AUD) unless the posting says USD; then currencyFromLocation is true
salaryPeriodyear / hour / month — how it was originally written
salaryMin, salaryMaxthe range in that original period
annualSalaryMin, annualSalaryMaxnormalized to a yearly figure for comparison
isNewSinceLastRuntrue if the posting was not in your previous run of the same search (keywords and location), false if it was, null on the first run and for job URLs
annualSalaryMinUsd, annualSalaryMaxUsd, fxRateDatethe same yearly figure in US dollars, at the European Central Bank's reference rate of fxRateDate, so pay in London, Toronto and Bangalore sits on one axis. USD jobs are copied as they are; if the rate cannot be read the columns are null, never a guessed rate
salaryTextthe exact phrase from the posting, so you can check us
officialWageUS jobs paid in USD: the official wage levels for the same occupation in the same metro area, and which level the posted pay reaches (below)
title, company, location, postedOn, seniority, employmentType, jobUrlthe posting

Posted pay against the official wage (added 2026-11-03). For a US job paid in USD, officialWage puts the posting beside the US Department of Labor's four wage levels for that occupation and metro area (the OFLC wage library, built from the BLS OEWS survey; the levels employers must meet in H-1B and green-card filings, wage year July 2026 – June 2027). level1 is about the 17th percentile, level2 the 34th, level3Median the median, level4 the 67th, all yearly in USD. postedMinReachesLevel and postedMaxReachesLevel say which level the bottom and top of the posted range reach (0 = below level 1, 4 = level 4 or above). occupation, occupationCode (SOC) and matchedTitle show how the job title was read; area is the metro area. The title is matched only to an exact title from ONET's list of job titles (after dropping words such as Senior, II or Remote, then leading words such as Backend, or one or two trailing words such as ICU, but never a word that changes the occupation: High School Math Teacher is not read as a math teacher); an engineer or developer named after a language or platform (Java, iOS, Android, Golang) is read as a software developer; a title ONET gives equally to two occupations, and a postdoc's or professor's title, is not matched. titleFromOneSource is true when O*NET has the title from a single source: check the occupation (on 45 posting titles, 4 of 11 such matches were the wrong one). The place is a metro area's city (Austin, TX; Greater Boston) or a town matched to its metro area through its county (Conroe, TX is in the Houston area; Census 2020 places); a job listed only as "United States" or a state is not matched. When nothing is matched, officialWage is { "matched": false, "reason": … }. Measured on our own run of "software engineer" in Austin: 10 of the 13 USD rows got levels. No extra charge.

All four levels for any title and city, without a posting (added 2026-11-04). Put a job title (or a SOC code) and a US city on each line, with a salary if you want to see which level it reaches, in Prevailing Wage Levels: the same DOL table as officialWage, and a line it cannot match is free.

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.

Bands, not rows: put this Actor's dataset into the Pay Band Calculator to get median and p10–p90 yearly pay per department, level or location. The rows go in as they are.

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:

eventpricewhen
Actor start$0.01once per run that returns a job
Job listed$0.0004one job seen in the listing
Job examined$0.0015one job page opened and read for pay

Together that is $1.90 per 1,000 jobs read. With Only jobs that state pay on (or a minimum salary set), you pay only for the jobs returned; jobs the filter drops cost nothing. A daily run of 50 jobs costs about $3 a month.

Our own running cost is about **$0.023 per 1,000 rows** of Apify platform usage at 256 MB (same 30-job search before and after, 11 October 2026; it was $0.113 at 512 MB in September — [how it was measured](https://dev.to/apify/i-measured-what-my-11-actors-cost-to-run-the-96x-spread-was-mostly-one-config-field-hoj)) — already included in the prices above; nothing is billed on top.

What you never pay for:

  • A failed fetch. A job page that could not be read is not charged.
  • A run that returns nothing. The $0.01 run-start fee is charged together with the first job, so a search with no results, or a run LinkedIn refuses outright, costs $0.
  • Jobs a stopped run never read. A run near its time limit stops 90 seconds early and lists the jobs it did not read in a free row, so you can run just those again.
  • Jobs a filter leaves out. With Only jobs that state pay on (or a minimum salary set), jobs the filter drops are not 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

Only at these companies. Give companies (LinkedIn company URLs such as https://www.linkedin.com/company/stripe/, slugs or numeric IDs, up to 20) and the search keeps to their jobs — LinkedIn's own company filter, with or without keywords: {"companies": ["stripe", "notion"], "keywords": "engineer", "location": "United States"} gives the pay ranges those companies post. Reading the company pages is free; a company that cannot be read is a free error row and the rest are searched. Without a location LinkedIn searches the US.

A city in the keywords is searched as the location. LinkedIn ignores a city written inside the keywords: data engineer austin with Location United States returned 10 jobs across the US and none in Austin. When the keywords contain a major city and Location is empty or that city's country, the run searches the city as the location (data engineer in Austin, United States: 10 of 10 in Austin) and says so in the log and in the summary's searchedAs. A Location you set to a city, a different country or a geoId is never changed. (Fixed 22 Oct 2026.)

Already have the job URLs? Paste them into Job URLs (jobs) instead of searching — for example the jobs the free LinkedIn Job Details marked salaryMentioned: true. Each job comes back with its salary as numbers, at the same price; minSalary and onlyWithSalary still apply. Added 2026-10-17.

Want the employer too? Tick Include company details (includeCompany) and each returned job gets companyDetails at the end of the row: industry, headquarters, founded, website, company type, size band, employees on LinkedIn and followers, read from the public company page once per company. No extra charge; off by default, so existing rows stay as they were. For how fast those companies are growing, paste companyDetails.url into LinkedIn Company Scraper — Employee Count & Headcount Growth. Added 2026-10-17.

{
"jobs": ["https://www.linkedin.com/jobs/view/4419969671"]
}

Or search:

{
"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 this run's rate-limit budget. Company logos are not redistributed.

officialWage uses the US Department of Labor's OFLC wage library (flag.dol.gov, public domain) and occupation titles from the ONET 30.0 Database by the U.S. Department of Labor, Employment and Training Administration (USDOL/ETA), used under the CC BY 4.0 license. ONET® is a trademark of USDOL/ETA. Towns are matched to metro areas with the US Census Bureau's 2020 place-by-county list.

Running locally

npm install
npm test # unit tests, no network, including the no-login guarantees and the salary parser's traps
**Something broke or looks wrong?** Open an issue on this Actor's Issues tab and I'll look at it. **Found it useful?** A short review on the Store page helps other people find it.

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

How the salary parser was tested

The write-up of the six ways it was quietly wrong on real postings, and the fixes, is published under the Apify organisation on DEV: I scraped 89 job postings to find out how my salary parser was lying to me

The parser and every regression test in that post are open source, with no dependencies: northbell-dev/honest-scraping


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 companies

Salaries from other sources (pay split into min / max / period the same way)

App stores