LinkedIn Job Scraper API - Salary, Skills & Full Descriptions avatar

LinkedIn Job Scraper API - Salary, Skills & Full Descriptions

Pricing

from $1.00 / 1,000 results

Go to Apify Store
LinkedIn Job Scraper API - Salary, Skills & Full Descriptions

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

Thodor

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 days ago

Last modified

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 titleCompanyLocationPostedSeniorityApplicants
Python Software EngineerIMC TradingAmsterdam, North Holland, Netherlands2026-07-26Mid-Senior level200+
Backend Engineer (Python / FastAPI / SQL)MarvelX AIAmsterdam, North Holland, Netherlands2026-07-17Associate200+
Python DeveloperMNThe Hague, South Holland, Netherlands2026-07-29Mid-Senior level98
Data EngineerSamsung ElectronicsSchiphol-Rijk, North Holland, Netherlands2026-07-22Associate200+
Data EngineerCIMSOLUTIONSNorth Holland, Netherlands2026-07-28Associate25

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_text is ready to feed to an LLM without stripping tags first.
  • Remote and seniority on every row. is_remote and seniority come 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_through tells 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

  1. Open the Actor and type your search terms into Search keywords, one per line. Each term runs as its own search.
  2. Set Location the way you would type it on LinkedIn: Berlin, Germany, London, United Kingdom, United States. Leave it empty to search worldwide.
  3. Click Start. Jobs stream into the dataset as they are found, so the Output tab fills up before the run finishes.
  4. 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 ApifyClient
client = 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
}
FieldTypeDefaultWhat it does
keywordsarraypython developerSearch terms, one per line. Each runs as its own search and a job matched by two terms is returned once.
locationstringemptyWritten as you would type it on LinkedIn. Empty means worldwide.
postedWithindropdownAny timeLimits results to the last 24 hours, week, or month.
jobTypedropdownAnyFull-time, part-time, contract, temporary, internship, volunteer, other.
maxJobsPerSearchinteger25Stop after this many jobs per keyword. Set it to 0 for everything LinkedIn serves.
scrapeDetailsbooleantrueOpen each job for the full record. Turn off for a much faster run with search-results fields only.
enrichCompaniesbooleanfalseAttach employer firmographics to every job, one lookup per company per run.
searchUrlsarrayemptyPaste LinkedIn job search URLs instead of typing keywords.

Output

LinkedIn Job Scraper API output showing job title, company, location, remote flag, posted date, applicant count, employment type and seniority for each scraped LinkedIn job

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

FieldNotes
job_id, view_urlStable LinkedIn identifiers. Use job_id to deduplicate across runs and detect new postings.
search_keywords, search_locationWhich of your searches produced this row, so multi-keyword runs stay traceable.
title, company, location, postedThe basics, on every row even with details switched off.
is_remotetrue when LinkedIn flags the posting as remote, read from the job's own page rather than the search results.
date_posted, valid_throughExact timestamps from LinkedIn's structured data, including when the posting expires.
applicantsCapped by LinkedIn at 200. A row showing 200 means "200 or more", and applicants_raw keeps the original wording.
seniority, job_function, industries, employment_typeAlways in English, on every job in every country.
months_of_experienceRequired experience as a number, so 24 means two years. Filter on it instead of parsing prose.
employer_job_idThe employer's own requisition ID, separate from LinkedIn's job_id. Joins against a careers page or ATS.
base_salaryPopulated when LinkedIn publishes structured pay, which is uncommon on public postings. Usually null.
description_html, description_textFull posting body, as HTML and as plain text with paragraphs and list items on their own lines.
skills, education, hiring_org, job_locationPresent when LinkedIn publishes them, null when it does not.
company_detailsEmployer firmographics, when Add company details is on. null otherwise.
detail_errornull 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 developer as a search term is the way to bias results that way. Every row still carries is_remote from 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 hours on a daily schedule and every run is already just the new ones. Keep the job_id set 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 week windows, 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_url always gets you to the posting itself.
  • base_salary is 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_remote and seniority columns 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.