LinkedIn Jobs Scraper With Salary Range Filters avatar

LinkedIn Jobs Scraper With Salary Range Filters

Pricing

$19.99/month + usage

Go to Apify Store
LinkedIn Jobs Scraper With Salary Range Filters

LinkedIn Jobs Scraper With Salary Range Filters

Scrape LinkedIn job listings with titles, companies, locations, salaries, seniority, and descriptions. Ideal for recruiters, analysts, and job-market insights with clean structured output.

Pricing

$19.99/month + usage

Rating

0.0

(0)

Developer

Scraper Engine

Scraper Engine

Maintained by Community

Actor stats

0

Bookmarked

327

Total users

0

Monthly active users

6 days ago

Last modified

Share

LinkedIn Jobs Scraper — Salary Range Filters and Parsed Pay Fields

LinkedIn Jobs Scraper With Salary Range Filters extracts public LinkedIn job listings and turns advertised pay into numbers you can sort on. Every row carries salaryMin, salaryMax, salaryCurrency, salaryPeriod and a normalized salaryYearlyMin / salaryYearlyMax, alongside title, companyName, location, publishedAt and the full description — as typed JSON, no HTML parsing. Set a yearly pay floor, keep only listings that state pay, or push a salary floor to LinkedIn itself. Start a run from the Apify Console with no required input.

What is LinkedIn Jobs Scraper With Salary Range Filters?

It is an Apify Actor that queries LinkedIn's public, logged-out job surfaces and returns 34 fields per posting, ten of which describe compensation. It searches by role, location, company and LinkedIn's own filters, then screens results against a yearly pay floor, a currency, or a stated-pay requirement before anything is written to your dataset.

No LinkedIn account, cookie or session token is required. Everything it returns is what a signed-out visitor sees.

Built for compensation analysts, recruiters, labour-market researchers and AI engineers who need pay as a number rather than a sentence.

What LinkedIn job and pay data is publicly available to scrape?

A signed-out visitor can read the full job posting on LinkedIn: title, employer, location, posting date, applicant count, employment type, seniority, industry and the complete description body. What a visitor cannot see is anything belonging to LinkedIn's logged-in product — and, critically, LinkedIn's structured pay field is not part of the public page.

Data categoryPublicly available (no login)Gated behind a LinkedIn login or paid seat
Job identity and employerTitle, company name, company URL, numeric company ID, job URL
Posting timelineExact ISO posting date on the search card, plus LinkedIn's relative "3 days ago" text
Competition signalApplicant count as LinkedIn words it (exact, "over N", "be among the first N")Applicant profiles and screening answers
ClassificationEmployment type, seniority level, job function, industry
Description bodyFull plain text and the original HTML markup
Structured pay rangeNot published on the logged-out surface — measured 0 of 48 search cards and 0 of 30 detail fragments carried a salary field or a pay currency in the criteria listLinkedIn's own salary insights product
Job poster identityName and profile URL where LinkedIn renders a poster block — measured on 6 of 60 listingsRecruiter contact details, InMail
Apply destinationNot on the guest surface — the apply-URL element was absent on 60 of 60 fragments, so no apply URL or Easy Apply flag is returnedThe apply flow itself

This Actor only returns publicly visible data — what any visitor sees. Nothing behind a login wall.

Where do the salary numbers come from?

Every pay figure in the output is parsed out of the job description's free text. LinkedIn publishes no structured pay field on its public job pages, so there is nothing else to read: salarySource is "description" whenever a figure was found and "none" when it was not. It never says "structured", because a structured source does not exist here.

That has consequences worth understanding before you set a filter.

A job with no stated pay is not a failure

Many LinkedIn postings simply do not mention money. Those rows come back with hasSalary: false, salarySource: "none" and every pay column null. That is an honest result, not a scrape error.

What the pay filters actually do to those rows

InputWhere it runsWhat happens to a job that states no pay
useLinkedInSalaryBucketServer-side — sent to LinkedIn as f_SB2 in the search queryLinkedIn decides; the Actor never sees the excluded jobs
payFloorYearlyAfter the job page is fetched and parsed✅ Kept — a floor cannot judge a job that states nothing
payCurrencyAfter the job page is fetched and parsed✅ Kept — same reason
onlyJobsStatingPayAfter the job page is fetched and parsed❌ Dropped — this is the only input that rejects on absence

A yearly pay floor on its own does not silently delete every posting that stayed quiet about money. payFloorYearly and payCurrency screen only jobs that actually stated a figure. If you want a pay-only dataset, you have to ask for it explicitly with onlyJobsStatingPay.

The floor compares against the top of the stated range, so a job advertising "$90,000 – $140,000" satisfies a payFloorYearly of 120000. A job that states pay but whose period could not be resolved is rejected as not comparable rather than guessed at.

Filtered-out rows are not charged

The three post-fetch filters run after the detail page has been retrieved. A screened-out job is never pushed to the dataset and never fires the row_result charged event — you asked not to receive it, so it is not billed. The run log names the reason for each one: no_stated_pay, currency_mismatch, below_pay_floor or pay_not_comparable.

Fetching still takes time, which is why the Actor scans a wider candidate pool when a pay screen is on: it collects up to maxJobs × 8 candidates, capped at maxJobs + 400 and at LinkedIn's 1,000-result ceiling, and stops the moment maxJobs rows have passed.

Period is normalized, currency is not

An hourly rate and an annual salary are made comparable before filtering. salaryYearlyMin and salaryYearlyMax are the stated figures multiplied by a fixed, stated assumption:

Stated periodMultiplier to yearly
hour2080 (40 hours × 52 weeks)
day260 (5 days × 52 weeks)
week52
month12
year1

An hourly rate has no true annual equivalent without knowing the contracted hours, so treat a normalized figure from an hourly posting as an estimate built on that assumption. salaryPeriodSource tells you which case you are in: "stated" when the listing wrote a unit ("$40–50/hr"), "inferred" when the Actor deduced the period from magnitude ("$150,000 to $350,000" reads as yearly). An inferred period is never passed off as a stated one.

Currency is detected but never converted. payFloorYearly compares raw numbers, so a ₹1,800,000 posting clears a floor of 150000 and a £120,000 posting does not. If your search spans currencies, set payCurrency as well — otherwise the comparison is arithmetic across units that are not the same unit.

Why funding rounds do not end up in the salary column

A bare currency regex over a job description returns nonsense: "raised $20M from Headline" becomes a $20 salary, and "$500/month dinner stipend" becomes $6,000 a year. A money figure is accepted here only when it carries a period unit, sits within 90 characters of genuine compensation wording, or forms a two-sided range between 1,000 and 2,000,000 that can only be pay. It is rejected outright when funding, revenue, valuation, budget or reimbursement wording sits within 70 characters, and a perk word ("stipend", "commuter", "wellness") vetoes it unless real compensation wording is also nearby. A normalized yearly figure outside 5,000–5,000,000 is discarded rather than emitted.

What data can I extract with this LinkedIn jobs scraper?

Each row describes one job posting across four groups: job and employer identity, compensation, hiring signals and classification, and the description body. All 34 keys are present on every row — an unavailable value comes back as null, never as a missing key.

Field nameDescription
idLinkedIn's numeric job posting ID — stable, use it to deduplicate across runs
titleJob title as posted
companyNameHiring employer's name
locationLocation as LinkedIn renders it on the posting
publishedAtExact posting date, taken from the search card's ISO timestamp
postedTimeLinkedIn's relative string, e.g. "3 days ago"
salaryRaw pay phrase lifted from the description, exactly as written
salaryTextThe pay token the structured parser matched, before decomposition
hasSalarytrue when a pay figure was credibly found, false otherwise
salaryMinLower stated figure as a number, in the stated currency and period
salaryMaxUpper stated figure, null when the posting quoted a single number
salaryCurrencyCurrency code detected from the symbol or code: USD, GBP, EUR, INR, AUD, CAD, SGD, CHF, NZD
salaryPeriodhour, day, week, month or year
salaryPeriodSourcestated when the listing wrote the unit, inferred when it was deduced from magnitude
salaryYearlyMinsalaryMin normalized to a yearly figure
salaryYearlyMaxsalaryMax normalized to a yearly figure
salarySourcedescription when pay was parsed out of the body, none when no pay was stated
applicationsCountApplicant figure as a string, with commas stripped
applicantsCountTypePrecision of that figure: exact, at_least or fewer_than
applicantsTextThe applicant line exactly as LinkedIn wrote it
contractTypeLinkedIn's Employment type criterion, e.g. "Full-time"
experienceLevelLinkedIn's Seniority level criterion, e.g. "Mid-Senior level"
workTypeLinkedIn's Job function criterion, e.g. "Engineering and Information Technology"
sectorLinkedIn's Industries criterion
jobUrlCanonical link back to the posting
companyUrlEmployer's LinkedIn company page, query string stripped
companyIdEmployer's numeric LinkedIn company ID
benefitsBenefit keywords detected in the description prose — a text signal, not a LinkedIn structured block
posterFullNameName of the person who posted the role, where LinkedIn renders one
posterProfileUrlThat person's LinkedIn profile URL, read from the same anchor so the two always agree
descriptionFull plain-text description body
descriptionHtmlThe same body with its original HTML markup preserved
errorReasonnull on a real job row; set on an uncharged accounting row
scrapedAtUTC timestamp of extraction, ISO 8601

Compensation fields

Ten of the 34 keys describe pay, and they are built to be audited rather than trusted blindly. salary and salaryText keep the employer's own wording so you can check any number against the sentence it came from. salaryMin / salaryMax hold the stated figures in the stated period. salaryYearlyMin / salaryYearlyMax hold the comparable annual figures. salaryCurrency, salaryPeriod and salaryPeriodSource describe the units, and hasSalary plus salarySource tell you whether there was anything to parse at all.

Hiring signal and classification fields

applicationsCount never lies about its own precision: LinkedIn states applicants three different ways, and applicantsCountType records which one you got, with applicantsText preserving the original line. publishedAt is an exact date rather than a rounded relative string, which is what makes a daily delta job possible. contractType, experienceLevel, workType and sector come straight from LinkedIn's criteria list.

Add-on: need more LinkedIn data?

Pay data pairs well with the people and company records behind the posting. LinkedIn Company URL - Mass Profile Finder With Firmographics resolves the employers you find here into full company records, and LinkedIn Profile Phone Number Scraper By Seniority & Title Filter works the other direction, from a role to the people holding it. Both are listed in the related scrapers table at the end.

Why not build this yourself?

LinkedIn has no public jobs API that returns arbitrary search results to an anonymous caller. Its Talent Solutions and partner APIs require an approved partnership and serve the jobs you posted, not the open market — so the public surface is the only route to market-wide pay data, and building it yourself means owning every one of the following.

The guest search endpoint serves 10 cards per page, not 25; a scraper that steps 25 skips 15 of every 25 result slots, which measured out at 40 unique jobs instead of 98 across the first 100 slots. LinkedIn intermittently pins a guest session so it stops honouring the start parameter and re-serves page one forever — waiting does not fix it, clearing the cookie jar and re-warming does. Fetching the HTML search page before calling the API is itself what triggers the pin, so the obvious warm-up is the bug. HTTP 999 is a throttle rather than a ban and needs backoff, not a proxy switch. The detail fragments declare UTF-8 in the HTTP header but carry no in-document meta charset, so a parser handed raw bytes will mojibake every "£" and every curly apostrophe. And pay must be pulled out of prose without mistaking a Series B for a salary.

Each is a small problem. Together they are why a first-pass LinkedIn jobs scraper produces a pay dataset you cannot use.

⚠️ What this scraper does not do

Four things deserve a straight answer rather than a footnote.

sortOrder is ignored by LinkedIn. The parameter is accepted so existing input JSON keeps working, and any value is taken without error, but the public job endpoint returns results in its own order — the returned job ID lists were byte-identical with no sort, with Most recent and with Most relevant. No sort parameter is sent, and the run log says so when you set one. Use publishedAt for recency; LinkedIn does honour that.

The 1,000-result ceiling is LinkedIn's, not the Actor's. Paging stops at start=1000, where LinkedIn returns HTTP 400. maxJobs accepts up to 1000 for that reason. Going wider means splitting one search across geoId values, employers or publishedAt windows and merging on id.

Proxy groups you pick are not used. Enabling Apify Proxy switches on a fixed ladder — datacenter first, residential on failure — regardless of the groups selected in the proxy editor. The toggle is honoured; the group list is not.

workType means two different things. As an input it filters on-site / remote / hybrid through LinkedIn's own search parameter. As an output field it carries LinkedIn's "Job function" criterion, e.g. "Engineering and Information Technology". They are unrelated values that share a name for backward compatibility.

How to use LinkedIn Jobs Scraper With Salary Range Filters

The Actor runs on the Apify platform. Start it from the Apify Console, or call it through the Apify API with your Apify token — there is no separate signup and no LinkedIn credential anywhere in the flow.

  1. Open the Actor on Apify and click Try for free.
  2. No parameter is required. Set Role or job title and Where, or leave the prefilled example — which searches Software Engineer roles at Google in the United States — and edit from there. Clear Employers to scrape for a market-wide sweep instead of a company-scoped one.
  3. Set the pay screen you want: a Minimum yearly pay floor, Only jobs that state pay for a pay-only dataset, Pay currency to keep a multi-country search comparable, or LinkedIn salary floor to narrow at the source.
  4. Add the ordinary filters if you need them — Date posted, Employment type, Seniority, On-site / Remote, LinkedIn region ID (geoId).
  5. Set Maximum jobs to save, click Start, and export the dataset as JSON, CSV or Excel.

Leave the proxy off to begin with. LinkedIn's public job surfaces are reachable directly, and that is the cheapest configuration; turn Apify Proxy on only if your region sees blocks or empty pages.

How to scale to bulk job extraction

companyInput is a list, so one run can sweep many employers against the same role, location and pay screen — mix company names, LinkedIn company URLs and numeric company IDs freely in the same array. Job IDs are deduplicated across every employer in the run, so a company reached under two spellings will not produce duplicate rows. Deduplication is per run; merging across runs is on you.

Because a single search stops at roughly 1,000 results, wide collection is better done by splitting than by raising maxJobs. Run the same role across several geoId values or publishedAt windows and merge the datasets on id, which is a stable LinkedIn identifier, so deduplication is exact rather than fuzzy.

What can you do with LinkedIn salary data?

  • 💰 A compensation analyst benchmarking a role uses salaryYearlyMin, salaryYearlyMax and payCurrency set to USD to build a like-for-like distribution for one title in one market, keeping only rows where salaryPeriodSource is stated when explicitly-quoted periods matter.
  • 🧾 A recruiter setting an offer range turns on onlyJobsStatingPay, filters by experienceLevel, and reads salaryText to see exactly how competing employers phrased the band they published.
  • 📈 A labour-market researcher runs the same role and geoId weekly, keys on id and publishedAt, and tracks the share of postings where hasSalary is true as a pay-transparency time series.
  • 🎯 A B2B sales team treats hiring as a buying signal, joins companyId onto existing account records, and ranks accounts by how many sector-matched roles they posted inside a publishedAt window.
  • 🤖 An AI engineer indexes description, title and sector into a vector store and attaches salaryYearlyMin / salaryYearlyMax as numeric metadata, so a RAG agent answers "what does this role pay in Berlin" with filterable numbers instead of prose it has to re-parse.

Any agent framework that can make an HTTP request can call the Actor and consume these rows directly.

How does the scraper handle rate limits and blocking?

It runs against LinkedIn's public guest endpoints with plain HTTP requests — no login, no cookie you supply, no browser, no TLS impersonation — and paces itself deliberately rather than racing. Detail pages are fetched two at a time, in batches of three, with a 1.2-second delay plus jitter before each request and a 2.5-second pause between batches; search pages are requested 1.2 seconds apart. Each request gets three attempts with a 20-second timeout.

HTTP 999 is LinkedIn's throttle rather than a block, so it is retried with a widening backoff instead of being treated as a failure. HTTP 403 and 429 trigger a proxy escalation on the first attempt when Apify Proxy is available, then exponential backoff. A suspiciously small HTML response — LinkedIn's empty-page signature — is retried the same way. When LinkedIn pins a guest session and starts re-serving page one, the Actor clears the cookie jar, re-warms the session and re-requests the same page, up to three times per search.

It does not solve CAPTCHAs and does not claim to. If a job still cannot be fetched or parsed after its retries, it is written as an uncharged accounting row rather than silently disappearing from the count.

⬇️ Input

No parameter is required. Called with an empty input, the Actor falls back to a United States search with no role keyword, maxJobs of 20, pay parsing on and every pay filter off. From the Console, the prefilled example fields give you Software Engineer roles at Google in the United States.

ParameterRequiredTypeDescriptionExample Value
payFloorYearlyNointegerDrop jobs whose stated pay tops out below this yearly figure, applied after hourly, weekly and monthly rates are normalized. Minimum 0. Empty means no floor. Jobs stating no pay are kept unless onlyJobsStatingPay is also on.150000
onlyJobsStatingPayNobooleanKeep only listings where a pay figure was found. Everything with hasSalary: false is skipped — not saved, not charged. Default false.true
payCurrencyNostringKeep only jobs whose parsed pay is in this currency. One of "", USD, GBP, EUR, INR, AUD, CAD, SGD, CHF, NZD. Default "" (any currency)."USD"
useLinkedInSalaryBucketNostringLinkedIn's own salary-bucket filter, sent with the search so LinkedIn narrows results at the source. "1" = $40,000+ through "9" = $200,000+, in LinkedIn's USD banding. Default ""."5"
parsePayFieldsNobooleanFill salaryMin, salaryMax, salaryCurrency, salaryPeriod, salaryYearlyMin and salaryYearlyMax by parsing the description. The pay filters need this on. Default true.true
roleSearchTermsNostringThe role to search for. Overridden by keywords when that is set."Data Analyst"
searchLocationNostringCity, region or country as LinkedIn understands it. Overridden by location when that is set."United Kingdom"
keywordsNostringOriginal base input, kept so existing input JSON keeps working. Takes precedence over roleSearchTerms. Default "".""
locationNostringOriginal base input, kept for compatibility. Takes precedence over searchLocation. Default "".""
companyInputNoarrayEmployers to scrape, as a name, a LinkedIn company URL or a numeric company ID. Empty searches all of LinkedIn.["Google", "https://www.linkedin.com/company/stripe/"]
maxJobsNointegerHow many matching jobs to save. Minimum 1, maximum 1000. Default 20. With a pay screen on, a larger candidate pool is scanned to find this many matches.100
publishedAtNostringDate window: "" any time, "r86400" past 24 hours, "r604800" past week, "r2592000" past month. Default ""."r604800"
workTypeNostringWork arrangement: "" any, "1" on-site, "2" remote, "3" hybrid. Default ""."2"
contractTypeNostringEmployment type: "" any, "F" full-time, "P" part-time, "C" contract, "T" temporary, "I" internship, "V" volunteer. Default ""."F"
experienceLevelNostringSeniority: "" any, "1" internship, "2" entry, "3" associate, "4" mid-senior, "5" director, "6" executive. Default ""."4"
geoIdNostringNumeric LinkedIn location ID sent alongside the location text. 16 countries are suggested; any custom geoId copied from a LinkedIn search URL also works. Non-numeric values are ignored. Default ""."103644278"
sortOrderNostring"", "relevance" or "date". Ignored by LinkedIn — kept for compatibility, logs a notice when set. Default "".""
proxyConfigurationNoobjectApify Proxy settings. Default is no proxy. When enabled, the ladder is datacenter then residential.{"useApifyProxy": false}

Setting parsePayFields to false disables payFloorYearly, onlyJobsStatingPay and payCurrency — the run logs a notice and continues with the structured pay columns empty, though the raw salary phrase is still extracted. useLinkedInSalaryBucket is unaffected, because it runs at the source.

Example input

{
"payFloorYearly": 150000,
"onlyJobsStatingPay": true,
"payCurrency": "USD",
"useLinkedInSalaryBucket": "5",
"parsePayFields": true,
"roleSearchTerms": "Senior Data Engineer",
"searchLocation": "United States",
"keywords": "",
"location": "",
"companyInput": [],
"maxJobs": 100,
"publishedAt": "r604800",
"workType": "2",
"contractType": "F",
"experienceLevel": "4",
"geoId": "103644278",
"sortOrder": "",
"proxyConfiguration": { "useApifyProxy": false }
}

⬆️ Output

One row per job posting, the same 34 keys every run, in a schema that does not shift when LinkedIn changes its page layout. Missing values arrive as null rather than as absent keys, so downstream code never needs a presence check. Export as JSON, CSV or Excel, or read the dataset through the Apify API.

Jobs that could not be fetched or parsed are written as accounting rows with every data field null and errorReason set — fetch_failed, unparseable_page, scrape_exception, throttled_999, blocked_empty_response, timeout, network_error or an http_<status> code. These rows are never charged. Filter them out with errorReason == null to get clean results, or keep them to audit exactly which postings were missed. Jobs removed by your own pay filters are a separate case: they produce no row at all and are likewise not charged.

Example output

{
"id": "4021873455",
"title": "Senior Data Engineer",
"companyName": "Ramp",
"location": "New York, NY",
"publishedAt": "2026-07-21",
"postedTime": "4 days ago",
"salary": "$180,000 - $225,000",
"salaryText": "$180,000 - $225,000",
"hasSalary": true,
"salaryMin": 180000,
"salaryMax": 225000,
"salaryCurrency": "USD",
"salaryPeriod": "year",
"salaryPeriodSource": "inferred",
"salaryYearlyMin": 180000,
"salaryYearlyMax": 225000,
"salarySource": "description",
"applicationsCount": "27",
"applicantsCountType": "exact",
"applicantsText": "27 applicants",
"contractType": "Full-time",
"experienceLevel": "Mid-Senior level",
"workType": "Engineering and Information Technology",
"sector": "Financial Services",
"jobUrl": "https://www.linkedin.com/jobs/view/4021873455",
"companyUrl": "https://www.linkedin.com/company/ramp",
"companyId": "18936223",
"benefits": "401(k), dental, equity, health insurance, paid time off, remote, vision",
"posterFullName": "Priya Raman",
"posterProfileUrl": "https://www.linkedin.com/in/priya-raman",
"description": "Ramp is hiring a Senior Data Engineer for our Platform team. The base salary range for this role is $180,000 - $225,000 plus equity and benefits...",
"descriptionHtml": "<div class=\"show-more-less-html__markup\"><p>Ramp is hiring a Senior Data Engineer for our Platform team.</p></div>",
"errorReason": null,
"scrapedAt": "2026-07-25T09:14:02Z"
}

An hourly posting decomposes the same way, with the period stated rather than inferred:

{
"salary": "$85/hr",
"salaryText": "$85/hr",
"hasSalary": true,
"salaryMin": 85,
"salaryMax": null,
"salaryCurrency": "USD",
"salaryPeriod": "hour",
"salaryPeriodSource": "stated",
"salaryYearlyMin": 176800,
"salaryYearlyMax": null,
"salarySource": "description"
}

How does it work?

The Actor calls two of LinkedIn's public guest endpoints: a search endpoint that returns job cards ten at a time, and a detail endpoint that returns the posting fragment for a single job ID. It warms a session against the LinkedIn homepage so guest cookies are issued, then pages through search results, collecting job IDs plus the exact posting date, location and company URL carried on each card. Each collected ID is then fetched individually and parsed into a row.

Requests go out directly by default; Apify Proxy is opt-in and escalates from datacenter to residential when a block is detected. Responses are decoded as UTF-8 before parsing, so accented characters and typographic dashes survive intact instead of arriving as mojibake. Only publicly visible data is read — no login, no cookie you supply, no logged-in surface.

Because parsing is anchored to the LinkedIn markup that has proven durable on the guest surface, and every value is normalized before it is written, the output schema stays the same across runs even as LinkedIn's visual layout shifts.

Integrations

It is an Apify Actor, so anything that can call the Apify API or read an Apify dataset can use it — your own code, a scheduler, or a no-code automation platform.

Calling the scraper programmatically

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("<YOUR_USERNAME>/linkedin-jobs-scraper-with-salary-range-filters").call(run_input={
"roleSearchTerms": "Senior Data Engineer",
"searchLocation": "United States",
"payFloorYearly": 150000,
"payCurrency": "USD",
"onlyJobsStatingPay": True,
"maxJobs": 100,
})
jobs = [j for j in client.dataset(run["defaultDatasetId"]).iterate_items()
if j.get("errorReason") is None]
for job in jobs:
print(job["companyName"], job["title"], job["salaryYearlyMin"], job["salaryYearlyMax"])

Works in Go, Ruby, Node.js, cURL — any language that can make an HTTP request. Filtering on errorReason is None separates real postings from uncharged accounting rows.

No-code tools (n8n, Make, LangChain)

In n8n, use the HTTP Request node pointed at the Actor's run endpoint, then a second request to the dataset items endpoint to pull the rows; the pay columns arrive as numbers, so a Filter node can compare them without a conversion step. In Make, the Apify modules run the Actor and iterate the dataset straight into a Google Sheet, an Airtable base or a CRM. In LangChain, wrap the same run-and-fetch call as a tool and hand the agent typed rows — description is the text to embed, salaryYearlyMin and salaryYearlyMax the metadata to filter on.

Apify schedules can run the Actor on a cron expression and fire a webhook when it finishes, which is the usual pattern for a daily pay-transparency delta.

Scraping publicly accessible job postings is broadly treated as permissible where no authentication is bypassed, and this Actor collects only what a signed-out visitor sees — it uses no account, cookie or session.

Almost every field in a row is a business record rather than personal data: a job title, an employer, a salary range and an industry describe a company's hiring activity, not an individual. GDPR does not attach to them the way it attaches to a profile scraper's output. What does apply is contractual and database-rights exposure — LinkedIn's Terms of Service govern automated access regardless of whether the data is personal, and republishing a large extracted collection is a different question from analysing it internally.

Two fields are the exception. posterFullName and posterProfileUrl identify a named individual, so storing them for EU or UK data subjects is processing personal data and needs a lawful basis under GDPR — typically documented legitimate interest for B2B recruitment — and falls within CCPA's scope for California residents. Nothing else in the row carries a personal identifier. Consult legal counsel if your use case involves bulk storage of personal data.

❓ Frequently asked questions

What LinkedIn job fields does this scraper return?

34 fields per posting. The most used are title, companyName, salaryYearlyMin and salaryYearlyMax, publishedAt and description, with ten compensation fields in total covering the stated figures, the currency, the period and whether that period was stated or inferred. The full list is in the data fields table above.

No. No login, no cookie, no session token, no LinkedIn credential of any kind. The Actor reads LinkedIn's public guest job surfaces anonymously. The only credential involved is your Apify token.

Where does the salary data come from, if LinkedIn does not publish one?

From the job description's free text. LinkedIn's public job pages carry no structured pay field — measured 0 of 48 search cards and 0 of 30 detail fragments — so every figure is parsed out of prose with compensation-context anchoring, and salarySource always says description when a figure was found. A null pay column means the employer did not state pay, not that the scrape failed.

Does a minimum salary filter drop jobs that state no pay?

No. payFloorYearly and payCurrency screen only jobs that actually stated a figure — a job with hasSalary: false passes both, because a floor cannot judge a number that does not exist. Only onlyJobsStatingPay rejects on absence, and it is off by default. That is deliberate: silently deleting every posting that did not publish a range would remove a large part of the market from your dataset without telling you.

What happens if no jobs pass the pay filter?

The dataset comes back with no saved rows, and the run log states exactly what happened: how many candidates were scanned, how many were screened out, the match rate, and a note that only N of the M requested jobs matched within the candidates available for that search. None of the screened-out jobs are charged. The usual fixes are lowering payFloorYearly, turning onlyJobsStatingPay off, widening payCurrency to any currency, or broadening the role and location — a narrow title in a small market may simply not contain enough postings that quote pay.

Are hourly rates comparable to annual salaries?

Yes for the period, no for the currency. Hourly, daily, weekly and monthly rates are normalized to salaryYearlyMin / salaryYearlyMax using fixed assumptions — 2,080 hours, 260 days, 52 weeks or 12 months per year — and salaryPeriodSource tells you whether the period was stated by the employer or inferred from magnitude. Currency is detected but never converted, so a floor set in one currency is compared as a raw number against figures in another. Set payCurrency whenever your search crosses currencies.

How many jobs can I extract in one run?

maxJobs accepts 1 to 1,000, and 1,000 is LinkedIn's ceiling rather than an Actor limit — the public endpoint returns HTTP 400 past start=1000, so a single role-and-location search cannot paginate further. To go wider, split across geoId values, employers or publishedAt windows and merge on id.

Can I scrape multiple employers at once?

Yes. companyInput is a list and accepts company names, LinkedIn company URLs and numeric company IDs in the same array. The Actor resolves each to a company ID, searches per employer, and deduplicates job IDs across the whole run so the same posting is never saved twice. If a company page cannot be resolved, that employer falls back to a keyword search on its name.

Which filters does LinkedIn actually honour?

keywords, location, geoId, companyInput, publishedAt, workType, contractType, experienceLevel and useLinkedInSalaryBucket all change the result set LinkedIn returns. sortOrder does not — it is accepted for compatibility and logs a notice. payFloorYearly, payCurrency and onlyJobsStatingPay are the Actor's own screens, applied after each job is fetched and parsed.

Does it work with Claude, ChatGPT and other AI agent tools?

Yes. It is callable as a standard HTTP endpoint through the Apify API, so LangChain, CrewAI, n8n or a hand-written tool definition can invoke it and receive typed JSON. No parsing step is needed before a row enters an LLM context window.

How does this compare to other LinkedIn job scrapers?

Checked on the Apify Store on 25 July 2026. khadinakbar/linkedin-jobs-scraper returns salary_min and salary_max as parsed numbers plus a skills array, and offers a salaryBase filter with values from 40000 to 120000; its listing states that salary data appears only where the employer added it. afanasenko/linkedin-jobs-scraper runs three operation modes and offers a filterSalaryMin filter, documenting that its filters apply after the upstream fetch and that "the paid event still fires on the raw fetch — filters narrow your output, not your bill." mabdulmoghni/linkedin-jobs-scraper---customized-filters-with-notifications documents five output fields — title, company, location, posted date and link — with no salary field, plus optional Telegram alerts. The observable differences here are that pay is decomposed into currency and period with a salaryPeriodSource flag separating stated from inferred, that the yearly normalization is its own filterable column, and that a row removed by a pay filter produces no charge.

What happens when LinkedIn changes its layout or anti-bot system?

The Actor is maintained and the output schema stays stable — field names and types do not change when LinkedIn's markup does. Parsing is anchored to the elements that have proven durable on the guest surface, and a value that cannot be read comes back null rather than as a corrupted guess.

Which fields work best for AI training data and RAG indexing?

For RAG, index description for the full text, with title, companyName and sector as the high-signal short fields, and descriptionHtml when the structure inside the posting matters. For training data and analytics, salaryYearlyMin, salaryYearlyMax, publishedAt, experienceLevel and applicantsCountType are the most consistently structured across records. Every field returns as a typed primitive — string, number, boolean or null — so no post-processing is needed before indexing.

ScraperWhat it extracts
LinkedIn Post ScraperPublic LinkedIn posts with engagement counts and post text
LinkedIn Post Comments ScraperComments on a LinkedIn post, with commenter identity
LinkedIn Company URL - Mass Profile Finder With FirmographicsCompany records and firmographics from LinkedIn company URLs
LinkedIn Profile Phone Number Scraper By Seniority & Title FilterPublic profile contact data filtered by seniority and job title
LinkedIn Phone Number Scraper: Lead ScoringPublic profile contact data with lead-quality scoring

💬 Your feedback

Found a bug, or a pay format the parser missed? Open an issue on the Actor's Issues tab. Reports that include the exact input JSON and a job URL are the fastest to reproduce and fix — a description whose salary wording was misread is the single most useful thing you can send.