LinkedIn Job Scraper API - Salary, Skills & Full Descriptions
Pricing
from $1.00 / 1,000 results
LinkedIn Job Scraper API - Salary, Skills & Full Descriptions
LinkedIn job scraper tool that searches by keyword and location and returns every posting with the full description, salary, skills, and applicant count. No login or cookies.
Pricing
from $1.00 / 1,000 results
Rating
0.0
(0)
Developer
Thodor
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
What does LinkedIn Job Scraper API do?
LinkedIn job scraper tool that searches by keyword and location and returns every matching posting with the full description, salary, skills, seniority, and applicant count on each row. Search python developer in Amsterdam, Netherlands and get the jobs back as structured JSON. No login, no cookies, no LinkedIn account.
Real output from a run on python developer and data engineer in Amsterdam:
| Job title | Company | Location | Posted | Seniority | Applicants |
|---|---|---|---|---|---|
| Python Software Engineer | IMC Trading | Amsterdam, North Holland, Netherlands | 2026-07-26 | Mid-Senior level | 200+ |
| Backend Engineer (Python / FastAPI / SQL) | MarvelX AI | Amsterdam, North Holland, Netherlands | 2026-07-17 | Associate | 200+ |
| Python Developer | MN | The Hague, South Holland, Netherlands | 2026-07-29 | Mid-Senior level | 98 |
| Data Engineer | Samsung Electronics | Schiphol-Rijk, North Holland, Netherlands | 2026-07-22 | Associate | 200+ |
| Data Engineer | CIMSOLUTIONS | North Holland, Netherlands | 2026-07-28 | Associate | 25 |
That run took 20 seconds for 10 jobs across two keywords. Every row also carries the full job description as HTML and as plain text, the employment type, posting and expiry dates, required skills, education level, the hiring organization block, and the original LinkedIn URL.
Why use LinkedIn Job Scraper API?
Most LinkedIn job scrapers give you what the search results page shows: title, company, location, date. This one opens every job and returns what is actually inside it.
- Full descriptions, in HTML and plain text. The description is where the tech stack, the team, the salary range, and the reporting line live.
description_textis ready to feed to an LLM without stripping tags first. - Remote and seniority on every row.
is_remoteandsenioritycome from each job's own page, so they are accurate even though LinkedIn's public search ignores its own filters for both. Filter on them in your spreadsheet or query. - Applicant counts as a competition signal. A three-day-old posting with 12 applicants is a different opportunity than one with 200.
- Expiry dates.
valid_throughtells you when a posting closes, which no search-results scraper can give you. - Company details on request. Switch on Add company details and every job carries the employer's industry, exact headcount, follower count, HQ, and founded year. Each company is fetched once per run however many of its jobs you get, and it is the one option billed on top of the jobs.
A worked example: run data engineer in Amsterdam, Netherlands on a daily schedule with Posted within set to Past 24 hours, diff the job_id column against yesterday, and push the new rows to Slack. That is a live feed of every company staffing a data team in your market, with the tech stack from each description and the applicant count telling you how contested each one is. Swap the diff for a group-by on company and the same feed becomes a hiring-intent list for sales.
Salary research works the same way: base_salary is populated when the employer publishes structured pay, and when it is not, the range is usually written into description_text, which is why the plain-text field is there.
How to use LinkedIn Job Scraper API
- Open the Actor and type your search terms into Search keywords, one per line. Each term runs as its own search.
- Set Location the way you would type it on LinkedIn:
Berlin, Germany,London, United Kingdom,United States. Leave it empty to search worldwide. - Click Start. Jobs stream into the dataset as they are found, so the Output tab fills up before the run finishes.
- Export as JSON, CSV, or Excel, or pull the dataset over the Apify API.
Already built the search on LinkedIn? Paste the URL into Search URLs instead and the keywords and location are read from it.
Running a list of terms from a spreadsheet? The Console's Bulk edit tab on the Search keywords field takes one term per line, so a column pastes straight in.
Use it as a LinkedIn jobs API
Every run is an HTTP endpoint. Call the Actor synchronously and get the jobs back in the response body:
curl -X POST "https://api.apify.com/v2/acts/thodor~linkedin-job-scraper-api/run-sync-get-dataset-items?token=<APIFY_TOKEN>" \-H 'Content-Type: application/json' \-d '{"keywords": ["python developer"], "location": "Amsterdam, Netherlands"}'
The same call from Python:
from apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")run = client.actor("thodor/linkedin-job-scraper-api").call(run_input={"keywords": ["python developer"],"location": "Amsterdam, Netherlands","maxJobsPerSearch": 0,})for job in client.dataset(run["defaultDatasetId"]).iterate_items():print(job["title"], job["company"], job["location"])
Run it on a schedule
Set Posted within to Past 24 hours and put the Actor on a daily Apify Schedule. Each run then returns only what appeared since the last one, so the dataset becomes a feed of new postings rather than a full re-scrape. Add a webhook and new jobs land in Slack, Airtable, or your CRM the morning they go live. The n8n and Make integrations take the same trigger.
Input
{"keywords": ["python developer", "data engineer"],"location": "Amsterdam, Netherlands","postedWithin": "30d","maxJobsPerSearch": 25,"scrapeDetails": true}
| Field | Type | Default | What it does |
|---|---|---|---|
keywords | array | python developer | Search terms, one per line. Each runs as its own search and a job matched by two terms is returned once. |
location | string | empty | Written as you would type it on LinkedIn. Empty means worldwide. |
postedWithin | dropdown | Any time | Limits results to the last 24 hours, week, or month. |
jobType | dropdown | Any | Full-time, part-time, contract, temporary, internship, volunteer, other. |
maxJobsPerSearch | integer | 25 | Stop after this many jobs per keyword. Set it to 0 for everything LinkedIn serves. |
scrapeDetails | boolean | true | Open each job for the full record. Turn off for a much faster run with search-results fields only. |
enrichCompanies | boolean | false | Attach employer firmographics to every job, one lookup per company per run. |
searchUrls | array | empty | Paste LinkedIn job search URLs instead of typing keywords. |
Output

Those nine columns are the Output tab's default view, not the whole record. Each row also carries the full description as HTML and plain text, the expiry date, required skills, education level, the employer block, and the structured location, which all come through on export and over the API.
One dataset row per job:
{"search_keywords": "python developer","search_location": "Amsterdam, Netherlands","job_id": "4445431410","title": "Python Software Engineer","company": "IMC Trading","company_url": "https://www.linkedin.com/company/imc-trading","location": "Amsterdam, North Holland, Netherlands","is_remote": false,"posted": "2026-07-26","date_posted": "2026-07-26T02:18:40.000Z","valid_through": "2026-08-26T02:18:40.000Z","employment_type": "FULL_TIME","seniority": "Mid-Senior level","job_function": "Engineering and Information Technology","industries": "Financial Services","applicants": 200,"employer_job_id": "f862f028-49d1-4487-96d8-47b7e73fd508","base_salary": null,"skills": null,"education": { "@type": "EducationalOccupationalCredential", "credentialCategory": "bachelor degree" },"description_html": "<p>...</p>","description_text": "...","job_location": { "locality": "Amsterdam", "region": null, "country": "NL", "street": null },"view_url": "https://nl.linkedin.com/jobs/view/python-software-engineer-at-imc-trading-4445431410"}
The example is trimmed to fit. You can preview results in the Output tab while the run is still going, and download the dataset as JSON, CSV, Excel, HTML, XML, or RSS from the Storage tab. Everything is also available through the Apify API and the Make, Zapier, n8n, and Google Sheets integrations.
Data table
| Field | Notes |
|---|---|
job_id, view_url | Stable LinkedIn identifiers. Use job_id to deduplicate across runs and detect new postings. |
search_keywords, search_location | Which of your searches produced this row, so multi-keyword runs stay traceable. |
title, company, location, posted | The basics, on every row even with details switched off. |
is_remote | true when LinkedIn flags the posting as remote, read from the job's own page rather than the search results. |
date_posted, valid_through | Exact timestamps from LinkedIn's structured data, including when the posting expires. |
applicants | Capped by LinkedIn at 200. A row showing 200 means "200 or more", and applicants_raw keeps the original wording. |
seniority, job_function, industries, employment_type | Always in English, on every job in every country. |
months_of_experience | Required experience as a number, so 24 means two years. Filter on it instead of parsing prose. |
employer_job_id | The employer's own requisition ID, separate from LinkedIn's job_id. Joins against a careers page or ATS. |
base_salary | Populated when LinkedIn publishes structured pay, which is uncommon on public postings. Usually null. |
description_html, description_text | Full posting body, as HTML and as plain text with paragraphs and list items on their own lines. |
skills, education, hiring_org, job_location | Present when LinkedIn publishes them, null when it does not. |
company_details | Employer firmographics, when Add company details is on. null otherwise. |
detail_error | null on success. Set when a detail page failed, so you can retry those rows. |
How much does it cost to scrape LinkedIn jobs?
Billing is per job returned, at the per-1,000 rate shown on this page, and maxJobsPerSearch doubles as a hard cost cap. Searches, result pages, and jobs that fail to load are all free.
The 10-job Amsterdam run above bills as 10 jobs, well under a cent per job at any plan tier. The monthly usage credit included in Apify's free plan covers a few thousand jobs.
Add company details is the one thing billed on top, because it fetches a company page that produces no row of its own. It is off unless you switch it on, and it bills once per company per run however many of that company's jobs you get, so 40 jobs across 12 employers bills 40 jobs plus 12 companies. A lookup that fails is not billed.
Tips and advanced options
- Search for remote work with the keyword. LinkedIn ignores its own remote filter on public searches, so
remote python developeras a search term is the way to bias results that way. Every row still carriesis_remotefrom the job's own page, so you can drop the ones that are not. - Detect new postings, not all postings. Set Posted within to
Past 24 hourson a daily schedule and every run is already just the new ones. Keep thejob_idset from last time and diff it if a missed run would matter. - Turn details off for a headcount sweep. If you only need to know how many jobs match a term in a market, Scrape full job details off makes the run several times faster.
- Split a broad search to get past the 1,000 cap. Run the same keyword against several locations, or the same location across
Past weekwindows, and each search gets its own 1,000. Jobs found twice are still only returned once.
What you get and what you do not
Public LinkedIn job search is generous but bounded. Being specific about the edges:
- One search returns at most 1,000 jobs. LinkedIn's ceiling for a logged-out visitor, not a limit of this Actor. Split by location or posting window to cover more.
- No recruiter or job poster. LinkedIn shows that only to logged-in members, so it is absent rather than guessed at.
- No external apply URL. Also login-gated.
view_urlalways gets you to the posting itself. base_salaryis usually null. Employers rarely publish structured pay. When they do you get currency, range, and period.- Remote and experience level are not search filters. LinkedIn ignores its own, so you filter on the
is_remoteandsenioritycolumns after the run instead.
Under the hood
No login, no cookies, no account. Jobs come from LinkedIn's public guest endpoints, the same pages a logged-out visitor sees. Requests use Chrome TLS impersonation so they look like an ordinary browser.
Only filters LinkedIn honours are offered. Its public search takes remote and experience-level parameters and then returns the same results either way, so those are not exposed here as settings you could trust. Both values are accurate on the job pages themselves and ship on every row as is_remote and seniority.
Job type is LinkedIn's own filter and it works, with one wrinkle. When too few postings match, LinkedIn relaxes the filter and returns adjacent types rather than an empty page, so a narrow search can include employment types you did not ask for. employment_type on each row is always the truth.
Jobs are not returned newest first. LinkedIn orders them by its own relevance ranking and ignores any request to sort by date, so Posted within is the way to bias a run toward recent postings. Sort on date_posted if you need strict chronology.
Requests go out directly first. Only when LinkedIn refuses does the Actor retry through a US proxy, up to 5 times, each on a different IP. A clean run never touches the proxy. Refusal means HTTP 403, 429, or 999, or a 200 served from the login wall.
Runs stop themselves when LinkedIn shuts the door. If 15 requests fail back to back, the run ends with an explanation rather than grinding on. Any success resets that counter. Whatever was scraped before the stop stays in the dataset.
FAQ
Do I need a LinkedIn account or cookies? No. The Actor reads LinkedIn's public guest endpoints, the same pages a logged-out visitor sees. Your account is never used, so it cannot be restricted or banned for your runs.
Is this legal? It collects only publicly visible job postings, which are published deliberately so people can find and apply to them. It does not touch private data and it does not log into anyone's account. You are responsible for how you use the output, and GDPR applies to you if you store personal data an employer chose to write into a description.
Does it include the recruiter who posted the job? No. LinkedIn shows the job poster only to logged-in members. Fields that require login are left out rather than filled with guesses.
Why is base_salary usually null?
LinkedIn publishes structured pay only when the employer provides it, which is uncommon. When it exists you get currency, range, and pay period. Otherwise salary often sits in the description, and description_text is there to mine for it.
Why is there no remote or seniority filter?
LinkedIn's public job search accepts both and then ignores them, so a toggle here would do nothing. Every row carries is_remote and seniority read from the job's own page, which is accurate, so filter on those columns after the run.
Why did my search return fewer jobs than LinkedIn shows? LinkedIn caps any public job search at 1,000 results and does not serve more to a logged-out visitor. Split the search by location or by posting window to cover more.
How current is the data? Fetched live from LinkedIn at the moment of the run. There is no cached database in between, so you get what the page shows right then.
Can I search within one company instead? Use the LinkedIn Company Jobs Scraper, which takes a company and returns every opening it has, and is not subject to the 1,000-result search ceiling.
Support
Found a bug or need a field that is not here? Open an issue on the Issues tab and it will be looked at.