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_SUMMARYrecord 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
}
-
maxResultsis per board, from 1 to 500. -
maxAgeDaysdrops postings older than N days, on every board, before billing. Undated rows are kept. -
strictKeywordMatchkeeps only rows whose title or description contains your search words. LinkedIn never answers "nothing matched"; it returns loosely related postings instead. -
onlyNewJobsremembers 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"])
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 |
| Software Engineer 1, Backend | Robinhood | New York, NY | 2026-10-14 | $128,000–$150,000 / year | |
| Software Engineer II (Java/AWS) | JPMorganChase | New York, NY | 2026-10-08 | ||
| 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"
}
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_sourceisdirect_dataon the salaried Indeed and Glassdoor rows anddescriptionon 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_remotewithwork_from_home_type. Two of the Indeed rows haveis_remote: trueand awork_from_home_typeofHybrid. -
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_countis 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)