DEV Community

Zakaria Blefkih
Zakaria Blefkih

Posted on

How I scrape jobs from LinkedIn, Indeed and Glassdoor in one run (no login)

Disclosure: I built this. Multi Job Board Scraper is my actor on Apify, and I earn money from it. Every number below comes from its README, its live pricing record or the real run printed in this post.

A "software engineer in New York" search normally means three tabs (LinkedIn, Indeed, Glassdoor), and the same role can show up on more than one of them. I wanted one search and one table, so I built it.

What it does

  • One search covers LinkedIn, Indeed, Glassdoor and The Muse by default. Eight keyless boards can be added; seven are remote-only and join remote searches automatically.
  • Every row has the same 65 columns. A field a board did not publish is null, never a missing column.
  • No login and no job-board API key: the actor never signs in anywhere, and every row is public data. From code you need an Apify token.
  • LinkedIn, Indeed and Glassdoor go through the open-source JobSpy library (python-jobspy), with my fixes on top for LinkedIn paging, Glassdoor and Markdown descriptions.
  • Every run writes a RUN_SUMMARY record that says what each board returned.

The input

The shortest complete run is a search term and a location; everything else is optional. This is the version I would put on a daily schedule:

{
  "searchTerm": "software engineer",
  "location": "New York, NY",
  "maxResults": 20,
  "maxAgeDays": 30,
  "strictKeywordMatch": true,
  "onlyNewJobs": true
}
Enter fullscreen mode Exit fullscreen mode
  • maxResults is per board, from 1 to 500.
  • maxAgeDays drops postings older than N days, on every board, before billing. Undated rows are kept.
  • strictKeywordMatch keeps only rows whose title or description contains your search words. LinkedIn never answers "nothing matched"; it returns loosely related postings instead.
  • onlyNewJobs remembers what this exact search already delivered, in a store in your own Apify account, and delivers and bills only postings it has not seen. An identical rerun delivered zero rows when I measured it (15 rows, then 0).

From Python:

from apify_client import ApifyClient

run_input = {"searchTerm": "software engineer", "location": "New York, NY",
             "maxAgeDays": 30, "strictKeywordMatch": True}

client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("flash_scraper/multi-jobboard-scraper").call(run_input=run_input)
rows = client.dataset(run["defaultDatasetId"]).list_items().items
summary = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("RUN_SUMMARY")["value"]
print(summary["jobs_per_board"])
Enter fullscreen mode Exit fullscreen mode

Real output: every row of one run

Twice a day I run a small private health check of the live code (build 0.1.87): software engineer, New York, NY, the four default boards, 3 rows per board. This is the 16 October 2026 run at 06:20 UTC, all of it: 12 rows, 3 per board, 65 columns each, 16 seconds end to end. Six of the columns, values as delivered:

Board Title Company Location Posted Salary
indeed Associate Software Engineer Index Engines Holmdel, NJ, US 2026-07-29
indeed Sr. Full-Stack Developer Valley Bank Morristown, NJ, US 2026-07-20 $113,500–$200,700 / year
indeed Senior Software Engineer Throtle Red Bank, NJ, US 2026-02-12 $140,000–$160,000 / year
linkedin Software Engineer 1, Backend Robinhood New York, NY 2026-10-14 $128,000–$150,000 / year
linkedin Software Engineer II (Java/AWS) JPMorganChase New York, NY 2026-10-08
linkedin Software Development Engineer III CLEAR New York, NY 2026-09-24
glassdoor Software Engineer Pure Interactive Entertainment, Inc New York, NY 2026-10-06 $120,000–$300,000 / year
glassdoor Senior/Software Engineer II, Backend Duolingo New York, NY 2026-10-03 $182,800–$247,300 / year
glassdoor Software Engineer - 2027 New Grads Ellipsis Labs New York, NY 2026-05-28 $150,000–$200,000 / year
muse Sr Software Engineer - Android Uber New York, NY; San Francisco, CA; Sunnyvale, CA 2026-10-01
muse Staff Software Engineer, Backend GlossGenius Flexible / Remote; New York, NY 2026-07-20
muse Senior Security Engineer, Detection & Response Flexport Flexible / Remote; San Francisco, CA 2026-10-13

One row with more of its fields (13 of 65, values as delivered):

{
  "site": "linkedin",
  "title": "Software Engineer 1, Backend",
  "company": "Robinhood",
  "date_posted": "2026-10-14",
  "job_level": "entry level",
  "company_industry": "Financial Services",
  "salary_text": "$128,000–$150,000 / year",
  "salary_source": "description",
  "applicants": 200,
  "applicants_is_bound": true,
  "found_on_sites": "linkedin",
  "duplicate_count": 1,
  "job_url": "https://www.linkedin.com/jobs/view/4476692726"
}
Enter fullscreen mode Exit fullscreen mode

What those rows tell you

  • Salary is on 6 of 12 rows: Glassdoor 3 of 3, Indeed 2 of 3, LinkedIn 1 of 3, The Muse 0 of 3. The README's larger samples agree: Glassdoor has pay on about 100% of its rows, and The Muse had 0 salary fields in 220 measured rows. salary_source is direct_data on the salaried Indeed and Glassdoor rows and description on the Robinhood row.
  • Indeed's three rows are all in New Jersey. distance, a radius in miles that LinkedIn and Indeed honor, is the setting for that.
  • Old postings come back. Throtle's is dated 12 February. Five of the twelve rows are more than 30 days old, so the scheduled input above would have dropped them before billing.
  • The Muse returned a remote role on a New York search: the Flexport row reads "Flexible / Remote; San Francisco, CA", with is_remote: true.
  • Read is_remote with work_from_home_type. Two of the Indeed rows have is_remote: true and a work_from_home_type of Hybrid.
  • LinkedIn applicant counts can be bounds. Robinhood's 200 has applicants_is_bound: true; JPMorganChase's 178 is a count.
  • No duplicates this time. duplicate_count is 1 on all 12 rows; at 3 per board, nothing overlapped.

How dedup works

Across boards, rows are matched on the title plus a normalized company name: legal suffixes are stripped, so Wipro and Wipro Limited collapse, and text is Unicode-folded, so Développeur and Developpeur match. Within one board, two rows merge only when they are the same listing (same job_url, else same id), so five real openings at one employer stay five rows. The surviving row lists every board in found_on_sites, and its empty cells are filled from the dropped duplicate; a missing salary is copied as one block from a single board. The merge happens before billing, so a role on three boards is charged once.

On a measured run at 30 per board on LinkedIn, Indeed and Glassdoor (data analyst, New York, 8 August 2026), 90 raw listings became 89 rows.

Limits, honestly

  • Glassdoor returns 403 for some whole runs (upstream; it comes back on its own within hours). The run status names it, and the missing board bills nothing. It also tops out around 28-30 rows per query.
  • The Muse has no salary data, and every word of your search term must appear in the title or description, so long phrases return few rows.
  • LinkedIn's guest search ignores its remote and job-type filters, so the actor drops contradicting rows itself, before billing.
  • Blocked at the source: Google Jobs, ZipRecruiter, Bayt, BDJobs and Naukri. DevITjobs UK was discontinued upstream.
  • Not supported: anything behind a login, auto-applying, and history. Each run reads the boards' current listings.

Price

From the actor's live pricing record, read on 16 October 2026: $0.005 per delivered job on Apify's free plan ($5 per 1,000), down to $3.50 per 1,000 on the Diamond plan, plus $0.00005 per run start. A 12-row run like the one above is $0.06 plus the start fee at the free-plan rate. Rows removed by a filter bill nothing, and a run that delivers nothing bills only the start. The Pricing tab on the actor page is authoritative.

Try it

Multi Job Board Scraper on Apify. Run it once with just a search term and a location, open RUN_SUMMARY, and see what each board returned for your search before you schedule anything. If your rows don't match what this post says, tell me in the comments.

Top comments (0)