DEV Community

Harsh Maur
Harsh Maur

Posted on

Scrape LinkedIn Jobs Without Login or API Key (Half Were Recruiters)

This morning I pulled every "founding engineer" job posted in the US in the last 24 hours, straight into Python. No LinkedIn account, no cookies, no API key from LinkedIn. The run took 10 seconds and cost about 1.5 cents.

The more useful part came after that: half of those "founding engineer" listings were not posted by a startup at all. They came from recruiting agencies. If you are job hunting, building a job board, or looking for companies that are hiring, that difference matters. So this post also shows the 15-line filter I use to split them.

Full disclosure: I built the scraper used here and sell it on the Apify Store. Everything below is a real run from 20 October 2026, with the real output.

Why there is no official LinkedIn jobs API for you

LinkedIn has a Job Posting API, but it is for partners who post jobs (ATS vendors, job boards with a contract). It does not let you search or read other companies' listings. If you want job data, your options are:

  1. Copy and paste from linkedin.com/jobs by hand.
  2. Write your own scraper and maintain it every time LinkedIn changes its pages.
  3. Use a hosted scraper that reads LinkedIn's public job pages, the ones anyone sees without logging in.

I went with option 3 and built LinkedIn Jobs Scraper on Apify. It never logs in, so there is no account to get banned and no cookie to refresh.

What you need

  • A free Apify account. The free plan includes $5 of credit each month.
  • Your Apify API token (Console → Settings → API & Integrations).
  • Python 3 with requests.

That is all. You do not need anything from LinkedIn.

Step 1: Describe the search

The input is plain JSON. These are the fields I used:

{
  "keywords": "founding engineer",
  "location": "United States",
  "datePosted": "1",
  "maxItems": 15,
  "scrapeCompany": true
}
Enter fullscreen mode Exit fullscreen mode
  • datePosted: "1" means the past 24 hours. Use "7" for a week or "30" for a month.
  • maxItems caps the number of jobs, and you pay per job, so keep it low while you test.
  • scrapeCompany adds the employer's employee count, website and headquarters to each row. It costs nothing extra.

If you already built a search on linkedin.com/jobs, you can paste its URL instead. LinkedIn's own filter menu only offers 24 hours, a week or a month. I use this free LinkedIn job search URL builder when I want "posted in the last 2 hours" or another custom window:

{ "startUrls": [{ "url": "https://www.linkedin.com/jobs/search/?keywords=founding%20engineer&location=United%20States&f_TPR=r86400" }] }
Enter fullscreen mode Exit fullscreen mode

Step 2: Run it from Python and get the jobs back

Apify has a "run and wait" endpoint that starts the scraper and returns the dataset in the same HTTP call. It is fine for a few hundred jobs.

import os, re, requests

ACTOR = "harshmaur~linkedin-jobs-scraper"
TOKEN = os.environ["APIFY_TOKEN"]

run_input = {
    "keywords": "founding engineer",
    "location": "United States",
    "datePosted": "1",        # past 24 hours
    "maxItems": 15,
    "scrapeCompany": True,    # adds employee count, website, HQ
}

jobs = requests.post(
    f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
    headers={"Authorization": f"Bearer {TOKEN}"},
    json=run_input,
    timeout=300,
).json()
Enter fullscreen mode Exit fullscreen mode

Each item in jobs is one job. Here is a real one from my run, trimmed (the description fields are long):

{
  "title": "Founding Fullstack AI Engineer",
  "company": "AustinWorks",
  "companyUrl": "https://www.linkedin.com/company/austinworksrecruitment",
  "location": "San Francisco Bay Area",
  "postedTimeAgo": "9 hours ago",
  "employmentType": "Full-time",
  "seniorityLevel": "Entry level",
  "applicants": 80,
  "salary": "$150,000.00/yr - $210,000.00/yr",
  "salaryMin": 150000,
  "salaryMax": 210000,
  "salaryCurrency": "USD",
  "salaryPeriod": "YEAR",
  "jobPosterName": "Charlie Austin",
  "jobPosterTitle": "Connecting top talent across Software + FDE + AI",
  "jobPosterProfileUrl": "https://www.linkedin.com/in/charlie-austin-a4a87a170",
  "url": "https://www.linkedin.com/jobs/view/4478636704"
}
Enter fullscreen mode Exit fullscreen mode

Every row also has descriptionText, descriptionMarkdown and descriptionHtml, plus industries, jobFunction, companyWebsite, companyEmployeesCount and companyHeadquarters. When LinkedIn does not show a value, the field is null instead of missing, so your code does not need try/except around every key.

Look at companyUrl in that example: austinworksrecruitment. That "founding engineer" job is a recruiter's listing.

Step 3: Filter out recruiting agencies

My first idea was to filter on industries == "Staffing and Recruiting". On real data, that caught only 2 of the agencies. Half of the agencies list "Software Development" or "Artificial Intelligence" as their industry, just like their clients.

What works better is to check the company name, its LinkedIn slug, its website and its industry together:

AGENCY = re.compile(r"recruit|staffing|talent|\bsearch", re.I)

def is_agency(job):
    text = " ".join(filter(None, [
        job["company"], job["companyUrl"], job.get("companyWebsite"), job.get("industries"),
    ]))
    return bool(AGENCY.search(text))

direct = [j for j in jobs if not is_agency(j)]
print(f"{len(jobs)} jobs, {len(jobs) - len(direct)} from agencies, {len(direct)} posted by the company")

for j in direct:
    pay = f"${j['salaryMin']:,}-${j['salaryMax']:,}" if j["salaryMin"] else "no pay listed"
    print(f"- {j['title']} @ {j['company']} ({j['companyEmployeesCount'] or '?'} staff), "
          f"{pay}, {j['applicants'] or '?'} applicants\n  {j['url']}")
Enter fullscreen mode Exit fullscreen mode

The \b before search matters. Without it, "Research Services" counts as an agency.

The real output

15 jobs, 8 from agencies, 7 posted by the company
- Founding Aerospace Engineer @ Aether (1 staff), no pay listed, ? applicants
- Founding Engineer @ Bearcroft (4 staff), no pay listed, 61 applicants
- Founding Backend Engineer @ ALL KNOWN SERVICES (9 staff), no pay listed, 111 applicants
- Founding Full-Stack Engineer (up to $220k) @ Dex (40 staff), no pay listed, ? applicants
- Founding AI Engineer @ Convoy Health (4 staff), no pay listed, 62 applicants
- Founding Engineer @ Arcafeed (Acq. by Whop) (4 staff), no pay listed, 52 applicants
- Founding Product Engineer @ WhyHireWrong? (1 staff), no pay listed, ? applicants
Enter fullscreen mode Exit fullscreen mode

(I removed the job URLs from this listing to keep it short. The script prints them.)

Three things surprised me:

  1. Every salary in the sample came from an agency. Five of the 15 jobs had a parsed salary, and all five were recruiter listings. None of the direct startup posts filled in LinkedIn's pay field. Dex put "up to $220k" in the job title instead, so salary is null for that row. If pay matters to you, also search title and descriptionText for a dollar sign.
  2. The filter is not perfect. ALL KNOWN SERVICES passed as "direct", but the person who posted it describes themselves as helping people land W2 contract roles. Adding jobPosterTitle to the text you check catches more agencies, with some false positives.
  3. Applicant counts are mostly low. Most direct posts had under 65 applicants within a day. A null count (printed as ?) usually means LinkedIn showed "Be among the first 25 applicants". LinkedIn shows at most about 200, so 200 means "200 or more".

Two runs a few minutes apart did not return exactly the same 15 jobs. LinkedIn reorders its results, so for a daily feed, de-duplicate on jobId.

What this costs

You pay per job in the dataset. There is no start fee and no subscription. On Apify's free plan it is $1.00 per 1,000 jobs, and $0.40 to $0.47 per 1,000 on paid plans. Recruiter fields and company details are included in that price.

My 15-job test cost about 1.5 cents on the free plan. A daily run of 50 new jobs is roughly $0.70 a month on the Starter plan.

Get more than 1,000 jobs

LinkedIn stops every search near 1,000 results. To go past that, turn on splitByLocation. The scraper then runs your search in each large city of the country and removes duplicates:

{ "keywords": "software engineer", "splitByLocation": true, "splitCountry": "us", "maxItems": 5000 }
Enter fullscreen mode Exit fullscreen mode

Make it a daily alert

  1. In Apify Console, save the input above as a task.
  2. Add a schedule that runs it every morning.
  3. Pick Slack, Notion or Google Sheets in the scraper's Send results to an app option, or call the run-and-wait endpoint from your own cron job and keep the filter step.

With datePosted: "1" and a daily schedule, you see each new job once.

FAQ

Can I scrape LinkedIn jobs without logging in?
Yes. Job listings are public. Anyone can open linkedin.com/jobs in a private window and read them. The scraper reads those same public pages and never asks for your LinkedIn account.

Do I need a LinkedIn API key?
No. You need only an Apify API token, which comes free with an Apify account.

Can I get the recruiter's name for a job?
When the listing names the person who posted it, you get jobPosterName, jobPosterTitle and jobPosterProfileUrl. In my run, 7 of 15 jobs named someone.

Do LinkedIn's remote and experience filters still work?
LinkedIn moved to an AI job search in 2026 and dropped several classic filters. Date posted, company and Easy Apply still filter exactly. Results for remote, job type and experience level can be approximate. Filter on seniorityLevel or employmentType afterwards if you need an exact match.

Is it legal?
The scraper collects only public job listings that anyone can see without logging in. It does not touch profiles or anything behind a login. Check the rules for your own use case.

Try it

LinkedIn Jobs Scraper on Apify. Paste the JSON from step 1 into the input form, press Start, and you have a spreadsheet of jobs in about 10 seconds.

Want to test a search before you scrape it? Build the URL with the LinkedIn job search URL builder, open it in a private window to check the results, then paste it into startUrls.

If you build something with it, or the agency filter misses a pattern you see often, tell me in the comments.

The Apify links in this post include my referral code.

Top comments (0)