LinkedIn Public Jobs Scraper (no login) | Salary & Level
Pricing
from $0.85 / 1,000 job extracteds
LinkedIn Public Jobs Scraper (no login) | Salary & Level
Search public guest LinkedIn job listings with no login or cookies: keywords, location, date, remote/hybrid, level, type, company. Title, company, salary, seniority, industries, full description. Deduped. Pay per job.
Pricing
from $0.85 / 1,000 job extracteds
Rating
0.0
(0)
Developer
Detroit Web Agent - Open Data
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 hours ago
Last modified
Categories
Share
LinkedIn Public Jobs Scraper (no login): salary, seniority, industries
Search LinkedIn's public job listings the same way a logged-out visitor sees them. No account, no cookies, no profiles. Each row is one job with company, location, posted date (ISO), salary when the posting shows one, seniority, employment type, industries, applicant count text, company details and the full description.
- In short: LinkedIn Public Jobs Scraper (Apify actor
transparent_meteorite/linkedin-public-jobs) reads only LinkedIn's public guest job pages (jobs-guestsearch and job-posting endpoints, plus the public company page) and returns clean, deduplicated job rows. No login, no cookies, no personal data. - Who it is for: recruiters and staffing teams, job boards and aggregators, sales teams using hiring as a buying signal, labour-market and salary researchers.
- Input: keywords, location or geoId, date posted (24h / week / month), workplace (on-site / remote / hybrid), experience level, job type, company filter, max jobs (1 to 1,000 per run).
- Output: 32 fields per job: jobId, title, company, companyUrl, location, postedAt, applicantsText, applicantsCount, salaryText/Min/Max/Currency/Period, seniority, employmentType, jobFunction, industries, workplaceType, applyType, applyUrl, jobUrl, isActivelyHiring, seven company fields, description (and optional HTML), scrapedAt.
- Extras: dedupe across pages, new-jobs-only mode with a named key-value store, retries with exponential backoff, Apify Proxy with direct fallback.
- Pricing: pay per event. $0.0017 per job on the Apify free plan, $0.00085 per job on paid plans, plus a start event of $0.00005 per GB of run memory. Duplicates and already-seen jobs are never charged.
- Limits: public guest pages only; about 1,000 results per query;
jobFunctionis always null; salary only when the posting publishes it.
Run in 30 seconds
- Open the prefilled run (free Apify account; it creates a saved task with a working input).
- Press Start. The input is small on purpose, so the first run is quick and costs a few cents at most.
- Open the Output tab, switch to Table view, download as CSV, Excel or JSON.
Prefilled input:
{"keywords": "software engineer","location": "United States","maxItems": 10}
Ready-made presets by use case (each opens as a saved task you can edit):
Why choose this over alternatives
- No login and no cookies: only the pages LinkedIn serves to logged-out visitors.
- Lower price per job on the free plan and on paid plans than the alternative we tested.
- Deduped across pages, with a new-jobs-only mode for schedules.
- On the 16 fields both tools return, fill rates were level in our test; see the table.
| This actor | Typical alternatives | |
|---|---|---|
| Rows returned for a 25-job request | 25 of 25 | 25 of 25 |
| Time for 25 jobs | 27 to 31 s | 81 s |
| Price per job, free plan | $0.0017 | $0.002 |
| Price per job, paid plans | $0.00085 | $0.001 |
| Fill rate on the 16 shared fields | 92.2% | 92.0% |
| Company logo present | 25 of 25 | not measured |
Measured 2026-10-11 (source: head-to-head test runs on the same query, "data analyst", United States, past week, 25 jobs each, one run per side, both runs succeeded). One run each is a small sample; your timings will vary with LinkedIn's rate limiting.
What it does and who it is for
You describe a job search (keywords, place, filters). The actor pages through LinkedIn's public guest search, opens the public posting page for each job, optionally reads the public company page once per company, and writes one flat row per job to an Apify dataset. You can download the dataset as JSON, CSV, Excel, XML or HTML, or read it through the API.
It is built for people who need job listings as data:
- Recruiters and staffing teams tracking which companies are hiring for a role in a region.
- Job boards and aggregators that need fresh listings with a stable id for upserts.
- Sales and research teams that treat hiring as a signal (a company posting ten data roles is investing in data).
- Salary and labour-market researchers who want the pay ranges that postings publish, parsed into numbers.
It is not a profile scraper. It never reads member profiles and never outputs recruiter or poster names.
Quick start
- Enter keywords (for example
data analyst) and a location. - Optionally narrow by date posted, remote/hybrid, level, job type or company.
- Set Max jobs and run. Download as JSON, CSV or Excel, or call the API.
{ "keywords": "data analyst", "location": "United States", "datePosted": "week", "workplace": ["remote"], "maxItems": 50 }
What you get: output fields
Every row has the same 31 keys (32 when you ask for description HTML). A field is null when LinkedIn does not publish it for that job.
| Field | Type | Meaning |
|---|---|---|
jobId | string | LinkedIn's job id. Stable, use it as your upsert key |
jobUrl | string | Clean public job URL, tracking parameters removed |
title, company, location | string | As shown on the public listing |
companyUrl | string | Public LinkedIn company page URL |
postedAt | string | ISO timestamp. LinkedIn publishes the date only, so the time part is midnight UTC |
applicantsText | string | Text exactly as shown, for example "Over 200 applicants". Null when not shown |
applicantsCount | number | The number parsed from applicantsText (200 for "Over 200 applicants"). Null when LinkedIn shows no count |
salaryText | string | The pay text as posted, for example $175,000.00/yr - $210,000.00/yr. Null when the posting publishes no pay |
salaryMin, salaryMax | number | Parsed from salaryText |
salaryCurrency, salaryPeriod | string | For example USD and year |
seniority, employmentType | string | The posting's own criteria, for example "Mid-Senior level", "Full-time". LinkedIn itself often shows "Not Applicable" for seniority |
jobFunction | null | Always null today: LinkedIn no longer shows job function to logged-out visitors. The key is kept so your columns stay stable |
industries | array | The posting's industries. Split from LinkedIn's sentence, so the last item can begin with "and" |
workplaceType | string | remote, hybrid or onsite. Exact when you filter to one workplace type; otherwise inferred from title, location and description when stated, else null |
applyType | string | linkedin (Easy Apply) or external |
applyUrl | string | The employer's application URL when the public page exposes it, otherwise the LinkedIn job URL |
isActivelyHiring | boolean | true when the listing carries LinkedIn's "actively hiring" badge, else null |
companyDescription, companyWebsite, companyIndustry | string | From the public company page (needs includeCompanyDetails, on by default) |
companySize, companyHeadquarters | string | For example "11-50 employees", "New York, New York" |
companyLogo | string | Logo image URL, with a fallback to the logo on the search card |
companySlogan | string | The company tagline. Null when the company publishes none |
companyAddress | object | {streetAddress, addressLocality, addressRegion, postalCode, addressCountry} from the company page's structured data. Passed through as published |
description | string | Plain-text job description. Null when includeDescription is off |
descriptionHtml | string | Only when includeDescriptionHtml is on |
scrapedAt | string | ISO timestamp of extraction |
Sample output
A real row from a test run on 2026-10-11 (build 0.1.9). The two long text fields and the logo URL are shortened with ...; nothing else is changed.
{"jobId": "4438505532","title": "Senior Data Analyst","company": "Confido","companyUrl": "https://www.linkedin.com/company/confidotech","location": "New York, NY","postedAt": "2026-10-03T00:00:00.000Z","applicantsText": "Over 200 applicants","salaryText": "$175,000.00/yr - $210,000.00/yr","salaryMin": 175000,"salaryMax": 210000,"salaryCurrency": "USD","salaryPeriod": "year","seniority": "Not Applicable","employmentType": "Full-time","jobFunction": null,"industries": ["Software Development"],"workplaceType": null,"applyType": "external","applyUrl": "https://www.linkedin.com/jobs/view/senior-data-analyst-at-confido-4438505532","jobUrl": "https://www.linkedin.com/jobs/view/senior-data-analyst-at-confido-4438505532","isActivelyHiring": null,"companyDescription": "Confido is the AI infrastructure powering CPG brands from deduction to production plan...","companyLogo": "https://media.licdn.com/dms/image/v2/D4E0BAQH_cCz9VXIQ0A/company-logo_200_200/...","companyWebsite": "https://www.confidotech.com","companyIndustry": "Software Development","companySize": "11-50 employees","companyHeadquarters": "New York, New York","companySlogan": "The AI Infrastructure for CPG Brands from Deduction to Production Plan","companyAddress": {"streetAddress": null,"addressLocality": "New York","addressRegion": "New York","postalCode": "10001","addressCountry": "US"},"description": "The brands on your kitchen shelf are run by people doing extraordinary work that nobody sees...","scrapedAt": "2026-10-11T05:09:51.264Z"}
Note what this row shows honestly: applyType is external but applyUrl is still the LinkedIn URL, because the public page did not expose the employer's link, and workplaceType is null because the search had no single workplace filter and the posting did not state one.
Input options
No field is required by the form, but an empty input runs the example search data analyst in United States.
| Input | Default | What it does |
|---|---|---|
keywords | none | Job title or skill, for example registered nurse |
location | United States when no geoId is given | City, region or country as you would type it on LinkedIn, for example Detroit, Michigan |
geoId | none | LinkedIn's exact location id, for example 103644278 for the United States. Use it with or instead of location when a place name is ambiguous |
datePosted | any | any, 24h, week or month |
workplace | all | Any of onsite, remote, hybrid |
experienceLevel | all | Any of internship, entry, associate, mid-senior, director, executive |
jobType | all | Any of full-time, part-time, contract, temporary, volunteer, internship, other |
companyFilter | any company | Numeric LinkedIn company ids are passed to LinkedIn's own filter. Company names or linkedin.com/company/<slug> URLs are matched against the company name on each listing |
searchUrls | none | Paste LinkedIn job search URLs (or single linkedin.com/jobs/view/... URLs). The URL's keywords, location, geoId, date, workplace, experience, job type, company, distance, Easy Apply and under-10-applicants filters are used; anything the URL does not set falls back to the fields here |
splitByLocation | none | List of locations. One search per location, results merged and deduplicated by job id, maxItems shared fairly between them |
distance | 0 (LinkedIn default) | Radius in miles around a city-level location |
easyApplyOnly | false | Only Easy Apply jobs |
under10Applicants | false | Only jobs with fewer than 10 applicants so far |
companyIds | none | Numeric LinkedIn company ids, filtered by LinkedIn itself |
minAnnualSalary | 0 (off) | Keep only jobs whose published pay (converted to a yearly figure) is at least this number. Jobs without published pay are dropped when set, and dropped jobs are not charged |
maxItems | 25 | Stop after this many jobs, 1 to 1,000 |
includeDescription | true | Plain-text description. Same price either way |
includeDescriptionHtml | false | Adds descriptionHtml with the original formatting |
includeCompanyDetails | true | Adds the company fields. One extra request per unique company, cached for the run, skipped automatically when LinkedIn walls company pages |
onlyNewSinceLastRun | false | Skip jobs delivered by earlier runs that share the same state store |
stateStoreName | linkedin-public-jobs-seen | Named key-value store that remembers delivered job ids |
proxyConfiguration | Apify Proxy | Falls back to a direct connection if the proxy keeps failing |
Filters & search URL
Set the filters on LinkedIn's jobs page, copy the address bar, and paste it into searchUrls. The actor reads the same filters from the URL and runs them through LinkedIn's public no-login endpoint. Example: https://www.linkedin.com/jobs/search/?keywords=registered%20nurse&location=Detroit&f_WT=2&f_TPR=r604800&f_AL=true&distance=25.
Checked against live LinkedIn responses on 2026-10-13: date posted, workplace type, experience level, job type, company id, distance, Easy Apply and under-10-applicants change the results. LinkedIn's public endpoint ignores its salary bucket (f_SB2) and sort (sortBy) parameters, so they are not sent. Use minAnnualSalary for a salary floor (applied by the actor) and read postedAt to sort recent-first yourself.
Input examples
Remote contract roles posted in the last 24 hours:
{ "keywords": "react developer", "location": "United Kingdom", "datePosted": "24h", "workplace": ["remote"], "jobType": ["contract"], "maxItems": 100 }
Entry and associate roles at named companies. Names are matched as text against the listing's company name, so a name filter can scan many listings to find few matches; listings that do not match are not delivered and not charged:
{ "keywords": "software engineer", "location": "United States", "experienceLevel": ["entry", "associate"], "companyFilter": ["Google", "https://www.linkedin.com/company/stripe"], "maxItems": 200 }
A saved search that returns only jobs it has not delivered before:
{ "keywords": "data analyst", "location": "Detroit, Michigan", "datePosted": "week", "onlyNewSinceLastRun": true, "stateStoreName": "detroit-analyst-jobs", "maxItems": 100 }
Pricing
Pay per event. There is no subscription and no rental fee for this actor.
| Event | Apify free plan | Paid plans (Bronze to Diamond) |
|---|---|---|
| Job extracted (one per job row delivered) | $0.0017 | $0.00085 |
| Actor start (one per GB of run memory, minimum one) | $0.00005 | $0.00005 |
The default run memory is 4 GB, so the start costs $0.0002 per run unless you lower the memory in the run options.
Worked examples at the default memory:
| Jobs delivered | Free plan | Paid plans |
|---|---|---|
| 25 (the form default) | 25 x $0.0017 + $0.0002 = $0.0427 | 25 x $0.00085 + $0.0002 = $0.0215 |
| 100 | $0.1702 | $0.0852 |
| 1,000 (the maximum for one run) | $1.7002 | $0.8502 |
| 10,000 (ten runs of 1,000) | $17.002 | $8.502 |
A daily schedule that finds 50 new jobs every day delivers 1,500 jobs in 30 days: 1,500 x $0.00085 + 30 x $0.0002 = $1.281 on a paid plan, or $2.556 on the free plan.
Apify's free plan includes $5 of monthly platform credit, which covers about 2,900 jobs with this actor at the free-plan price.
What is not charged: duplicates inside a run, jobs skipped by new-only mode, listings removed by a company-name filter, and jobs whose page has been taken down (HTTP 404 or 410). To cap spend, set Maximum cost per run in the run options; the actor checks the remaining budget before each job and stops cleanly when the limit is reached.
How to run it
In Apify Console
Open the actor, fill in the form and press Start. Rows appear in the Output tab while the run is going. The status line shows progress as delivered/requested jobs. When the run ends, use Export for JSON, CSV, Excel, XML or HTML.
From the API or a client library
Set APIFY_TOKEN to your token from Apify Console > Settings > API & Integrations.
curl -X POST "https://api.apify.com/v2/acts/transparent_meteorite~linkedin-public-jobs/runs?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"keywords":"data analyst","location":"United States","datePosted":"week","maxItems":25}'
Python (pip install apify-client)
import osfrom apify_client import ApifyClientclient = ApifyClient(os.environ["APIFY_TOKEN"])run_input = {"keywords": "data analyst", "location": "United States", "datePosted": "week", "maxItems": 25}run = client.actor("transparent_meteorite/linkedin-public-jobs").call(run_input=run_input)for job in client.dataset(run["defaultDatasetId"]).iterate_items():print(job["jobId"], job["title"], job["company"], job["salaryText"])
JavaScript / Node.js (npm install apify-client)
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('transparent_meteorite/linkedin-public-jobs').call({keywords: 'data analyst', location: 'United States', datePosted: 'week', maxItems: 25,});const { items } = await client.dataset(run.defaultDatasetId).listItems();console.log(items.length, 'jobs');
On a schedule
Create a schedule in Apify Console (Schedules > Create new), add this actor with your saved input, and pick a cron expression such as 0 7 * * * for 07:00 every day. Turn on onlyNewSinceLastRun so each run returns, and charges for, only the jobs you have not received yet.
With n8n, Make, Zapier or AI agents
Start the actor with the Apify API (POST /v2/acts/<actor>/runs) or the Apify client in Python or JavaScript, wait for the run, then read GET /v2/datasets/<id>/items. The same input works as an n8n, Make or Zapier Apify node, and any MCP-capable agent can call it as an Apify tool. Every row has a stable jobId for upserts.
Webhooks work as for any Apify actor: add one for the "run succeeded" event and your endpoint receives the run id and dataset id as soon as the data is ready.
New jobs only (monitor mode)
Turn on onlyNewSinceLastRun and keep the same stateStoreName between runs. The actor stores the id of every job it delivers in that named key-value store. On the next run, jobs already in the store are skipped before their page is fetched and are not charged.
- Use one store name per saved search, for example
detroit-analyst-jobsandremote-react-contracts. Two searches sharing a store would hide each other's overlaps. - Ids are kept for 90 days, then dropped.
- A job is marked as seen only after its row is pushed and charged, so an interrupted run does not lose jobs.
- The mode reports new jobs. It does not report jobs that were removed or edited.
Run summary record
Each run writes a record named OUTPUT to its key-value store. It tells you what happened without reading the log:
| Key | Meaning |
|---|---|
query | The search as the actor understood it |
pagesFetched, cardsSeen | Search pages read and listings seen |
duplicates, filteredOut, skippedAlreadySeen | Listings not delivered, and why |
detailFailures | Job pages that could not be read after retries |
delivered, chargeCalls | Rows written and events charged (these match) |
chargeLimitReached | True when your maximum cost stopped the run |
proxy, http | Proxy used, and request, retry, blocked and failure counts |
warnings | Plain-language notes, for example that company pages were walled |
Limits and honest caveats
- Public guest pages only. No login, no cookies, no private data. Anything LinkedIn shows only to signed-in members is out of reach, including job function, the exact applicant number and most external apply links.
- About 1,000 results per query. That is LinkedIn's own cap on the public search, and
maxItemsstops at 1,000 for the same reason. Split bigger searches by location, date window or job type. - Salary is there only when the posting publishes it. Many postings publish no pay, and those rows have null salary fields. The actor never estimates a salary.
- Company fields depend on the public company page. In a 30-job test run,
companyAddresswas filled on 27 rows andcompanySloganon 14; many companies publish no tagline. Some addresses carry LinkedIn's own placeholder text instreetAddress(for example "Headquarters"); it is passed through as published. If LinkedIn walls company pages for the run's IP, the company fields are skipped and a warning is written toOUTPUT. - A job whose detail page cannot be read is still delivered with the search-card fields (title, company, location, posted date, URL) and null detail fields, and it is charged.
OUTPUT.detailFailurescounts these. Jobs that return 404 or 410 are dropped and not charged. - Rate limiting. LinkedIn limits anonymous traffic. The actor retries each request up to 4 times with exponential backoff and rotates through the proxy. If LinkedIn keeps refusing, the run ends with the rows it has and a warning in
OUTPUT; it never invents rows. - Order. Rows come in LinkedIn's own result order for the query, not strictly newest first.
FAQ
Do I need a LinkedIn account or cookies? No. The actor has no login step and sends no cookies.
Why is jobFunction always null? LinkedIn removed job function from the pages it serves to logged-out visitors. The column stays so that existing exports keep their shape.
Why does applyUrl equal the LinkedIn URL when applyType is external? The public page tells us the application happens on the employer's site but rarely exposes the link itself. applyType tells you which case you have.
Can I get more than 1,000 jobs? Yes, across several runs. Split the search by location (states, cities), by datePosted or by jobType, and merge on jobId.
How fast is it? A 25-job run took 27 to 31 seconds in our test on 2026-10-11, and a 30-job run with company details took 72 seconds. Speed depends on how hard LinkedIn is rate limiting at that moment.
Does it return recruiter or poster names? No. That is personal data and the actor does not collect it.
How do I search one country precisely? Use geoId. Free-text locations are interpreted by LinkedIn and can match a city of the same name elsewhere.
Is the description complete? Yes, the full public text. Turn on includeDescriptionHtml if you need the original formatting.
Troubleshooting
| Symptom | Likely cause and fix |
|---|---|
| Zero rows and a warning that LinkedIn kept refusing requests | Rate limit on the run's IP. Run again later, or choose a different proxy group in the Proxy field |
Fewer rows than maxItems | The search has fewer public results, a company-name filter removed listings (OUTPUT.filteredOut), new-only mode skipped them (OUTPUT.skippedAlreadySeen), or your maximum cost was reached |
| Company fields are all null | Company pages were walled for this run (see OUTPUT.warnings) or includeCompanyDetails is off |
| New-only run returns everything again | The stateStoreName changed, or the store was deleted. Keep the name identical between runs |
| Jobs from the wrong place | Ambiguous place name. Use geoId |
Terms & responsible use
This actor requests only pages LinkedIn serves publicly to logged-out visitors. It does not log in, use cookies, bypass authentication or collect personal profiles. LinkedIn's User Agreement restricts automated access and scraping, and rules differ by country and by what you do with the data. You are responsible for how you use the output: respect LinkedIn's terms and applicable law (including privacy law), keep volumes modest, do not republish descriptions wholesale, and do not use the data to contact individuals. This is not legal advice. This actor is not affiliated with or endorsed by LinkedIn.
More resources
- Guide & examples: mamoo85.github.io/open-data-actors/linkedin-public-jobs.html
Works well with
Other actors from Detroit Web Agent - Open Data that pair with this one:
- Career Site Jobs API (Greenhouse, Lever, Ashby and more): cover employers that post on Greenhouse, Lever, Ashby and other career sites.
- Workday Jobs Scraper: cover employers that post on Workday career sites.
Changelog
- 2026-10-09: First version (private): public guest search and job pages, salary parsing, dedupe, new-jobs-only mode.
- 2026-10-11: Company logo falls back to the search-card logo when the company page is walled (build 0.1.8).
- 2026-10-11: Added
companySloganandcompanyAddressfrom the public company page's structured data (build 0.1.9). - 2026-10-12: README expanded: full field table, sample output, pricing examples, API and scheduling guides, limits, FAQ and troubleshooting.
- 2026-10-13: Added a Run in 30 seconds section with a one-click prefilled run link, saved use-case presets and cross-links to pairing actors.
- 2026-10-13: Added
searchUrls(paste a LinkedIn search or job URL),splitByLocation,distance,easyApplyOnly,under10Applicants,companyIds,minAnnualSalaryand the numericapplicantsCountfield.
Support
Found a job that should have been returned, a field that parsed wrongly, or need a field added? Open an issue on this actor's Issues tab with the run URL and what you expected. The run URL lets us see the exact input and the OUTPUT record.
Need more than the data?
This actor is built by Detroit Web Agent, a small Detroit studio that builds websites, lead pages and simple data tools for small businesses. If you want this data wired into a dashboard or an alert, get in touch.
Built and maintained by Detroit Web Agent - Open Data (Detroit, Michigan). Questions, bugs or a field you need: open an issue on this Actor's Issues tab and we will reply there.
Use it from AI assistants (MCP)
LinkedIn Public Jobs Scraper works as a tool for AI assistants through the official Apify MCP server. Add this server URL to any MCP client (Claude Desktop, Claude Code, Cursor, ChatGPT connectors, VS Code):
https://mcp.apify.com/?actors=transparent_meteorite/linkedin-public-jobs
{"mcpServers": {"apify": {"url": "https://mcp.apify.com/?actors=transparent_meteorite/linkedin-public-jobs","headers": { "Authorization": "Bearer <YOUR_APIFY_TOKEN>" }}}}