LinkedIn Jobs Scraper by URL — Listings, Salaries (No Login)
Pricing
from $1.00 / 1,000 result scrapeds
LinkedIn Jobs Scraper by URL — Listings, Salaries (No Login)
Scrape LinkedIn job listings by search URL — extract job title, company, salary, location, skills and apply links. No login, no API key, no rate limits. Bypass anti-bot detection. CSV/JSON output. Pay per result. Proxycurl alternative for job board scraping and recruiter lead generation.
Pricing
from $1.00 / 1,000 result scrapeds
Rating
5.0
(3)
Developer
Web Data Labs
Maintained by CommunityActor stats
10
Bookmarked
781
Total users
29
Monthly active users
5.3 hours
Issues response
2 days ago
Last modified
Categories
Share
LinkedIn Jobs Scraper — Public Job Listings (No Login Required)
Free plan? Apify gives every user free monthly platform credits — enough to scrape plenty of job listings at this actor's per-result price. No credit card needed.
Scrape LinkedIn job listings at scale without login credentials or authentication. This actor uses LinkedIn's public guest API to search and extract job postings, returning structured data including job title, company, location, description, apply type, and apply links when LinkedIn exposes them.
No login required | Full job details in search mode | Pay per result
Why Use This Actor?
LinkedIn is the world's largest professional job board with millions of active listings. But LinkedIn's interface makes it difficult to:
- Export job data in bulk
- Compare salaries across roles and locations
- Track job market trends over time
- Build pipelines of job leads for recruiting
- Feed job data into analytics dashboards
This actor solves all of that. You get clean, structured JSON data from LinkedIn's public job listings — no login, no cookies, no accounts to manage.
Key Use Cases
🔍 Job Market Research
Track hiring trends across industries. Monitor which skills are in demand, which companies are hiring, and how salaries change by region. Perfect for HR consultants, workforce analysts, and career coaches who need data-driven insights.
📊 Recruitment Analytics
Build dashboards showing competitor hiring activity. Know when a competitor posts 50 new engineering roles — that's a signal. Recruiting firms use this to identify clients who are actively scaling.
💼 Lead Generation
Every job posting is a buying signal. A company posting for a "DevOps Engineer" probably needs DevOps tools. A company hiring "SDRs" is scaling sales. Feed job listings into your CRM to identify warm leads based on hiring activity.
🤖 Automated Job Alerts
Set up scheduled runs to monitor specific keywords and locations. Get notified when new roles matching your criteria appear. Build custom job boards or feeds for niche audiences.
📈 Salary Benchmarking
Collect salary data across roles, locations, and experience levels. Build compensation reports for clients or internal planning. Compare your offers against market rates.
🎓 Academic Research
Study labor market dynamics, remote work trends, skill demand evolution, or geographic hiring patterns. Export to CSV/Excel for use in research papers and reports.
How It Works
The actor operates in two modes:
Mode 1: Search Jobs
Search LinkedIn's public job listings by keyword and location. Returns a list of matching jobs — by default with the same full detail fields as job mode (description, applicants, seniority, employment type, job function, industries, apply info). Detail fetching is time-budgeted: rows still waiting for their detail page when the run's remaining time runs out come back with the 6 summary fields at the same per-row price — a longer run timeout yields more detailed rows. Set fetchDetails: false for the lighter 6-field summary rows.
Mode 2: Get Job Details
Fetch the full details of a specific job posting by URL or job ID. Returns the complete job description, requirements, and metadata.
Input Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
action | string | Yes | search | "search" to find jobs by keywords, "job" to get full details of a specific job |
keywords | string | No | "python developer" | Search terms (e.g., "data engineer", "marketing manager remote") |
location | string | No | "" | Location filter (e.g., "United States", "London", "Berlin") |
maxItems | integer | No | 5 | Upper bound on results — batch accepts up to 1,000, the HTTP API interface caps at 100 per request. A ceiling, not a guarantee: LinkedIn's public search stops paginating well before 1,000 for most queries |
fetchDetails | boolean | No | true | Fetch the full posting for every search result — slower (one extra page per job), the price per row is the same. Time-budgeted: rows that miss the run's remaining time fall back to the 6 summary fields at the same price. false returns only the 6-field summary rows |
datePosted | string | No | "any" | Posting-age filter for search: "any", "past24h", "pastWeek", "pastMonth" |
companyIds | array | No | — | Restrict results to these LinkedIn company IDs — numeric strings like "1068" (JPMorganChase), up to 10. Server-side filter: it rides the search URL, rows keep the 6 summary fields |
easyApply | boolean | No | false | LinkedIn's own Easy Apply filter — jobs you apply to on LinkedIn itself. Server-side, and when details are on each row is also checked: clearly-external applies are dropped, undetectable rows are kept |
under10Applicants | boolean | No | false | LinkedIn's own Under 10 applicants filter — early postings only. It is LinkedIn's filter, not a recount: the guest page can show at most "first 25 applicants", so rows are only dropped when a numeric count over 10 is visible; unknown counts are kept |
experienceLevels | array | No | — | Keep only these seniority levels: Internship, Entry level, Associate, Mid-Senior level, Director, Executive. Client-side filter — forces detail fetching and scans extra listings until maxItems match (bounded — see Filtering) |
jobTypes | array | No | — | Keep only these employment types: Full-time, Part-time, Contract, Temporary, Volunteer, Internship, Other. Client-side filter — same scanning behavior as experienceLevels; combined, both must match |
url | string | No | "" | LinkedIn job URL or job ID, used with action: "job" |
Input Examples
Search for remote Python jobs in the US (25 results):
{"action": "search","keywords": "python developer remote","location": "United States","maxItems": 25}
Search for marketing roles in London:
{"action": "search","keywords": "marketing manager","location": "London","maxItems": 50}
Get full details of a specific job:
{"action": "job","url": "https://www.linkedin.com/jobs/view/1234567890"}
Broad search for data engineering jobs (highest allowed ceiling):
{"action": "search","keywords": "data engineer","maxItems": 1000}
maxItems is an upper bound, not a quota — LinkedIn's public job search stops paginating well before 1,000 for most queries, so a large request usually returns fewer rows. When that happens the run's status message says so ("LinkedIn search ended after N results").
Jobs posted in the last 24 hours, summary rows only:
{"action": "search","keywords": "registered nurse","location": "Chicago","datePosted": "past24h","fetchDetails": false}
Full-time mid-or-senior engineering roles at specific companies, Easy Apply only:
{"action": "search","keywords": "software engineer","location": "New York","maxItems": 20,"companyIds": ["1068"],"easyApply": true,"experienceLevels": ["Mid-Senior level", "Associate"],"jobTypes": ["Full-time"]}
Filtering
Two kinds of filters, and they cost differently:
- Server-side —
companyIds,easyApply,under10Applicants,datePosted,location,keywords. These ride the search URL, so LinkedIn does the filtering and the scan costs nothing extra. - Client-side —
experienceLevels,jobTypes, pluseasyApply/under10Applicantsverification when details are on. LinkedIn only exposes seniority and employment type on each job's detail page, so these filters fetch and check each listing untilmaxItemsrows match — they force detail fetching on and scan extra listings.
What that means in practice:
- Recall is bounded, not exhaustive. A client-side filter keeps scanning until
maxItemsmatch, the scan cap is hit (up to 10×maxItemsin batch, 3×maxItemscapped at 50 on the HTTP interface), or the listings stop matching fast enough to keep going — whichever comes first. Most LinkedIn jobs mark seniorityNot Applicable, so a narrowexperienceLevelschoice can scan many jobs before matching — and may still return fewer rows than asked, or the paid zero-match notice. - A zero-match filtered scan delivers one notice row — "0 of N scanned jobs matched your filters" — charged as 1 result, exactly like a clean empty search. If every detail page failed instead, the run fails and nothing is charged.
- Only matching rows are delivered and charged. Rows the filter drops are never pushed and never billed.
- Degraded rows don't count as matches. A listing whose detail page couldn't be fetched has unknown seniority/type and is skipped by client-side filters — it is neither delivered nor charged.
Sample Output
Each job listing returns a structured JSON object. Here's what a typical result looks like:
{"jobId": "3876543210","title": "Senior Python Developer","company": "TechCorp Inc.","location": "San Francisco, CA (Remote)","postedDate": "2 weeks ago","applicants": "127 applicants","applicantsCount": 127,"isEarlyApplicant": false,"description": "We're looking for a Senior Python Developer to join our platform team...","seniority": "Mid-Senior level","employmentType": "Full-time","jobFunction": "","industries": "Software Development","url": "https://www.linkedin.com/jobs/view/3876543210","applyType": "EXTERNAL","applyUrl": null,"salary": "$137,750.00/yr - $185,000.00/yr","salaryMin": 137750.0,"salaryMax": 185000.0,"salaryCurrency": "USD","salaryPeriod": "year","salaryProvider": "TechCorp Inc. provided pay range","companyUrl": "https://www.linkedin.com/company/techcorp","companyLogoUrl": "https://media.licdn.com/dms/image/.../techcorp_logo","companyId": "1068"}
Fields returned (availability depends on what LinkedIn provides for each listing):
Search results (action: "search") return the full field set by default — fetchDetails fetches each posting's detail page, within a time budget derived from the run's timeout. With fetchDetails: false — or whenever one detail page cannot be fetched, or whenever the detail budget runs out before that row's turn — the row falls back to the 6 summary fields at the same price:
jobId,title,company,location,postedDate(ISO date),url
Job details (action: "job") and detail-enriched search rows additionally return:
applicants/applicantsCount/isEarlyApplicant— applicant caption and parsed countdescription— full job description textseniority,employmentType,jobFunction,industries— job criteria.jobFunctionis no longer sent on LinkedIn's public guest pages — the key is kept for compatibility but arrives empty on current listings.applyType—"EASY_APPLY","EXTERNAL"(offsite company ATS), ornullwhen the guest page shows no apply marker LinkedIn can be classified fromapplyUrl— usuallynull. LinkedIn does not expose the external apply URL on the public guest surface without login; this field is filled only when the guest HTML still includes it (opportunistic). Do not rely on it being present.salary— the raw salary text exactly as LinkedIn shows it, e.g."$137,750.00/yr - $185,000.00/yr";nullwhen the employer doesn't disclose paysalaryMin/salaryMax— parsed numeric bounds ("From $X"→ min only,"Up to $X"→ max only, a single value → both)salaryCurrency—USD,CAD,AUD,NZD,GBP,EUR,INRor the ISO code LinkedIn usedsalaryPeriod—"year","month"or"hour". An unrecognised period keepssalarytext but leaves the numbersnull— bounds are never inventedsalaryProvider— who published the range, e.g."Employer-provided"companyUrl— the company's LinkedIn page (tracking parameters stripped);nullwhen LinkedIn doesn't link itcompanyLogoUrl— the employer's logo image URL;nullwhen absentcompanyId— the company's numeric LinkedIn ID — feed it straight back intocompanyIds;nullwhen LinkedIn doesn't expose one
Code Examples
Python — Search for Jobs and Save to CSV
from apify_client import ApifyClientimport csvclient = ApifyClient("YOUR_APIFY_API_TOKEN")run_input = {"action": "search","keywords": "data scientist","location": "New York","maxItems": 50,}run = client.actor("cryptosignals/linkedin-jobs-scraper").call(run_input=run_input)# Save results to CSVitems = list(client.dataset(run["defaultDatasetId"]).iterate_items())with open("linkedin_jobs.csv", "w", newline="", encoding="utf-8") as f:if items:writer = csv.DictWriter(f, fieldnames=items[0].keys())writer.writeheader()writer.writerows(items)print(f"Saved {len(items)} jobs to linkedin_jobs.csv")
Python — Monitor New Jobs Daily
from apify_client import ApifyClientimport jsonclient = ApifyClient("YOUR_APIFY_API_TOKEN")# Search for recently posted jobsrun_input = {"action": "search","keywords": "machine learning engineer","location": "Remote","maxItems": 100,}run = client.actor("cryptosignals/linkedin-jobs-scraper").call(run_input=run_input)items = list(client.dataset(run["defaultDatasetId"]).iterate_items())# Filter for jobs with salary infojobs_with_salary = [job for job in items if job.get("salary")]print(f"Found {len(jobs_with_salary)} jobs with salary data out of {len(items)} total")for job in jobs_with_salary[:5]:print(f" {job['title']} at {job['company']} — {job['salary']}")
Python — Get Full Details of a Specific Job
from apify_client import ApifyClientclient = ApifyClient("YOUR_APIFY_API_TOKEN")run_input = {"action": "job","url": "https://www.linkedin.com/jobs/view/3876543210",}run = client.actor("cryptosignals/linkedin-jobs-scraper").call(run_input=run_input)items = list(client.dataset(run["defaultDatasetId"]).iterate_items())if items:job = items[0]print(f"Title: {job['title']}")print(f"Company: {job['company']}")print(f"Description:\n{job.get('description', 'N/A')}")
Node.js — Search and Process Results
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_APIFY_API_TOKEN' });const input = {action: 'search',keywords: 'frontend developer react',location: 'United States',maxItems: 50,};const run = await client.actor('cryptosignals/linkedin-jobs-scraper').call(input);const { items } = await client.dataset(run.defaultDatasetId).listItems();console.log(`Found ${items.length} jobs`);// Group by companyconst byCompany = {};for (const job of items) {const company = job.company || 'Unknown';byCompany[company] = (byCompany[company] || 0) + 1;}console.log('\nTop hiring companies:');Object.entries(byCompany).sort((a, b) => b[1] - a[1]).slice(0, 10).forEach(([company, count]) => {console.log(` ${company}: ${count} open positions`);});
Node.js — Get Job Details
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: 'YOUR_APIFY_API_TOKEN' });const input = {action: 'job',url: 'https://www.linkedin.com/jobs/view/3876543210',};const run = await client.actor('cryptosignals/linkedin-jobs-scraper').call(input);const { items } = await client.dataset(run.defaultDatasetId).listItems();if (items.length > 0) {const job = items[0];console.log(`${job.title} at ${job.company}`);console.log(`Location: ${job.location}`);console.log(`Salary: ${job.salary || 'Not disclosed'}`);console.log(`Apply: ${job.applyUrl}`);}
Using the Apify API Directly (cURL)
# Start a runcurl -X POST \"https://api.apify.com/v2/acts/cryptosignals~linkedin-jobs-scraper/runs?token=YOUR_TOKEN" \-H "Content-Type: application/json" \-d '{"action": "search","keywords": "product manager","location": "Berlin","maxItems": 25}'# Fetch results (use the defaultDatasetId from the run response)curl "https://api.apify.com/v2/datasets/DATASET_ID/items?format=json&token=YOUR_TOKEN"
Call as an API (instant HTTP interface)
This actor also exposes a request/response HTTP interface — results come back in the response body, no run polling and no dataset step. Same input parameters, same per-result pricing.
GET/POST https://cryptosignals--linkedin-jobs-scraper.apify.actor
# Search jobs — GET with query paramscurl "https://cryptosignals--linkedin-jobs-scraper.apify.actor/?action=search&keywords=devops&location=Remote&maxItems=5" \-H "Authorization: Bearer YOUR_APIFY_TOKEN"# Or POST with a JSON bodycurl -X POST "https://cryptosignals--linkedin-jobs-scraper.apify.actor/" \-H "Authorization: Bearer YOUR_APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"action":"search","keywords":"data engineer","maxItems":5}'# Get one job postingcurl "https://cryptosignals--linkedin-jobs-scraper.apify.actor/?action=job&url=https://www.linkedin.com/jobs/view/1234567890" \-H "Authorization: Bearer YOUR_APIFY_TOKEN"
Response:
{"items": [ { "jobId": "…", "title": "…", "company": "…", "location": "…", "url": "…" } ],"count": 5}
Notes:
- Authenticate with your Apify token in the
Authorization: Bearerheader (or?token=query param), exactly like the Apify API. - Cold start: if no server instance is warm, the first request may wait up to ~2 minutes while one starts. Warm requests answer in seconds.
proxyConfigurationis not supported on this interface — requests containing it are rejected with HTTP 400.maxItemsis capped at 100 per request on this interface — batch runs accept up to 1,000 as an upper bound (LinkedIn's public search ends earlier for most queries).fetchDetailsis opt-in here — it defaults to off on this interface, so an unchanged call keeps the fast 6-field rows; passfetchDetails=truefor full postings inline. Detail fetches share the request's time budget — rows that miss it still return the 6 summary fields, at the same per-row price. Client-side filters (experienceLevels,jobTypes) force it on.- All five filters work in the query string:
companyIds=1068,961661(comma-separated),easyApply=true,under10Applicants=true,experienceLevels=Director,Executive,jobTypes=Contract,Part-time— or as arrays in a JSON POST body. An out-of-domain enum or a non-numeric company id is rejected with HTTP 400 before any work. - A filtered scan on this interface scans at most 3×
maxItemslistings (hard-capped at 50) — a request can legitimately return fewer rows than asked. Rows already collected when the request deadline hits come back as a partial HTTP 200 with anotefield — never thrown away. - Common field aliases work too (
query→keywords,limit→maxItems,jobId→url). - You are charged the same per-result price, only for results actually delivered in the response.
Pricing
This actor uses Pay Per Event (PPE) pricing — you only pay for the results delivered. The current price per result is shown live on the actor's Apify Store page.
| Event | How it's billed |
|---|---|
| Result | One charge per job row delivered — including the single no-results notice row a clean empty search returns |
No platform fees. A search that matches nothing still performs the lookup, so it delivers one notice row explaining the empty result and is charged as 1 result.
Free-plan Apify users get monthly platform credits which can be used alongside PPE actor costs — plenty for testing, small projects, or personal job searches.
Tip: Start with
maxItems: 5to test your search parameters before scaling up — a small run costs only a few results.
Scheduling & Automation
Set up scheduled runs on Apify to automatically scrape new job listings on a daily or weekly basis:
- Go to the actor's page on Apify Console
- Click "Schedule" in the top menu
- Set your desired frequency (daily, every 6 hours, weekly, etc.)
- Configure your input parameters
- Results are automatically saved to your dataset
Combine with Apify Integrations to push results directly to:
- Google Sheets
- Slack notifications
- Webhooks (your own API)
- Zapier / Make.com
- Email alerts
Output Formats
Export your results in any format:
| Format | How |
|---|---|
| JSON | Default. Download from dataset or use API |
| CSV | Click "Export" → CSV in Apify Console |
| Excel | Click "Export" → Excel in Apify Console |
| API | Fetch programmatically via dataset API endpoint |
Frequently Asked Questions
Do I need a LinkedIn account?
No. This actor uses LinkedIn's public guest API. No login, no cookies, no account required. It only accesses publicly available job listings — the same ones visible to anyone browsing LinkedIn without logging in.
Is this against LinkedIn's terms of service?
This actor accesses only publicly available data through LinkedIn's public endpoints. It does not bypass any authentication or access private data. Public job listings are intentionally made available by employers to reach the widest audience.
How many jobs can I scrape per run?
maxItems sets the ceiling — up to 1,000 per run in batch, 100 per request on the HTTP standby/API interface. In practice the real limit is LinkedIn's public job search itself: it stops paginating well before 1,000 for most queries, so large requests usually return fewer rows — the run's status message reports when the search ended early. For larger volumes, set up multiple runs with different search parameters (different keywords, locations, or datePosted windows).
How fresh is the data?
The data is scraped in real-time from LinkedIn when you run the actor. You always get the latest listings. For monitoring new postings, schedule the actor to run daily or every few hours.
What if my search returns 0 results?
- Check your keyword spelling
- Try broader search terms
- Remove or change the location filter
- Narrow filters like
datePosted: "past24h"or a restrictiveexperienceLevels/jobTypeschoice legitimately hit this more often — fewer postings match within the bounded scan - LinkedIn may not have listings matching very niche queries
- A clean empty search — and a filtered scan that matched nothing — delivers one notice row and is charged as 1 result — the lookup itself is the work
Can I filter by remote/hybrid/on-site?
Yes — include terms like "remote" in your keywords. For example: "software engineer remote".
Can I get salary data?
Salary information is returned when the employer includes it in the listing. Not all job postings disclose salary. The actor returns whatever LinkedIn provides.
How is this different from other LinkedIn scrapers?
- No login required — many competitors require LinkedIn credentials (which risk account bans)
- Public API only — we don't scrape HTML or use headless browsers, so it's fast and reliable
- Simple pricing — pay per result, no hidden fees
- Full details in bulk — search fetches each posting's detail page by default, bounded by the run's time budget
- Two modes — search for jobs OR get full details of a specific listing
Can I use this for commercial purposes?
Yes. The data is publicly available. You can use it for analytics, lead generation, research, and building products. Always ensure your use case complies with applicable data protection laws in your jurisdiction.
What happens if LinkedIn changes their API?
We actively maintain this actor. If LinkedIn makes changes, we update the actor to ensure continued functionality.
Rate Limits & Best Practices
- Start small: Test with
maxItems: 5before scaling up - Be specific: Precise keywords return better results than broad ones
- Use location: Adding a location filter reduces noise and speeds up results
- Schedule smart: Daily runs catch most new postings; hourly is rarely needed
- Combine searches: Run multiple searches with different keywords to build comprehensive datasets
Support
Having issues or questions? Use the Issues tab on this actor's Apify page to report bugs or ask questions. We typically respond within 24 hours.
Built and maintained by cryptosignals.
Found this actor useful? Leave a review - it helps a lot. Something broken? Report it on the Issues tab.