Multi Job Board Scraper: LinkedIn, Indeed, Glassdoor + Dedup
Pricing
from $4.00 / 1,000 results
Multi Job Board Scraper: LinkedIn, Indeed, Glassdoor + Dedup
Multi job board scraper, job board aggregator and job aggregator: LinkedIn, Indeed, Glassdoor, The Muse + 8 keyless boards in one run, deduplicated. New jobs alert and job monitoring on a schedule; ATS job scraper for Greenhouse, Lever, Ashby. Multi source job scraper with dedup, no API key.
Pricing
from $4.00 / 1,000 results
Rating
5.0
(3)
Developer
Flash Scrape
Maintained by CommunityActor stats
6
Bookmarked
44
Total users
25
Monthly active users
2 days ago
Last modified
Categories
Share
Multi Job Board Scraper is a job board aggregator on Apify that runs one search across LinkedIn, Indeed, Glassdoor, The Muse and eight keyless job boards and returns one deduplicated row per role — a job posted on three boards bills once, not three times.
Try it: New Job Alerts: Daily Job Monitor, Only New Postings
At a glance
| Boards | LinkedIn, Indeed, Glassdoor and The Muse by default, plus 8 keyless job boards (9 keyless sources counting The Muse) — 12 working boards in one run. Five more (Google Jobs, ZipRecruiter, Bayt, BDJobs, Naukri) are blocked at the source and the Board status table below says so |
| Dedup | One deduplicated row per role across boards — a job posted on three boards bills once; found_on_sites and duplicate_count show the merge on the row |
| Price | $0.005 per deduplicated job — $5 per 1,000 on the free plan; paid plans pay less (Pricing below; the Pricing tab is authoritative). Rows removed by a filter are never billed |
| New jobs alert | Tick Only new jobs, schedule it, and each run delivers and bills only postings it has not seen before — with Slack / Discord / webhook digests via webhookUrl |
| Keys / proxy | No login, no API key, nothing to bring — every row is public data; Apify's datacenter proxy pool is the measured default |
| Export | CSV, JSON or Excel — the same 54 columns on every row, seven saved Output views |

Pricing
| Event | Free plan | Paid plans |
|---|---|---|
| Deduplicated job delivered | $0.005 ($5 per 1,000) | less — $0.0045 on Bronze down to $0.0035 on Diamond (live pricing record read 2026-08-29; the Pricing tab on this page is always authoritative) |
| Run start | $0.00005 | $0.00005 |
Two worked costs at the free-plan rate: an untouched form (the four default boards, up to 20 rows each — the input schema's defaults) delivers at most 80 deduplicated jobs for at most $0.40; a 500-job sweep costs $2.50. A job found on three boards bills once, rows removed by a filter bill nothing, and a failed or empty run bills only the $0.00005 start.
Use from an AI agent
- MCP: point Claude, ChatGPT, Cursor or any MCP client at
https://mcp.apify.com?tools=flash_scraper/multi-jobboard-scraper; the tool is named after the Store slug and takes this actor's input unchanged. Keywords for the server'ssearch-actorstool: LinkedIn jobs, Indeed jobs, job postings, job board scraper, job listings. Tool-name spellings, payment without an Apify token and measured timings: Use it from an AI agent (MCP). - Smallest useful call (Python
apify-client; the same JSON works in the Console, the REST API and n8n/Make/Zapier):
from apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")run = client.actor("flash_scraper/multi-jobboard-scraper").call(run_input={"searchTerm": "data analyst", "location": "New York, NY"})rows = client.dataset(run["defaultDatasetId"]).list_items().items
- Output contract: the same 54 columns on every row of every run — a field a board did not publish is
null, never a missing column. The full field list with measured fill rates is under Output fields and how often they are filled; every run also writes a machine-readableRUN_SUMMARYrecord to its key-value store.
Board status — read this first
We would rather tell you than let you find out on a paid run. Checked 2026-08-07, re-measured 2026-08-11: all boards work from Apify's default datacenter pool (52 of 54 rows vs residential on the same search, faster and far cheaper). Residential remains available in Proxy configuration if a board starts coming back short:
| Board | Status | Notes |
|---|---|---|
| Working (default) | Full job details fetched by default — descriptions on every LinkedIn row | |
| Indeed | Working (default) | Richest company data (size, revenue, addresses) |
| Glassdoor | Working (default), intermittently blocked | Salary on ~100% of rows it returns, plus employer rating. Glassdoor 403s some runs entirely (upstream, comes back on its own within hours) — when that happens the run status names it and the missing board bills nothing. Tops out around ~28-30 rows per query — a board-side cap, not a bug |
| The Muse | Working (default since 2026-08-14) | The only extra board with real city coverage (server-side location filtering — measured 12/12 success from the standard proxy pool, 20 rows/page). Spell places its way — New York, NY, Toronto, Canada: a spelling it does not know is reported as a Muse failure naming the fix instead of billing its remote pool (measured 2026-08-29), and Remote as the location maps to that pool. Carries no salary data — 0 salary fields in 220 measured rows |
| Remotive | Working (remote-only) | Auto-joins remote searches; rows may not be re-posted to other job boards (see Licensing below) |
| Jobicy | Working (remote-only) | Auto-joins remote searches; credit Jobicy + keep the original apply links |
| Himalayas | Working (remote-only) | Auto-joins remote searches; link back to himalayas.app |
| HN "Who is hiring?" | Working (remote-only) | Startup jobs from the monthly Hacker News thread |
| Remote OK | Working (remote-only) | 100 newest remote listings, server-side tag filtering. Rows keep their remoteok.com link — required attribution, see Licensing |
| We Work Remotely | Working (remote-only) | Category RSS feeds (25-100 rows); salary parsed from listing text where published (~56% of programming posts) |
| Working Nomads | Working (remote-only) | Curated remote list (~50 rows); no salary data |
| DevITjobs US | Working (opt-in) | US/Canada tech board, salary on ~100% of rows (annual, currency as published), experience level + tech stack on nearly every row |
| DevITjobs UK | Discontinued upstream (2026-08-29) | The board's public endpoint now redirects to a signup page (re-probed 2026-09-05). It stays selectable in case it returns, is named in the run status and bills nothing. Eleven of the twelve working boards deliver rows today |
| Google Jobs | Blocked at source | Google serves a bot-check page instead of results |
| ZipRecruiter | Blocked at source | API returns HTTP 403 |
| Bayt (MENA) | Blocked at source | HTTP 403 |
| BDJobs (Bangladesh) | Blocked at source | Redirects away from results |
| Naukri (India) | Blocked at source | Demands a CAPTCHA |
Company boards (atsCompanies) | Working (opt-in) | Greenhouse, Lever, Ashby, Recruitee, BambooHR, Workable — each company's own careers API, no search needed; see Can I scrape a specific company's careers page? below |
In a verified run (2026-08-08, python developer, remote only, 10 requested per board) the five API boards delivered: The Muse 10, Jobicy 10, HN "Who is hiring?" 5, Himalayas 6, Remotive 2 rows. Per-board counts track each board's live inventory for the term and move day to day (an earlier run saw Remotive 10 and Himalayas 9).
LinkedIn, Indeed, Glassdoor and The Muse are the default selection. The other boards are opt-in, with one convenience: when Remote jobs only is on and the board list is left at its default, the seven remote-only boards (Remotive, Jobicy, Himalayas, HN, Remote OK, We Work Remotely, Working Nomads) join the run automatically — RUN_SUMMARY.remote_boards_auto_added records exactly which ones were added. They are deliberately not added to location searches: for a query like nurse in Dallas they contribute nothing, so we don't run them. The Muse is the exception with real US city coverage, but its category-based search means every word of your search term must appear in the job's title or description — keep Muse search terms short.
The blocked five stay selectable in case they recover, and they cost you nothing when they return no rows — but do not plan a project around them today.
Every run writes a RUN_SUMMARY record to its key-value store with the exact per-board outcome, so you always know which board gave you what and why one was quiet. It also carries columns_per_row and the full columns list, so you can diff a run's schema without reading a single row. If every board you selected fails to fetch, the run is marked FAILED rather than quietly succeeding with an empty dataset.
Every successful run also sets a status message in the Console run header — row count, column count, and which preset fired and what it changed. Previously that line only appeared when search terms had been dropped, so a run driven by a preset finished without ever saying which preset had run.
What it does
One search. Up to twelve job boards. One deduplicated row per role — so a job posted to LinkedIn, Indeed and Glassdoor bills you once, not three times.
LinkedIn, Indeed and Glassdoor plus The Muse and 8 keyless boards (9 keyless sources: The Muse, Remotive, Jobicy, Himalayas, Hacker News "Who is hiring?", Remote OK, We Work Remotely, Working Nomads, DevITjobs US/UK) merged into a single clean table: title, company, location, salary (min/max/interval/currency plus annualized columns), remote flag, job type, posting date, full description, employer rating and company details. Search by keyword + location, filter by remote / job type / recency / salary present / title keywords / company, and export to CSV, JSON, or Excel.
Key facts:
- 12 working job boards in one run — LinkedIn, Indeed, Glassdoor and The Muse by default, plus 8 keyless boards you can add.
- $0.005 per deduplicated job ($5 per 1,000) — a job found on several boards bills once, and filtered rows are never billed.
- No login, no API key — the actor never logs in anywhere; every row is public data.
- Same 54 columns on every row, exported as CSV, JSON or Excel, with salaries normalized to annual for sorting.
- Monitoring mode + webhook alerts — schedule it with Only new jobs and it delivers and bills only unseen postings, with Slack / Discord digests via
webhookUrl.
Pricing: pay per row — $0.005 per deduplicated job ($5 per 1,000) from 2026-08-28, plus $0.00005 per run start; the live rate is always on this page's Pricing tab. A job found on three boards bills once, filtered rows are never billed, and a failed or empty run bills no per-row events (only the $0.00005 start).
What it does not do: it never logs in anywhere and never auto-applies — every row is public data. It cannot reach behind login walls (for LinkedIn it reads the pay span on the public search card when a posting carries one, plus posting text — the fuller base-pay block that LinkedIn's public job page shows on some postings, measured 2 of 6 on 2026-08-29, is not parsed by this actor yet), it is not a historical archive (each run reads the boards' current listings), and five selectable boards (Google Jobs, ZipRecruiter, Bayt, BDJobs, Naukri) are blocked at the source today — see the Board status table below.
Watch specific companies' careers pages — list company names in Watch companies and the actor reads each one's own ATS board directly (Greenhouse, Lever, Ashby, Recruitee, BambooHR or Workable — all keyless, tried in that order) and merges the live openings into the same 54-column table. Fresher than any job board, straight from the source. Leave the search term untouched and you get each company's whole board; type a term and only matching roles are delivered and billed.
Only pay for NEW jobs on a schedule — turn on Only new jobs (monitoring mode) and the actor remembers every job this search has already delivered to you (in a private store in your own account) and bills only postings it has not seen before. Measured before this existed: two identical runs a day apart shared 47% of their rows — a schedule was re-buying half its data every run. Memory is kept per search for 90 days.
Set up a daily new-jobs feed in 60 seconds — the recurring setup most buyers actually want:
- Fill in your search term and location, tick Only new jobs (monitoring mode), hit Save & start once.
- On the run page choose Actions → Schedule, pick daily (or hourly for hot markets).
- Done. Every run now delivers — and bills — only postings the previous runs have not seen. The first run seeds the memory; identical reruns bill zero (measured: 15 rows, then 0). Point the schedule's webhook at Slack, a Google Sheet via Zapier/Make, or your ATS, and it becomes a job alert service you own.
Remote jobs across eleven boards in one click — turn on Remote jobs only and hit Save & start: Indeed, Glassdoor and The Muse are asked for their own remote filters, LinkedIn rows are gated by their own is_remote column after scraping — LinkedIn's guest search accepts f_WT and ignores it, so the Actor forwards the flag and enforces it itself, before billing (measured) — while the seven remote-only boards (Remotive, Jobicy, Himalayas, HN Who-is-Hiring, Remote OK, We Work Remotely, Working Nomads) join automatically. No other job-board actor on the Store combines the big boards' remote filters with the remote-only APIs in one run. (Only want the remote-only boards, without LinkedIn / Indeed / Glassdoor? The leaner Remote Job Aggregator sweeps the ten keyless remote boards at $2 per 1,000.)
Built for recruiters & staffing agencies, job boards & aggregators, market researchers, and sales teams tracking hiring signals who want one consolidated jobs feed instead of running eight scrapers and reconciling them by hand.
What you get
- Twelve working boards in one run — LinkedIn, Indeed, Glassdoor and The Muse selected by default, plus eight keyless boards: seven remote-only ones (Remotive, Jobicy, Himalayas, HN "Who is hiring?", Remote OK, We Work Remotely, Working Nomads) that join remote searches automatically, and DevITjobs US/UK you switch on. One schema, one dataset.
- Cross-board dedup that shows its work — the same role on several boards becomes one row carrying
found_on_sitesandduplicate_count. Company names are normalized first, so Wipro and Wipro Limited collapse together. It merges across boards only: five genuinely different openings one employer posted to one board stay five rows, because you paid to scrape all five. You are billed for the surviving row only. - Salaries you can actually sort — hourly, weekly and monthly pay is normalized into
salary_min_annual/salary_max_annualon every salaried row, no flag to find. - Filters that never bill you — salary-required, title / company / city excludes, competitor-only, experience level, agency excludes and max posting age all run before the charge, and
RUN_SUMMARY.filter_removedcounts each one. - Seven ready-made Output views — Overview, Salary, Cross-board, Remote & location, Company info, Standard table, All columns. No column wrangling on your first run (exports still carry every column).
- The same 54 columns on every row, every run — a field a board did not publish is
null, never a missing column. Spreadsheet imports line up,pd.read_csvbehaves, positional parsers keep working. - Presets and quick-pickers — pick a goal and the matching options switch themselves on, with every change (and every refusal) written to the log.
- Paste a search URL — already built the search on LinkedIn, Indeed or Glassdoor? Drop the URL in and skip the form.
- Honest per-board reporting — every run writes a
RUN_SUMMARYnaming each board's real outcome. Boards that are blocked at the source are labelled in the table below, not sold to you.
Measured, not promised
Every number below comes from a real run of this Actor. Inputs and dates are given so you can reproduce them.
| Run | Result |
|---|---|
| Untouched form, pressed Start (the then-default 3 boards, 20/board) — 2026-08-08 | 60 rows in 27 s — LinkedIn 20, Indeed 20, Glassdoor 20. When Glassdoor is having one of its intermittent blocked spells you get ~40 rows from the other two, the status message says so, and the missing rows are never billed |
data analyst · New York, NY · 30/board, 3 boards — 2026-08-08 | 90 raw listings → 89 billed rows in 39 s; 1 duplicate merged across boards (Indeed 30, Glassdoor 30, LinkedIn 29) |
| Salary coverage on that same run | salary_min_annual filled on 74 of 89 rows (83%) |
| Stress run, 120/board, 3 boards — 2026-08-07 | 258 rows in 226 s — LinkedIn 118, Indeed 112, Glassdoor 28. Measured before the 2026-08-29 LinkedIn paging fix (the scraping library skipped result offsets 20–29, 40–59, … on runs above 20 LinkedIn rows, so those 118 were not the newest 118); not yet re-measured on the fixed build |
| Remote-only run, the five API boards, 10 requested each — 2026-08-08 | The Muse 10, Jobicy 10, HN 5, Himalayas 6, Remotive 2 — per-board totals track each board's live inventory for your term and vary day to day |
| LinkedIn descriptions on a default run — 2026-08-07 | 20 of 20 LinkedIn rows (detail fetching is on by default) |
Glassdoor tops out around ~28–30 rows per query no matter the cap — a board-side limit we report rather than hide. The Muse joined the default board set on 2026-08-14, so an untouched run now sweeps four boards. Every row of every run carries the same 54 columns; per-field fill rates are further down, under Output fields and how often they are filled.
First run
{"searchTerm": "data analyst","location": "New York, NY"}
That is a complete run. Everything else already has a working default: LinkedIn + Indeed + Glassdoor + The Muse, 20 job postings per board, full LinkedIn job details on, Apify datacenter proxy (measured: same rows as residential at a fraction of the cost). Open the Actor, hit Save & start without touching anything, and you get rows.
Presets
Pick a goal in Preset and the matching options are switched on for you. A preset only fills options you left at their default — anything you set yourself always wins — and both the run log and RUN_SUMMARY.preset_applied list exactly what it changed and what it refused to change, with your conflicting value.
| Preset | What it switches on | Effect on your bill |
|---|---|---|
| Salary research | requireSalary, enforceAnnualSalary, sortBy: salary_desc | Lower — rows carrying no pay are dropped before billing |
| Competitor hiring intel | includeCompanyDetails (a no-op since v0.1.48 — the company block ships on every run) | Unchanged — it does not narrow the run by itself: add the employers you are watching under Only these companies, or the run stays unfiltered and the log says so |
| Remote-only sweep | isRemote | Higher — with the board list left untouched this also brings in the seven remote-only boards (Remotive, Jobicy, Himalayas, HN, Remote OK, We Work Remotely, Working Nomads), and extra boards add rows |
| Fresh postings only | maxAgeDays: 7, plus a 168-hour board-side window when that cannot clash with your job type / easy-apply / remote choices | Lower — anything older is dropped before billing |
| Recruiter lead-gen | excludeAgencies (plus includeCompanyDetails, a no-op since v0.1.48) | Lower — staffing-agency reposts are dropped before billing |
Two quick-pickers sit beside it: Role (quick pick) and City (quick pick). Precedence is searchTerms > roleSelect > searchTerm and locations > citySelect > location — a picker overrides the matching free-text box (and warns you in the log when it does), while the multi-value arrays override the picker. Picking a non-US metro also switches countryIndeed to that country unless you set it yourself. Leave all three shortcuts alone and the Actor behaves exactly as it always has.
What the Output tab looks like
Seven views ship with the Actor; pick one above the results table instead of scrolling a wall of 54 raw columns. Overview is what opens by default.
| View | Columns | Use it for |
|---|---|---|
| Overview | logo, title, company, location, salary, remote, posted, source, match %, link | the first read of any run |
| Salary | readable salary, raw min / max / currency / period, yearly-normalized min / max, salary source | comp benchmarking on one axis |
| Cross-board | found on boards, listings for this role, applicants (LinkedIn), posted and updated dates | proof of the dedup, hiring-urgency signals |
| Remote & location | remote flag, work arrangement (Indeed), location, matched location, source | telling true remote from remote-ish |
| Company info | industry, size, revenue, rating, reviews, company page, website, address, logo | employer research (Indeed fills most of it) |
| Standard table | the original 16 columns | the pre-2026-08-29 default view, field for field |
| All columns | every one of the 54 columns, labeled | the whole row on screen |
Views are column presets, not filters: Salary still lists rows whose board published no pay (those cells are simply blank) and Cross-board still lists roles seen on a single board (listings for this role = 1). To actually remove rows, use requireSalary or isRemote — rows removed by a filter are never billed. Every CSV / JSON / Excel export from the Dataset API hands you all 54 columns unless you ask for a view: the API accepts ?view=<key>, and in the Console you select the view above the table, then Export — see How to read the output below. Until 2026-08-29 ?view=overview returned the 16-column Standard table; it now returns the 10-column Overview, and the old set is addressable as ?view=standard.
Why use this
- Twelve boards, one run — LinkedIn + Indeed + Glassdoor + The Muse by default, plus eight keyless job boards (Remotive, Jobicy, Himalayas, HN "Who is hiring?", Remote OK, We Work Remotely, Working Nomads, DevITjobs US/UK) merged into a single dataset.
- Paste a search URL — already built the search on LinkedIn, Indeed or Glassdoor? Copy the URL into
searchUrlsand skip the form. Keywords, location, radius (LinkedIn and Indeed only), remote flag, recency, job type, experience level and sort are read straight out of the URL — every honored parameter is listed below (with one LinkedIn caveat:f_WT/f_JTare forwarded but ignored at the source, see the table), and an unparseable URL is reported instead of quietly scraping something else. Note this replaces the search term, locations and board list only; a preset and your filters still apply on top. - Keyword × location matrix —
locationsruns every search term against every location and merges the results, which is how you get past a board's per-query result cap (one query for "United States" returns one capped page; five city queries return five). - Deduplicated, and it tells you — the same job on multiple boards becomes one row carrying
found_on_sites(every board it appeared on) andduplicate_count. Company names are normalized before matching (legal suffixes stripped), so Wipro on one board and Wipro Limited on another collapse into one billed row. Being live on three boards is a real hiring-urgency signal. Within a single board two rows are merged only when they are the same listing (samejob_url, else sameid), so a company advertising six distinct Software Engineer II roles on Indeed gives you six rows, not one. Readduplicate_countas how many listings in this run shared this title and company, not as a count of rows we deleted: across boards those extras really were merged into the one billed row, while on a single board they each kept their own row, so the same number can legitimately appear on several rows. - Filters that never bill — salary-required, job type, remote-only, title-keyword excludes, company excludes, company includes (competitor hiring watch), experience level, city excludes, staffing-agency excludes, strict keyword match and max posting age all run after dedup and before billing; a filtered row is never charged, and
RUN_SUMMARY.filter_removedsays exactly how many each filter took. - Every job scored for keyword fit — each row carries
matched_keywordsandkeyword_match_percent, scored against yourresumeKeywordswhen you give them and against the run's own search terms otherwise. Pure annotation: it never removes a row, never changes the bill, andsortBy: relevanceputs the best matches on top. - Comparable salaries — hourly, weekly and monthly pay is normalized into
salary_min_annual/salary_max_annual(hourly ×2080, weekly ×52, monthly ×12), so one column sorts every salaried row. - Resilient — a slow or blocked board is skipped on a timeout so the run still finishes with everything the others returned. No all-or-nothing failures. And a run that runs out of time is stopped by the actor, a safe margin before the platform's own limit: if the run had already assembled its rows for delivery they are pushed to your dataset (once — never twice, so nothing is billed twice), and the status message says what the run was doing when it stopped, instead of the run being killed at the timeout with everything thrown away (
RUN_SUMMARY.status: "stopped-at-time-limit"). A run stopped before it got that far delivers nothing and bills nothing, and says so. - Honest per-board reporting —
RUN_SUMMARYnames the board and the actual HTTP error, instead of a silent empty result. - Multi-search — pass several search terms in one run; results are merged and deduped, and each row records which term matched it.
- Clean text — descriptions are converted to real Markdown without the stray backslash escapes (
full\-time,Web3 \| NYC) that the underlying library emits by default. - Export anywhere — CSV, JSON, Excel, or pipe to Google Sheets / your CRM.
How to use it
The shortest complete run is a search term and a location: LinkedIn, Indeed, Glassdoor and The Muse are searched at 20 job postings per board with full LinkedIn job details on and Apify's datacenter proxy, and an untouched form pressed on 2026-08-08 delivered 60 rows in 27 seconds (LinkedIn 20, Indeed 20, Glassdoor 20). Everything below is optional.
- (Shortcut) Already have the search open on LinkedIn, Indeed or Glassdoor? Copy the URL into Search URLs and hit Save & start — everything below is filled in from the URL.
- Enter a search term (job title/keywords) and a location (or several under Locations, which runs every term against every location).
- Keep the default boards (LinkedIn, Indeed, Glassdoor, The Muse), and set Max job postings per board. For remote searches, turning on Remote jobs only auto-adds the seven remote-only boards; DevITjobs US/UK are the only working boards you must add yourself.
- Full LinkedIn job details is on by default — that's where LinkedIn descriptions,
job_typeandcompany_industrycome from. Turn it off only for faster runs. - Optionally filter: remote-only, job type, posted-within-N-hours, distance, country — plus the post-scrape filters below (salary required, title/company excludes, max posting age), which are never billed.
- Run → get a clean, deduplicated jobs table.
Input
The Console form is grouped into sections that go from loudest to quietest — Quick start, What to search, Paste a job-search URL, Filters & limits, Board-specific options, Output, and a deliberately plain Advanced (networking). The optional plumbing lower down is grouped at the bottom on purpose, because a default run needs none of it.
| Field | Type | Description |
|---|---|---|
preset | string | Optional goal shortcut: salary_research, competitor_intel, remote_only, fresh_postings, recruiter_leads. A preset only fills options you left at their default — anything you set yourself wins — and everything it changed (and everything it refused to change) is logged and recorded in RUN_SUMMARY.preset_applied. Empty (the default) is a strict no-op. |
roleSelect | string | Optional quick-pick job title. Precedence: searchTerms > roleSelect > searchTerm. Sent to the boards as plain keywords — it is not a fixed taxonomy. Empty = use searchTerm. |
citySelect | string | Optional quick-pick metro. Precedence: locations > citySelect > location. Picking a non-US metro also sets countryIndeed to the matching country unless you set that field yourself. Empty = use location. |
searchUrls | array | Paste LinkedIn / Indeed / Glassdoor search URLs and skip the form. Replaces searchTerm, locations and sites when set. See the table of honored parameters below. |
atsCompanies | array | Watch companies: for each name the actor finds that company's own careers board (Greenhouse, Lever, Ashby, Recruitee, BambooHR or Workable — all keyless, tried in that order; see Can I scrape a specific company's careers page? below) and merges its live openings into the same 54-column table. Leave the search term at its default and you get each company's whole board; set your own term and only matching roles are delivered and billed. Capped at 20 companies per run. |
onlyNewJobs | boolean | Monitoring mode: remembers every job this exact search has already delivered (in a private named store in your own account) and delivers — and bills — only postings it has not seen before. Identical reruns bill zero (measured: 15 rows, then 0). Memory is kept per search; a posting still live is re-stamped on every run that delivers rows (the memory is only written when something new lands), and the 90-day prune runs at that same write, so it only forgets postings that disappeared — long-lived postings are never re-billed. If the memory cannot be read, the run stops and charges nothing rather than re-deliver the baseline. The memory key is terms + locations + boards (+ remote / hoursOld / atsCompanies when set); post-scrape filters such as excludeTitleKeywords / requireTitleKeywords are not part of it, so toggling them never re-baselines a watch. |
webhookUrl | string | Optional alert webhook: when rows are delivered, POST a digest to this URL — Slack incoming webhook or Discord webhook gets a message, any other URL (n8n / Make / Zapier catch hook) gets JSON with counts, console links and the first 20 rows. Quiet runs send nothing. See Alerts below. |
searchTerm | string | Job title / keywords (e.g. software engineer). |
searchTerms | array | Multiple searches in one run (merged + deduped); overrides searchTerm. Capped at 5 — extras are reported in the run status, never dropped silently. |
location | string | City / state / country (e.g. New York, NY). Empty = anywhere. |
locations | array | Several locations in one run; every search term is run against every location. Replaces location. Capped at 10 locations and 50 term × location searches; caps are reported in RUN_SUMMARY. |
sites | array | Boards to scrape. Defaults to linkedin, indeed, glassdoor, muse; the other keyless boards (remotive, jobicy, himalayas, hn_hiring, remoteok, weworkremotely, working_nomads, devitjobs_us, devitjobs_uk) are opt-in — the seven remote-only ones auto-join when Remote jobs only is on and this list is left at its default. Unrecognised names are reported, not silently ignored. |
maxResults | integer | Job postings requested per board (1–500, clamped). 0 and negatives clamp to 1 rather than falling back to the default. |
isRemote | boolean | Asks each board for its own remote filter, then drops any row whose own is_remote is false before billing — LinkedIn accepts f_WT and ignores it, so the board-side filter alone is not enough. Rows with no description carry no evidence and are kept. With the board list left at its default it also auto-adds the seven remote-only boards, which adds rows and raises the bill. |
jobType | string | Full-time / part-time / internship / contract. Only Indeed and Glassdoor filter this board-side (LinkedIn and the keyless API boards ignore it), so rows whose own job_type contradicts your choice are dropped before billing. A posting with no job_type at all is kept. |
hoursOld | integer | Only jobs posted in the last N hours (board-side filter). Above 8760 (one year) the age limit is dropped and your other board-side filters are kept — it does not silently strip easy-apply/job-type/remote the way any other non-zero value must. Any board-side filter a board refuses to combine with it is dropped for that board only and named in RUN_SUMMARY.board_filters_dropped. |
maxAgeDays | integer | Drop postings older than N days. Applied by the actor after scraping, so it works on every board; filtered rows are never billed. |
requireSalary | boolean | Keep only rows carrying a salary figure. Filtered rows are never billed. |
excludeTitleKeywords | array | Drop jobs whose title contains any of these words/phrases (case-insensitive). Filtered rows are never billed. |
requireTitleKeywords | array | Keep only jobs whose title contains at least one of these words/phrases (case-insensitive substring, title only) — the mirror of excludeTitleKeywords, for boards that match your search term against descriptions. Filtered rows are never billed, and the onlyNewJobs memory is untouched either way. |
excludeCompanies | array | Drop these companies. Names are normalized first, so Wipro also excludes Wipro Limited. Filtered rows are never billed. |
targetCompanies | array | Keep only these employers (competitor hiring watch). Same normalization, plus a parent match: Amazon keeps Amazon Web Services but not Amazonia. Filtered rows are never billed. |
experienceLevel | array | internship / entry / associate / mid_senior / director / executive. Best-effort — see the note below. Filtered rows are never billed. |
excludeCities | array | Drop jobs whose location mentions any of these places (whole-word, case-insensitive). Filtered rows are never billed. |
excludeAgencies | boolean | Drop rows whose company name looks like a staffing agency or recruiter. Heuristic; see the note below. Filtered rows are never billed. |
strictKeywordMatch | boolean | Keep only rows whose title or description contains your search words. Off by default. Use it for narrow roles — LinkedIn never answers "nothing matched" and returns loosely related postings instead. Filtered rows are never billed. |
resumeKeywords | array | Score every row against your skills instead of the default search-term scoring — matched_keywords and keyword_match_percent are filled either way. Never filters, never changes the bill. |
sortBy | string | relevance, date_desc or salary_desc. Applied before the charge cap. |
countryIndeed | string | Country for Indeed & Glassdoor (e.g. usa, uk, india). Leave it alone and a location naming a country (Berlin, Germany) points Indeed at that country's site automatically. A spelling we don't recognise falls back to usa with a note in RUN_SUMMARY — it never fails the run. |
distance | integer | Search radius in miles. LinkedIn and Indeed honor it; Glassdoor and the API boards ignore it. |
offset | integer | Skip the first N job postings per board, to page past a run you already have. LinkedIn, Indeed and Glassdoor page server-side; on the keyless API boards the Actor fetches N extra rows and hands you the tail. Boards order results their own way, so it is not a stable cursor. |
easyApply | boolean | LinkedIn/Indeed direct-apply jobs only. It cannot be combined with hoursOld: when both are set it is dropped for LinkedIn and Indeed, the run log says so and RUN_SUMMARY.board_filters_dropped records it. Unlike jobType / isRemote, which Indeed also drops under hoursOld but the actor then enforces after scraping, there is no post-scrape easy-apply check — the rows come back unfiltered. |
linkedinFetchDescription | boolean | On by default. Fetches each LinkedIn job's own page — the source of LinkedIn descriptions, job type and industry. Turn off for faster runs. |
descriptionFormat | string | markdown or html. |
descriptionHtml | boolean | Also emit description_html (the board's original HTML) alongside description. Both in one run, no extra requests. |
includeCompanyDetails | boolean | Kept for compatibility — since v0.1.48 the company block (including company_ceo, company_banner, company_addresses_all and company_details_filled) ships on every run automatically, so this toggle changes nothing. No extra requests either way. |
proxyConfiguration | object | Proxy settings. Datacenter is the default and what you want (measured 2026-08-11: 96% of the rows at ~1/5 the cost); switch to Residential only if a board starts coming back short. |
Example input:
{"searchTerm": "data analyst","location": "Austin, TX","sites": ["linkedin", "indeed", "glassdoor"],"maxResults": 50,"hoursOld": 168,"requireSalary": true,"excludeTitleKeywords": ["senior", "intern"]}
Paste-a-URL input:
{"searchUrls": ["https://www.indeed.com/jobs?q=python+developer&l=Austin%2C+TX&fromage=14&radius=25","https://www.linkedin.com/jobs/search?keywords=python%20developer&location=Austin%2C%20Texas&f_TPR=r604800"],"maxResults": 25,"resumeKeywords": ["Python", "AWS", "Kubernetes"],"sortBy": "relevance"}
Multi-city + competitor watch input:
{"searchTerms": ["data analyst", "business analyst"],"locations": ["Austin, TX", "Chicago, IL", "Denver, CO"],"maxResults": 30,"targetCompanies": ["Deloitte", "Accenture", "Capgemini"],"includeCompanyDetails": true,"sortBy": "date_desc"}
Can I scrape a specific company's careers page?
Yes. List the company names in Watch companies (atsCompanies) and the Actor reads each one's own careers API directly — Greenhouse, Lever, Ashby, Recruitee, BambooHR or Workable, all keyless — and merges the live openings into the same 54-column table, up to 20 companies per run.
A job board is a search; a company's careers board is an inventory. For every name in atsCompanies the actor turns it into a slug ("Stripe Inc." → stripeinc), asks each ATS below for that slug in this order, and the first one that answers with postings wins — a company lives on one ATS. A company on none of them is named in the log and RUN_SUMMARY and costs nothing. maxResults caps the rows per company, newest first. Up to 20 companies per run.
Recruitee, BambooHR and Workable were added on 2026-08-29 and verified from a residential IP rather than the Apify datacenter pool; Greenhouse, Lever and Ashby came first. A company on none of the six is named in the log and RUN_SUMMARY and costs nothing.
| ATS | What is read | Posting date | Description |
|---|---|---|---|
| Greenhouse | boards-api.greenhouse.io/v1/boards/{slug}/jobs | first_published (updated_at ships separately as date_updated) | in the feed; a placeholder location label (N/A, TBD) ships as null, not as a place |
| Lever | api.lever.co/v0/postings/{slug} | createdAt | in the feed |
| Ashby | api.ashbyhq.com/posting-api/job-board/{slug} | publishedAt | in the feed; pay range where the company publishes one |
| Recruitee | {slug}.recruitee.com/api/offers/ | published_at | in the feed (description + requirements); salary only when the company publishes it |
| BambooHR | {slug}.bamboohr.com/careers/list | on each posting's detail page only | on each posting's detail page only |
| Workable | apply.workable.com/api/v3/accounts/{slug}/jobs | published | on each posting's detail page only |
The three added on 2026-08-29 (Recruitee, BambooHR, Workable) were verified from a residential IP, not from the Apify datacenter pool — if Workable's edge walls datacenter exits, that company simply reads "not found" and costs nothing. BambooHR's list carries no date and no description, and Workable's carries no description: those come from one extra request per posting, opened newest-first for up to min(40, 3 × maxResults) postings per company while Full LinkedIn job details is on (its default). Postings past that cap still ship — with date_posted null (BambooHR) or description null — and undated rows pass the freshness filters, because staleness cannot be proven. Workable boards larger than one page have not been measured; the actor reads what one call returns.
Company watch on a schedule — the input that pays for itself:
{"atsCompanies": ["Stripe", "bunq", "Softheon"], "sites": [], "onlyNewJobs": true, "maxResults": 100, "webhookUrl": "https://hooks.slack.com/..."}
sites: [] skips the job-board search entirely (ATS-only run); leave searchTerm untouched for the whole board, or set one to keep only matching titles and descriptions. Save, then Actions → Schedule (daily is plenty — these boards change a few times a week). The first run seeds the memory; every later run delivers and bills only postings not seen before. The memory key is each posting's own URL, so a posting the company re-saves is not re-billed, and the search's memory key is built from the company names you typed — adding the three new providers changed nothing for schedules that already exist.
Can I paste a LinkedIn, Indeed or Glassdoor search URL instead of filling in the form?
Yes. Paste it into searchUrls and it replaces searchTerm, locations and sites for the run — LinkedIn, Indeed (any country sub-domain) and Glassdoor URL shapes are accepted, and each URL scrapes only the board it came from.
Build the search on the board's own site, copy the URL, paste it in. A LinkedIn URL runs LinkedIn only. Anything the URL contains that is not in this table (geoId, tracking ids, currentJobId, saved-search ids) is ignored.
Verified 2026-08-08: an Indeed URL and the equivalent manual inputs returned the same 10 job ids and the same output columns, so the URL is a front door onto the same pipeline, and a Glassdoor slug URL returned 8 rows while a keyword-less glassdoor.com/Job/jobs.htm URL was rejected with a readable reason.
| Board | URL shapes accepted | Parameters honored |
|---|---|---|
linkedin.com/jobs/search?keywords=… | keywords → search term, location, distance (miles), f_WT=2 → remote only, f_TPR=r<seconds> → posted-within, f_JT → job type, f_E → experience level, f_C → company ids, sortBy (DD/R), start → offset. Caveat (measured): f_WT and f_JT are read and forwarded, but LinkedIn's guest search ignores both, and a pasted URL by itself does not switch on the post-scrape remote / job-type gate — set Remote jobs only / Job type in the form next to the URL and the contradicting rows are dropped before billing | |
| Indeed | indeed.com/jobs?q=… (any country sub-domain, e.g. ca., uk., de.) | q → search term, l → location, radius, fromage (days) → posted-within, sc attr(DSQF7) → remote, sc jt(…) → job type, explvl → experience level, sort=date, start → offset, and the country from the sub-domain |
| Glassdoor | glassdoor.com/Job/…-SRCH_IL.<a>,<b>_IC…_KO<c>,<d>.htm or …/Job/jobs.htm?sc.keyword=… | sc.keyword / keyword (or the KO slug offsets) → search term, locKeyword / locName (or the IL slug offsets) → location, fromAge (days), remoteWorkType=1 → remote, jobTypes, seniorityType → experience level, sortBy, and the country from the domain suffix. radius is not read — Glassdoor's scraper has no distance parameter, so a radius in the URL changes nothing (only LinkedIn and Indeed honor distance) |
A URL we cannot parse is skipped with a reason, never guessed at: the other URLs still run and RUN_SUMMARY.search_url_errors names the URL and what was missing. If none of them parse, the run scrapes nothing and bills nothing.
Each URL's filters stay on that URL. A seniority filter carried in one URL (f_E, explvl, seniorityType) is applied only to the rows that URL returned — pasting an executive-only LinkedIn URL next to an unfiltered Indeed URL does not thin out the Indeed results. Those rows are removed before de-duplication and therefore before billing, and the count shows up in RUN_SUMMARY.filter_removed as experienceLevel (from search URL), with the per-URL levels in RUN_SUMMARY.search_url_experience_levels. Setting the experienceLevel input explicitly overrides every URL's own level filter.
Verified locally on 2026-08-08: an Indeed URL and the equivalent manual inputs (searchTerm + location + hoursOld: 336 + distance: 25) returned the same 10 job ids and the same output columns — the URL path is a front door onto the same pipeline, not a different one. A Glassdoor SRCH_…KO… slug URL returned 8 rows; glassdoor.com/Job/jobs.htm (no keyword anywhere) was rejected with a readable reason while the other URL still ran.
Keyword × location matrix (locations)
locations runs every search term against every location and merges the results into one deduplicated table. It is the way past a board's per-query cap: one query for a whole country returns one capped page, five city queries return five. Every pair asks each board for maxResults rows, so cost scales with the number of pairs — capped at 10 locations and 50 pairs per run, and both caps are reported (never applied silently). Rows carry matched_search_term and, whenever a run covers more than one location, matched_location.
Live run (2026-08-08, data analyst × [Austin, TX, Chicago, IL] × LinkedIn + Indeed + Glassdoor at 12/board): 72 raw rows → 67 after cross-search dedup → 63 billed after filters, split 28 Austin / 35 Chicago.
Post-scrape filters — filtered rows are never billed
These filters run inside the actor after scraping and deduplication and before billing, so a filtered row is never charged. RUN_SUMMARY.filter_removed records how many rows each one took.
requireSalary— keep only rows carrying a salary figure (salary_minorsalary_max).jobType— also enforced here, not just board-side. Indeed and Glassdoor filter employment type themselves; LinkedIn's guest search accepts the parameter and returns the same rows anyway, and the keyless API boards have no such filter at all. So a row whose ownjob_typecontradicts your choice is dropped before billing. A posting the board published with nojob_typeis kept — silence is not a contradiction. Measured 2026-08-08:jobType: internshipon Remotive + Jobicy + Himalayas dropped all 35 mismatching rows and billed 0; on LinkedIn + Indeed it dropped LinkedIn's 10 full-time rows and billed 10 rows that all carryinternship.isRemote— also enforced here for the same reason: LinkedIn ignoresf_WT. Rows whose ownis_remoteisfalseare dropped before billing; rows with no description carry no evidence and are kept. Measured 2026-08-08 on the Remote-only sweep preset: 19 rows dropped, and 0 of the billed rows hadis_remote: false(before this, 10 of 10 LinkedIn rows billed under that preset were on-site jobs in Midland TX, El Paso TX and Pittsburgh PA).strictKeywordMatch— opt-in, off by default. Keep only rows whose title or description contains your search words. LinkedIn never answers "nothing matched"; it degrades to loosely related cards. Measured 2026-08-08:underwater welderin Fargo, ND billed 10 rows of marine techs, trenchless engineers and welders with it off, and 0 with it on. Either wayRUN_SUMMARY.rows_not_mentioning_search_termcounts the off-topic rows per board (that run:{"linkedin": 10}), so you can see the problem before deciding to pay for it.excludeTitleKeywords— drop titles containing any listed word or phrase (case-insensitive substring match).requireTitleKeywords— the mirror: keep only titles containing at least one listed word or phrase (case-insensitive substring match, title only). Built for the Software Engineering Intern problem, where a board matches the search term against descriptions and returns full-time roles that merely mention interns;RUN_SUMMARY.filter_removed.requireTitleKeywordscounts what it removed. Neither title filter is part of theonlyNewJobsmemory key — they only remove rows before delivery, and undelivered rows are never marked seen — so adding one to a scheduled watch never re-delivers or re-bills the baseline.excludeCompanies— drop listed companies; names are normalized before matching, soWiproalso excludesWipro Limited.targetCompanies— the inverse: keep only the listed employers. Same normalization plus a parent match, soAmazonkeepsAmazon Web Serviceswhile leavingAmazoniaout. This is the competitor-hiring-watch input.maxAgeDays— drop postings older than N days. UnlikehoursOldthis is applied by the actor itself, so it works on every board.excludeCities— drop jobs whose location text mentions a listed place. Whole-word and case-insensitive, matched against the location exactly as the board published it —DallasdropsDallas, TX,Dalldrops nothing. Best-effort: boards format locations inconsistently.excludeAgencies— drop rows whose company name matches a staffing/recruiting pattern (staffing,recruit*,headhunt*,talent*,manpower,personnel,placement*,consultanc*,resourcing,workforce,temp agency,employment agency,search partners/group/associates,HR solutions,staff augmentation). It reads the name only, so a re-poster with a neutral name gets through and a genuine employer withWorkforcein its name gets dropped — useexcludeCompanieswhen you need exact control.experienceLevel— best-effort, and here is exactly how. When a board publishes a seniority field we use it (LinkedIn does with detail fetching on — measured 19/19 LinkedIn rows; Jobicy, Himalayas and The Muse also publish one). Otherwise we read it off the title:Senior/Sr./Staff/Principal/Lead→ mid-senior,Junior/Jr./Graduate/Entry level→ entry,Intern→ internship,Director→ director,VP/Chief/Head of→ executive. A job whose level we cannot determine is kept, not dropped — we only remove rows we can prove don't match. On a live 63-row run the level came from a board field on 19 rows, from the title on 16, and was genuinely unknown on 28.
Verified on live runs: requireSalary removed 7 rows and excludeTitleKeywords 3 (2026-08-07); excludeAgencies removed 4 rows and experienceLevel 1 (2026-08-08), all recorded in RUN_SUMMARY.filter_removed, and a targetCompanies value matching nothing ended the run with 0 rows pushed and nothing billed. excludeAgencies reads the company name only, so treat it as a heuristic rather than a measurement: across the 178 distinct employers returned by the 2026-08-08 local runs it flagged none, which tells you how few agency reposts a given search contains, not that it is precise. Its known failure mode is the workforce token — Texas Workforce Commission and Workforce Software are both flagged, and both are genuine employers posting their own jobs. Use excludeCompanies when you need exact control.
Keyword scoring (matched_keywords / keyword_match_percent) and sortBy
Every row is scored for keyword fit, at no extra request and no extra cost:
- With
resumeKeywordsset (Python,Kubernetes,SOC 2,C++,.NET,node.js— case-insensitive substring, so punctuation works as written):matched_keywordslists which of your keywords appear in the title or description, in your order, andkeyword_match_percentis the share of them matched (0–100). Measured on a 63-row live run: 8 rows at 100%, 17 at 75%, 15 at 50%, 8 at 25%, 15 at 0%. - Without it (the default), the same two columns score the row against the run's own search terms, using the same tokenizer the actor's keyword gate uses — so
keyword_match_percentandRUN_SUMMARY.rows_not_mentioning_search_termcan never disagree about what "mentions the search term" means.matched_keywordslists which words of your best-matching term the row actually mentions (empty at 0%), and a search term that is all stopwords (remote jobs) leaves both columns null, because there is nothing to score against.RUN_SUMMARY.keyword_match_scored_againstnames which set scored the run.
Honesty note: until v0.1.48 (2026-08-22) these columns filled only with resumeKeywords, which no run had ever set — so they were null on 100% of the 5,936 rows this actor had ever delivered, while the actor computed the same match internally and threw it away. Back-tested by re-scoring those 5,936 delivered rows with the shipped function: 89.4% score 100, 8.9% partial, 1.8% score 0 — the 0% bucket being exactly the board drift the score exists to expose ("Full Stack Developer" billed for a "software engineer" search).
Scoring is annotation only: no row is removed and the bill is unchanged. sortBy: relevance ranks by match percent, then by how many boards the job was found on, then by date. sortBy: date_desc and salary_desc sort on posting date and on the annualized salary; rows with no value for the key always land last, and sorting runs before the charge cap so a truncated run keeps your top rows.
Output fields and how often they are filled
Every row has the same 54 columns
One column tuple per run — and the same tuple in every run. Every row the Actor delivers carries all 54 columns below, in the same order, whatever board it came from and whatever options you set. A field the board did not publish is null, never missing. That is what makes the CSV safe to open in a spreadsheet, pd.read_csv without dtype surprises, and parse by column position.
This changed on 2026-08-08. Rows used to have their empty fields stripped out before delivery, so a column existed on the rows one board filled in and silently vanished from the rest — a LinkedIn + Indeed + Glassdoor run of this shape once shipped 10 different column tuples ranging from 20 to 31 columns. Since the fix, every row of a run carries the same column tuple, however many rows come back and whatever boards fill them. A live default LinkedIn + Indeed + Glassdoor run delivered one tuple of 50 columns on every row (the tuple of the day — four more were appended at the end on 2026-08-29, see below) — the row count is just whatever the boards return that day (57 the first time this was measured, 60 on the latest run) — and separate runs across the five API boards and an Indeed-only run with every option switched on carried that same tuple in the same order.
The column order, which is stable and safe to depend on:
id, title, company, location, job_url, job_url_direct, site, job_type, is_remote, date_posted, salary_source, salary_min, salary_max, salary_currency, salary_interval, salary_min_annual, salary_max_annual, job_level, job_function, listing_type, company_industry, company_url, company_url_direct, company_addresses, company_description, company_num_employees, company_revenue, company_rating, company_reviews_count, company_logo, description, emails, skills, experience_range, vacancy_count, work_from_home_type, found_on_sites, duplicate_count, search_term, matched_search_term, scraped_at, description_html, company_ceo, company_banner, company_addresses_all, company_details_filled, matched_keywords, keyword_match_percent, matched_location, source_search_url, date_updated, applicants, applicants_is_bound, salary_text.
The last four were appended on 2026-08-29 (at the end, so the first 50 positions are exactly what they were). date_updated is Greenhouse's updated_at — the board bumps it when a posting is re-saved without changing, so it is no longer used as date_posted (that now reads first_published); null on every other board. applicants is LinkedIn's applicant caption read off the job page the run already fetches when Full LinkedIn job details is on (the default), and applicants_is_bound is true when that figure is a bound rather than a count ("Over 200 applicants", "Be among the first 25 applicants"); both null on other boards, when the page shows no caption, or with detail fetching off. No extra request for any of the three. salary_text, appended the same day, is the four salary columns rendered as one readable string ($158,100–$213,800 / year, $45 / hour); it is derived from the row's own salary_min / salary_max / salary_currency / salary_interval and is "" when the board published no amount — see How to read the output.
The last nine are feature-driven, and since v0.1.48 most fill without any toggle. company_ceo, company_banner, company_addresses_all and company_details_filled fill automatically on the rows Indeed published them for (includeCompanyDetails is kept for compatibility and changes nothing). matched_keywords / keyword_match_percent fill on every scorable row — against your resumeKeywords when given, otherwise against the run's own search terms (null only when every search term is stopwords, e.g. remote jobs). Two columns are genuinely conditional: description_html fills only with descriptionHtml: true and source_search_url only on a searchUrls run — null there means "you did not use that feature", never "we could not get it". matched_location fills only when a run covers more than one location, for the same reason. The column is there either way. A column that appears and disappears with a setting is the same ragged-CSV problem in miniature, so we don't do that. New fields are only ever appended to the end of this list, never inserted, so a positional parser keeps working.
Two things that can still hand you a narrower row, both of them your own choice: ?fields= / ?omit= on the Dataset API, and clean=true (a shortcut for skipHidden=true + skipEmpty=true), which strips empty values back out. Plain format=csv, format=xlsx, format=json and the Console Export button all give you the full 54.
How often is each field actually filled?
title, site, job_url, is_remote, date_posted, found_on_sites and duplicate_count are filled on 100% of rows; company on 95-98% and location on 98-100%; description on 100% of rows with LinkedIn detail fetching on (the default) and 64% with it off; a salary figure on 59-70%. The rest depends on which board the row came from, and the table below gives the measured rate for every column.
Job boards publish wildly different amounts of detail, so most fields are conditional — the column is always present, but how often it carries a value depends entirely on the board. These are measured fill rates, not aspirations — taken from live runs on 2026-08-07 (58–85 rows for the detail-off column, 67–72 rows with LinkedIn detail fetching on). Ranges span two independent runs on different queries; expect your own numbers to land inside them. Rows marked all-time were re-measured 2026-08-23 across every row this actor has ever delivered (5,936 rows, 83 non-empty runs) — for single-board fields that is the honest scope, because the per-run percentage is really just that board's share of that day's run.
LinkedIn detail fetching is now on by default, so the right-hand column is what a default run delivers — a live default run ({}) returned 60 rows with descriptions on 20 of 20 LinkedIn rows. The left column is what you fall back to if you turn it off for speed.
| Field | Detail fetch off | Detail fetch on (default) | Comes from |
|---|---|---|---|
id, title, site, job_url, is_remote, date_posted, scraped_at, search_term, matched_search_term, found_on_sites, duplicate_count | 100% | 100% | all boards |
company | 98% | 95% | all boards |
location | 98% | 100% | all boards |
company_url | 97% | 94% | all boards |
description | 64% | 100% | Indeed + Glassdoor always; LinkedIn only with detail fetching |
salary_min, salary_max, salary_interval, salary_currency, salary_source | 56–60% | 59–70% | Glassdoor 95–100%, Indeed 50–80% (varies by query), LinkedIn none without detail fetching and ~35% with it |
company_logo | 49% | 86% | Glassdoor + Indeed; LinkedIn with detail fetching |
listing_type | 11.4% all-time | 11.4% all-time | Glassdoor only — 100% of Glassdoor rows (676 of 676 ever delivered), value always sponsored or organic. The dataset-wide rate is just Glassdoor's share of a run; an earlier version of this table said 32%, measured on two Glassdoor-heavy runs |
job_url_direct | 31% | 33% | Indeed only (the employer's own posting URL) |
company_rating | 10.1% all-time | 10.1% all-time | Glassdoor only — 88.5% of Glassdoor rows (598 of 676 ever delivered; Glassdoor sends no rating for unrated employers). An earlier version of this table said 29–30%, measured on two Glassdoor-heavy runs |
job_type | 24% | 65% | Indeed ~80%; LinkedIn 100% only with detail fetching; never from Glassdoor |
company_url_direct, company_addresses, company_num_employees, company_revenue, company_description | 17–20% | 19–23% | Indeed only (~55–70% of Indeed rows) |
job_level, job_function | 0% | 34% | LinkedIn only, and only with detail fetching (then 100%) |
company_industry | 2% | 38% | LinkedIn with detail fetching (100% of LinkedIn rows); Indeed ~10% |
emails | 5–8% | 12–14% | scraped out of description text when a job lists one |
applicants, applicants_is_bound (appended 2026-08-29) | 0% | varies by category/board | LinkedIn only, and only with detail fetching — filled when the job page shows an applicant caption; not yet measured on a live run |
date_updated (appended 2026-08-29) | varies by category/board | varies by category/board | Greenhouse (atsCompanies) only — its updated_at; null on every other board |
linkedinFetchDescription is on by default, so job_type, company_industry, job_level and job_function arrive without touching anything. Turn it off and LinkedIn contributes none of them — that trade is yours to make for speed.
Two derived columns normalize pay so every row sorts on one axis: salary_min_annual / salary_max_annual convert hourly (×2080), weekly (×52) and monthly (×12) figures to annual. In a live default run they were present on 57 of 57 rows that carried a salary. Two guards keep them honest. When enforceAnnualSalary is on, the scraping library annualizes description-parsed pay itself but leaves salary_interval reading hourly/monthly — we rewrite that interval to yearly instead of multiplying a second time, which is what used to ship salary_min_annual: 95180800 on a warehouse job and print $45,760 / hourly in the raw columns. And a result that cannot be a wage is left null rather than published; the raw salary_min / salary_max are never touched. That backstop is applied only to USD-scale currencies, so a real 3,000,000 KRW or 10,000,000 IDR monthly salary still annualizes normally. Measured 2026-08-08 on the salary_research preset (warehouse associate, Dallas TX, Indeed + LinkedIn): 0 of 14 billed rows carried an impossible annual figure and 0 claimed an hourly rate above $1,000/hour — the same run shape previously produced 9 of 13. The three salary qualifier columns (salary_interval, salary_currency, salary_source) are blank on any row without an actual amount — a qualifier with nothing to qualify is noise. Verified on the 2026-08-08 runs: 0 of 57 and 0 of 69 rows carried an orphan qualifier.
Three columns are genuinely unavailable upstream: experience_range, vacancy_count and company_reviews_count are only ever published by Naukri, which is CAPTCHA-blocked — measured 0 of the 5,936 rows this actor has ever delivered. They stay in the row for parser compatibility only; expect them to be blank and do not plan a project around them. skills is not in that group: Remote OK, DevITjobs US/UK and Working Nomads publish it on 100% of their rows (103 of 5,936 rows ever delivered, 1.7% — the dataset-wide rate is just those boards' share of your run; on the default boards it is blank). And work_from_home_type left the blank list on v0.1.48: Indeed publishes the work arrangement in a response the actor already fetches, so Indeed rows now carry Remote / Hybrid / Work from office where the posting was labelled — measured live on 600 Indeed listings: 27.8% carry a label (14.5% Remote, 12.7% Hybrid, 0.7% In person); unlabelled postings stay null rather than guessed.
The company block — what you actually get, per board
Since v0.1.48 this block ships on every run: includeCompanyDetails is accepted for compatibility and changes nothing (the run log says so when you set it). Measured 2026-08-22: the toggle was on in 0 of this actor's 85 real runs, so company_ceo, company_banner, company_addresses_all and company_details_filled had been null on 100% of rows ever delivered — while the values were being fetched from every Indeed response and thrown away. Nothing is invented: company_details_filled counts how many company fields the row actually got, and the three Indeed extras (company_ceo, company_banner, company_addresses_all — the full address list; company_addresses only ever held the first) are fields Indeed already sends and the underlying library discards. No extra requests, no extra cost per row.
Measured on a live 63-row run (2026-08-08, LinkedIn + Indeed + Glassdoor, LinkedIn detail fetching on):
| Field | Indeed rows | LinkedIn rows | Glassdoor rows | Dataset-wide |
|---|---|---|---|---|
company_url | 96% | 100% | 100% | 98% |
company_logo | 71% | 100% | 90% | 86% |
company_industry | 33% | 100% | 0% | 43% |
company_num_employees (size band) | 75% | 0% | 0% | 29% |
company_url_direct (employer's own site) | 75% | 0% | 0% | 29% |
company_addresses / company_addresses_all | 71% | 0% | 0% | 27% |
company_description | 67% | 0% | 0% | 25% |
company_revenue | 58% | 0% | 0% | 22% |
company_ceo (new) | 54% | 0% | 0% | 21% |
company_banner (new) | 54% | 0% | 0% | 21% |
company_rating | 0% | 0% | 95% | 30% |
Read that as: Indeed is the company-data board (size, revenue, description, corporate website, addresses, CEO), LinkedIn contributes the industry (100% of LinkedIn rows, but only with Full LinkedIn job details on — it was 0% in a run with it off), and Glassdoor contributes the employer rating. For the five API boards, which board a row came from decides this entirely, so the per-board answer is the only one worth quoting (measured 2026-08-08 on a 34-row remote-only run across all five):
| Column | Jobicy | Himalayas | The Muse | Remotive | HN |
|---|---|---|---|---|---|
company_industry | 10/10 | 0/7 | 0/10 | 0/2 | 0/5 |
company_logo | 10/10 | 5/7 | 0/10 | 2/2 | 0/5 |
job_level | 10/10 | 7/7 | 10/10 | 0/2 | 0/5 |
job_function | 0/10 | 0/7 | 0/10 | 2/2 | 0/5 |
In words: Jobicy is the only API board that publishes company_industry; Jobicy, Himalayas and Remotive publish company_logo; Jobicy, Himalayas and The Muse publish job_level; Remotive is the only one that publishes job_function; HN publishes none of them. Everything else on these boards is null. Don't read the dataset-wide percentage as a property of the Actor — it is just each board's share of your particular run, and Jobicy is capped at 100 rows per call. If a board doesn't publish a field, the column stays null and says so; we never fill it from somewhere else.
descriptionHtml — both formats in one run
With descriptionHtml: true every row carries description_html (exactly what the board published) alongside the normal description in your chosen descriptionFormat. There is no second request and no price change: the boards return HTML anyway, so the Markdown is produced from that same HTML locally. Use the HTML when you need the original links, lists and headings for your own site or ATS; use the Markdown for LLM prompts and spreadsheets. Budget for the size: description_html holds the raw HTML that the Markdown description is generated from, so it is necessarily about as large as the column beside it. On a 19-row A/B run (2026-08-08, same query with the switch off and on) description_html was filled on 19/19 rows and ran 111% of the description column, growing the whole dataset 86%. An earlier run measured +167% / +132%. Plan on roughly doubling your dataset, as the input form says — not the "about 13%" an earlier version of this README claimed.
JSON output sample
A complete, unedited row from a live Glassdoor scrape (2026-08-08, software engineer / New York NY / LinkedIn + Indeed + Glassdoor, description truncated for length). This is the whole row — all columns, nulls included (50 when it was captured; the four columns appended on 2026-08-29 are shown at the end as they arrive today). Every other row in that run, from every board, had exactly these keys in exactly this order:
{"id": "gd-1009849012184","title": "Software Development Engineer, ML Systems, Annapurna Labs","company": "Annapurna Labs (U.S.) Inc.","location": "New York, NY","job_url": "https://www.glassdoor.com/job-listing/j?jl=1009849012184","job_url_direct": null,"site": "glassdoor","job_type": null,"is_remote": false,"date_posted": "2026-02-28","salary_source": "direct_data","salary_min": 158100.0,"salary_max": 213800.0,"salary_currency": "USD","salary_interval": "yearly","salary_min_annual": 158100,"salary_max_annual": 213800,"job_level": null,"job_function": null,"listing_type": "organic","company_industry": null,"company_url": "https://www.glassdoor.com/Overview/W-EI_IE7470741.htm","company_url_direct": null,"company_addresses": null,"company_description": null,"company_num_employees": null,"company_revenue": null,"company_rating": 3.7,"company_reviews_count": null,"company_logo": "https://media.glassdoor.com/sql/7470741/amazon-web-services-squareLogo-1680754591940.png","description": "**DESCRIPTION** About Amazon Annapurna Labs ...","emails": null,"skills": null,"experience_range": null,"vacancy_count": null,"work_from_home_type": null,"found_on_sites": "glassdoor","duplicate_count": 1,"search_term": "software engineer","matched_search_term": "software engineer","scraped_at": "2026-08-08T15:52:57.101661+00:00","description_html": null,"company_ceo": null,"company_banner": null,"company_addresses_all": null,"company_details_filled": null,"matched_keywords": null,"keyword_match_percent": null,"matched_location": null,"source_search_url": null,"date_updated": null,"applicants": null,"applicants_is_bound": null,"salary_text": "$158,100–$213,800 / year"}
The trailing nulls, read honestly: description_html is null because this run did not set descriptionHtml: true, and source_search_url is null because it did not use searchUrls — null in those two means "you did not use that feature", never "we could not get it". The four company extras are null because this is a Glassdoor row and only Indeed publishes them (since v0.1.48 they fill on Indeed rows automatically, no toggle needed). matched_keywords / keyword_match_percent are null because this row predates v0.1.48 — on the current build this exact row scores "software, engineer" / 100.0 against its own search term. matched_location fills only on a multi-location run. date_updated is null because this is not a Greenhouse row, and applicants / applicants_is_bound are null because it is not a LinkedIn row. salary_text is the row's own four salary columns written out — "" on a row with no published pay. The columns never move, appear or disappear.
Results render as a sortable table on the Output tab and export to CSV, JSON, or Excel.
Example output
A real sample from a live run (software engineer, New York NY, three boards):
| title | company | site | company_rating | salary_min | job_url |
|---|---|---|---|---|---|
| Software Engineer, Systems ML - Compilers | Meta | glassdoor | 3.7 | 154003 | https://www.glassdoor.com/job-listing/j?jl=101021… |
| Silicon Software Lead | Normal Computing Corporation | indeed | https://www.indeed.com/viewjob?jk=76bcf… | ||
| AI Modernization Senior Lead Software E… | JPMorganChase | indeed | 133000 | https://www.indeed.com/viewjob?jk=74806… | |
| DevOps Engineer | OnMed | https://www.linkedin.com/jobs/view/… |
How to read the output
The Output tab opens on Overview; the other views are the same rows with a different column preset. One line each:
- Overview — the first read: logo, title, company, location,
salary_text, remote flag, posted date, source board, match % against your search terms, apply link. - Salary — for comp work: the readable salary next to the raw min / max / currency / period and the yearly-normalized pair that sorts hourly, monthly and yearly on one axis.
- Cross-board — the dedup evidence (
found_on_sites,duplicate_count) plus the demand signals LinkedIn and Greenhouse publish (applicants,applicants_is_bound,date_updated). - Remote & location —
is_remotebeside the location and Indeed'swork_from_home_type(Hybrid / Remote / In person), so you can tell true remote from remote-ish. - Company info — the employer block (industry, size, revenue, rating, reviews, company page, website, address, logo); Indeed fills most of it.
- Standard table — the original 16 columns, for anyone who built on the pre-2026-08-29 default view.
- All columns — all 54 columns of every row, labeled, in delivery order.
Derived columns — appended at the end of every row, always present, "" when unknown, never invented:
salary_text—salary_min/salary_max/salary_currency/salary_intervalas one string with the currency symbol, thousands separators and an en dash:$90,000–$175,000 / year,€60,000–€80,000 / year,$45 / hour;$90,000+ / yearwhen only a minimum was published,up to $175,000 / yearwhen only a maximum. Symbols are used for USD, EUR, GBP, JPY, INR, CAD, AUD and a few more; any other code is written once as a prefix (CHF 90,000–110,000 / year). Empty when the board published no amount.- Dates need no derived column:
date_postedis alreadyYYYY-MM-DD,date_updatedandscraped_atare ISO timestamps.
Exporting just a view. The Dataset API accepts the view key (documented view parameter of Get dataset items): GET https://api.apify.com/v2/datasets/<datasetId>/items?view=salary&format=csv (keys: overview, salary, dedup, remote, company, standard, all). In the Console, select the view above the table, then Export. ?fields= and ?omit= still work for any ad-hoc column pick.
The run's status line reads the same way every time: Done — 57 jobs delivered for “software engineer” in New York, NY. <any honest coverage clause — a board that returned nothing, a charge cap, a dropped term> Report: <link>. Happy with the data? A quick review on the Store page helps a lot! A board that no longer answers from any IP (Google Jobs, ZipRecruiter, Bayt, BDJobs, Naukri) is reported as discontinued upstream. The REPORT record shows the Overview columns under the same labels, with the headline tiles (jobs delivered, boards contributing, cross-board duplicates merged, share with a salary, remote share, new since last run on a monitor).
Use cases
- Recruiting & staffing — pull every open role for a title/location across boards into one pipeline.
- Job boards & aggregators — backfill and keep a niche board fresh from multiple sources.
- Hiring-signal sales — a role live on three boards at once is a company hiring urgently;
duplicate_countandfound_on_sitessurface exactly that. - Labor-market research — salary ranges, remote share, and demand by title and location.
- Personal job hunt — one deduped feed instead of refreshing three sites.
Use with AI agents & automation
Run from the Apify MCP server so AI agents (Claude, ChatGPT, Cursor) can pull jobs as a tool call, schedule runs via Make, n8n, or Zapier to alert on new postings, or sync the dataset to Google Sheets for a live jobs dashboard. Clean flat JSON drops into ATS/CRM pipelines with no glue code.
For an agent, the minimal call is {"searchTerm": "<title>", "location": "<city>"} — every other field has a working default, the output carries the same 54 columns on every row of every run, and the RUN_SUMMARY record in the run's key-value store reports each board's outcome machine-readably. In n8n, use the Apify node with actor flash_scraper/multi-jobboard-scraper (or an HTTP Request node on the run-sync-get-dataset-items endpoint below); in Make and Zapier, use the Apify app's "Run an Actor" module on a schedule.
Job board API, job feed API and job market data
Every run is a job board API call: send a search term and a location, get one deduplicated row per role back as JSON or CSV. That makes it usable as a job feed API behind your own product, and as a source of job market data and employment data for analysis. Rows carry the posting date and, where a board publishes it, the salary, so a series of scheduled runs becomes a salary data set for a role and city. onlyNewJobs on a schedule delivers only new job postings since the last run, which is the cheapest way to track hiring over time. Nothing here needs an API key from the boards themselves.
Use it from an AI agent (MCP)
- The Apify MCP server at
https://mcp.apify.comexposes this actor as a tool named after its Store slug,flash_scraper/multi-jobboard-scraperwith the/rewritten —flash_scraper--multi-jobboard-scraperin the server's current README (Apify's docs page still shows the older-slash-spelling,flash_scraper-slash-multi-jobboard-scraper). Add it explicitly withhttps://mcp.apify.com?tools=flash_scraper/multi-jobboard-scraper, or let the agent reach it through the defaultcall-actortool afterfetch-actor-details; either way the rows land in the run's dataset and the agent reads them withget-dataset-items. - Smallest useful call:
{"atsCompanies": ["bunq"], "sites": [], "maxResults": 5}— one keyless request to that company's own board, rows in well under 60 s (measured locally 2026-08-29), and at most 5 × $0.005 + $0.00005 ≈ $0.03. - Per-result price, read from the live pricing record on 2026-08-29: $0.005 per dataset item on the free plan ($0.0045 Bronze → $0.0035 Diamond), plus $0.00005 per run start; the Pricing tab is always authoritative.
- Every row carries the same 54 columns, so an agent can address fields by name on the first call;
RUN_SUMMARYin the run's key-value store reports each board's outcome machine-readably. - Per the Apify MCP server docs, an MCP client can pay for Actor runs with x402 (USDC on Base) or a Skyfire payment token instead of an Apify API token.
Alerts: get a Slack / Discord / webhook message when new rows land
Set webhookUrl and every run that delivers at least one row POSTs a digest there — no polling, no extra automation. A Slack incoming webhook gets a text message (one line per job, first 20, plus the dataset link); a Discord webhook gets the same as a message; any other URL (n8n / Make / Zapier catch hook, your own endpoint) gets JSON: {actor, delivered, run_url, dataset_url, rows[:20], text}. Quiet runs send nothing. Pair it with onlyNewJobs on a schedule and the actor is a job-alert service on its own. Delivery is best-effort: a webhook that fails or answers non-2xx is reported in the run's status message (and in RUN_SUMMARY.webhook) and never fails the run.
{"searchTerm": "data engineer","location": "Austin, TX","onlyNewJobs": true,"webhookUrl": "https://hooks.slack.com/services/T000/B000/XXXX"}
Every run comes with a report
Every run that delivers at least one row also writes a REPORT record to its key-value store: one self-contained HTML page with the headline numbers (jobs, boards, cross-board duplicates merged, share with a salary), three small charts (jobs per board, top companies, how many boards each job was found on), the first 100 rows in the Overview view's columns under the same labels, and the same honest notes as the run's status message (filters applied, boards that returned nothing, time budget). Find it in the run's Output tab under REPORT, or follow the Report: link at the end of the status message. It is a single file with no scripts and no external assets, so it is safe to forward, attach to an email or screenshot for a client. The dataset stays the source of truth: the report never carries more than the dataset does, and a run that delivered nothing writes no report.
Licensing & attribution — what you may do with the rows
The API boards publish their feeds with strings attached, and those strings pass through to you:
| Board | May you re-post rows on another job board? | Required credit |
|---|---|---|
| The Muse | Yes (attribution requested) | Credit The Muse |
| Remotive | No — their ToS explicitly forbid it | Credit Remotive |
| Jobicy | Only with credit, and apply buttons must link to the original job URL | Credit Jobicy + keep the job_url links |
| HN "Who is hiring?" | User-authored HN content; link back to the HN item | Credit Hacker News / Algolia |
| Himalayas | Yes, with a link-back | Link to himalayas.app |
| Remote OK | Only with a visible, clickable (dofollow) link back to the row's job_url and "Remote OK" named as the source — their terms suspend API access otherwise. Logo use is forbidden | Dofollow link to remoteok.com + name "Remote OK" |
| We Work Remotely | Their terms carry no republication bar; a link-back is good practice | Link to the job_url |
| Working Nomads | No stated bar; link-back is good practice | Link to the job_url |
| DevITjobs US/UK | Undocumented public API — treat rows as pointers and keep the job_url links. ~99% of rows are partner-syndicated (e.g. from Indeed), so salary figures may be partner-estimated | Keep the job_url links |
In plain terms: Remotive rows are fine for lead-gen, research and your own job hunt, but must not be republished on another job board (their terms name Jooble, Google Jobs, LinkedIn and the like explicitly). If you build a job site on Jobicy data, keep the job_url apply links and credit Jobicy. The job_url column already points at each board's original posting, which covers the link-back half of these requirements.
API examples
Same input keys as the Console form, from code:
const { ApifyClient } = require('apify-client');const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });const run = await client.actor('flash_scraper/multi-jobboard-scraper').call({searchTerm: 'data analyst', location: 'Austin, TX', maxResults: 50, onlyNewJobs: true,});const { items } = await client.dataset(run.defaultDatasetId).listItems();
from apify_client import ApifyClientclient = ApifyClient("YOUR_APIFY_TOKEN")run = client.actor("flash_scraper/multi-jobboard-scraper").call(run_input={"searchTerm": "data analyst", "location": "Austin, TX", "maxResults": 50, "onlyNewJobs": True,})items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
curl -X POST "https://api.apify.com/v2/acts/flash_scraper~multi-jobboard-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \-H 'Content-Type: application/json' \-d '{"searchTerm":"data analyst","location":"Austin, TX","maxResults":50}'
Wire the finished-run webhook into n8n, Make or Zapier to land rows in Slack, Sheets or your ATS
automatically - combined with onlyNewJobs on a schedule, that is a self-maintaining job feed.
Only need LinkedIn, as cheap as possible? The same publisher's LinkedIn Jobs Scraper is the single-board budget tool - priced in the Store's cheapest tier.
How much does it cost?
$0.005 per delivered row, so $5 buys 999 rows (1,000 × $0.005 minus the single $0.00005 Actor Start event) — read off the live Store pricing record on 2026-08-29. A visitor on Apify's free plan can run it with no card on file: the plan's monthly free usage credit covers several default-size searches, and a run that delivers nothing (an over-tight filter, a blocked board) costs only the $0.00005 start event.
Pay-per-event. You are charged one Result event per row delivered to your dataset, plus a one-off Actor Start event. The per-result rate on this page (plus that one Actor Start event) is the whole price — no platform-usage surcharge is added to your bill (verified 2026-08-29 against the Store pricing record). Because de-duplication happens before the push, you pay once for a job found on three boards, not three times — and that holds across a keyword × location matrix too, so the same role returned by two city searches is one billed row. Boards that return nothing cost nothing, rows removed by any post-scrape filter (requireSalary, excludeTitleKeywords, requireTitleKeywords, excludeCompanies, targetCompanies, experienceLevel, excludeCities, excludeAgencies, maxAgeDays) are never billed, and failed runs deliver no rows and so bill no results. descriptionHtml fills a column that is otherwise blank, resumeKeywords swaps the keyword-score columns onto your own list instead of the search terms, and the company block ships on every run — none of them ever adds a row, so none of them costs anything extra. See the Apify Store page for current prices.
Cost note for locations: each term × location pair is a full search on every selected board, so 3 terms × 3 locations × 3 boards can bill up to 9× a single search before de-duplication. The caps (10 locations, 50 pairs) exist for exactly that reason and are reported in RUN_SUMMARY, never applied silently.
If you set a maximum total charge on the run and the scrape exceeds it, the actor delivers as many rows as the budget allows and says so in the run status — it will not spend your budget and then hand back an empty dataset.
How it compares
Read from Apify's public Store API on 2026-09-05. Four other multi-board job scrapers: openclawai/job-board-scraper (2,582 users, 5.0 from 3 reviews, $5/1,000, reaches Google Jobs and ZipRecruiter), doggo/uk-jobs-board-scraper (368, 5.0 from 2, $5/1,000 plus a $0.10 run start, UK boards), khadinakbar/jobs-scraper (353, no reviews, $3/1,000) and vero-api (208, 5.0 from 1, $5/1,000, adds Bayt and Naukri). Single-board specialists cost less: curious_coder/linkedin-jobs-scraper is $2/1,000 with 148,567 users. Those figures are each Actor's own Store listing and stats, not a test of it.
This Actor (42 users, 5.0 from 3) is $5/1,000 and covers 12 selectable working boards — 11 of them delivering rows today, since DevITjobs UK was discontinued upstream on 2026-08-29 — with cross-board deduplication and only-new alerts. It cannot reach Google Jobs or ZipRecruiter. If those are required, openclawai's is the one to try.
FAQ
How do I scrape LinkedIn, Indeed and Glassdoor jobs in one run?
Type a search term and a location, leave the board list at its default, and press Save & start — LinkedIn, Indeed, Glassdoor and The Muse are searched in the same run and merged into one 54-column table. An untouched run delivered 60 rows in 27 seconds on the then-default three boards (measured 2026-08-08). Eight more keyless boards can be added under Job boards, and RUN_SUMMARY names what each board actually returned.
Will I pay twice for the same job posted on LinkedIn, Indeed and Glassdoor?
No. A role found on three boards is merged into one row before billing, so you are charged for one delivered row instead of three. The surviving row carries found_on_sites (every board it appeared on) and duplicate_count. Dedup keys on the title plus a normalized company name with legal suffixes stripped, so Wipro and Wipro Limited collapse together. Within one board, two rows merge only when they are literally the same listing — five genuinely different openings at one employer stay five rows.
Measured 2026-08-08 on data analyst / New York, NY at 30 per board across LinkedIn, Indeed and Glassdoor: 90 raw listings became 89 billed rows in 39 seconds, with 1 duplicate merged across boards (Indeed 30, Glassdoor 30, LinkedIn 29).
How much does it cost to scrape 1,000 job postings?
$5 per 1,000 delivered rows — $0.005 each, read off the live Store pricing record on 2026-08-29 — plus a one-off $0.00005 per run start. Duplicates merged across boards bill once. Rows removed by a post-scrape filter bill nothing. A run that delivers nothing bills only the $0.00005 start event. The live rate is always on this page's Pricing tab.
Is there a free way to scrape LinkedIn, Indeed and Glassdoor jobs?
Yes — Apify's free plan needs no card on file, and its monthly usage credit covers several default-size searches. Beyond that the price is $0.005 per delivered row ($5 per 1,000).
At the free-plan rate read from the live pricing record on 2026-08-29, an untouched form (the four default boards at up to 20 rows each) delivers at most 80 deduplicated jobs for at most $0.40, a 500-job sweep costs $2.50, and a run that delivers nothing bills only the $0.00005 start.
Does Multi Job Board Scraper need an API key or a login?
No. The Actor never logs in to any board, and every row it delivers is public data. The extra boards are keyless public feeds and the six ATS providers are keyless too. From code you only need your Apify token.
All 12 selectable working boards were checked from Apify's default datacenter pool on 2026-08-07 and re-measured 2026-08-11 with no login and no key (DevITjobs UK has since been discontinued upstream, 2026-08-29, re-probed 2026-09-05, so 11 deliver rows today) (52 of 54 rows versus residential on the same search), and the six ATS providers — Greenhouse, Lever, Ashby, Recruitee, BambooHR and Workable — are keyless public APIs.
Which job boards can I scrape without an API key?
Nine keyless public boards ship with this Actor: The Muse, Remotive, Jobicy, Himalayas, Hacker News "Who is hiring?", Remote OK, We Work Remotely, Working Nomads and DevITjobs US/UK. LinkedIn, Indeed and Glassdoor need no key either, because they are read from their public search pages. Five other selectable boards (Google Jobs, ZipRecruiter, Bayt, BDJobs, Naukri) are blocked at the source today — the Board status table above says which, and why.
In a verified remote-only run on 2026-08-08 (python developer, 10 requested per board) the five API boards delivered The Muse 10, Jobicy 10, Hacker News 5, Himalayas 6 and Remotive 2 rows; per-board counts track each board's live inventory for the term and move day to day.
Where does the data come from?
Public job listings on LinkedIn, Indeed and Glassdoor via the open-source JobSpy engine plus our own fixes on top of it, and the public keyless feeds of The Muse, Remotive, Jobicy, Himalayas, Hacker News (Algolia), Remote OK, We Work Remotely (its category RSS feeds), Working Nomads and DevITjobs US/UK. Company boards come straight from the employer's own ATS API.
Board status was checked 2026-08-07 and re-measured 2026-08-11 from Apify's default datacenter pool. Across the 5,936 rows this Actor had delivered by the 2026-08-23 all-time re-measure (83 non-empty runs), Glassdoor accounted for 676 rows, every one carrying listing_type (sponsored or organic).
Can I run JobSpy without hosting it myself?
Yes. LinkedIn, Indeed and Glassdoor are read through the open-source JobSpy engine, running on Apify with proxies already configured — you POST a search as JSON and get rows back, with no Python environment to maintain. Three fixes ride on top of the library: LinkedIn paging now advances by the page's own cards instead of the running total (so offsets 20-29, 40-59 are no longer skipped above 20 LinkedIn rows), Glassdoor carries patches backported from open JobSpy pull requests #347 and #350, and descriptions are converted to Markdown without the stray backslash escapes (full\-time) the library emits by default.
The LinkedIn paging fix shipped on 2026-08-29 (deeper pages no longer repeat and newest-first ordering holds at depth), and the proxy comparison of 2026-08-11 measured 52 rows from the default datacenter pool against 54 from residential on the same search, at roughly a fifth of the cost.
How fresh is the data?
Every run reads each board's current listings live, so a row is as fresh as the board was at the moment of the run — there is no cached index and no historical archive. scraped_at records when the Actor read the row and date_posted is the board's own posting date. Company boards (atsCompanies) are the freshest source here, because they are the employer's own ATS feed rather than a board's copy of it.
Two identical runs a day apart shared 47% of their rows (measured before monitoring mode existed), which is what Only new jobs removes from a schedule; the Fresh postings only preset keeps the last 7 days (maxAgeDays: 7 plus a 168-hour board-side window), and since the 2026-08-29 paging fix LinkedIn's newest-first ordering holds beyond 20 rows.
How do I run it on a schedule?
Fill in your search, tick Only new jobs (monitoring mode), press Save & start once, then on the run page choose Actions → Schedule and pick daily (or hourly for hot markets). Every later run delivers, and bills, only postings the previous runs have not seen. The first run seeds the memory and an identical rerun bills zero (measured: 15 rows, then 0). Point the schedule's webhookUrl at Slack, at a Google Sheet via Zapier or Make, or at your ATS.
Schedule-grade filters, Slack / Discord / JSON digests and the safe stop before the time limit shipped on 2026-08-25; the memory is kept per search for 90 days in a private store in your own account, and a memory that cannot be read stops the run and charges nothing rather than re-delivering the baseline.
How do I get an alert for only new job postings each day?
Turn on Only new jobs (monitoring mode), set a webhookUrl, and schedule the run daily. The Actor remembers every posting this exact search has already delivered, in a private store in your own account, and delivers and bills only postings it has not seen before. A run with nothing new delivers no rows, sends no webhook, and bills only the $0.00005 run start. The memory is kept per search for 90 days.
Webhook alerts shipped on 2026-08-25: a Slack or Discord webhook gets one line per job for the first 20 rows plus the dataset link, any other URL gets JSON with counts, console links and the first 20 rows, and a webhook that fails is reported in RUN_SUMMARY.webhook without failing the run.
Can I monitor a specific company's careers page for new roles?
Yes — put the company names in Watch companies (atsCompanies), turn on Only new jobs, and schedule it. The Actor reads each company's own ATS board (Greenhouse, Lever, Ashby, Recruitee, BambooHR or Workable, tried in that order) and delivers only postings that were not there last time. Set sites: [] to skip the job-board search entirely. Up to 20 companies per run, maxResults rows per company, newest first. The memory key is each posting's own URL, so a posting the company re-saves is not re-billed.
Recruitee, BambooHR and Workable joined Greenhouse, Lever and Ashby on 2026-08-29, verified from a residential IP; adding them changed nothing for schedules that already existed, because the watch's memory key is built from the company names you typed. Daily is plenty — these boards change a few times a week.
Can I paste a LinkedIn or Indeed job search URL instead of building a query?
Yes — put it in searchUrls. LinkedIn, Indeed (any country sub-domain) and Glassdoor are supported, and the exact parameters read out of each URL are listed above. A URL from another site, or one with no keyword in it, is skipped with a written reason in RUN_SUMMARY.search_url_errors rather than silently scraping something else.
Checked 2026-08-08: an Indeed URL and the equivalent manual inputs (searchTerm, location, hoursOld: 336, distance: 25) returned the same 10 job ids and the same output columns. Radius is honored by LinkedIn and Indeed only; Glassdoor's scraper has no distance parameter, so a radius in a Glassdoor URL changes nothing.
Does it scrape remote jobs?
Yes — Remote jobs only asks Indeed, Glassdoor and The Muse for their own remote filters, enforces LinkedIn's is_remote after scraping, and auto-adds seven remote-only boards (Remotive, Jobicy, Himalayas, HN "Who is hiring?", Remote OK, We Work Remotely, Working Nomads). LinkedIn's guest search accepts f_WT and ignores it, which is why the Actor gates those rows itself, before billing.
Measured 2026-08-08 on the Remote-only sweep preset: 19 rows were dropped before billing and 0 of the billed rows had is_remote false — before that gate, 10 of 10 LinkedIn rows billed under the preset were on-site jobs in Midland TX, El Paso TX and Pittsburgh PA.
Why didn't the remote boards run on my location search?
By design: the seven remote-only boards carry remote jobs only, so they add nothing to a query like nurse in Dallas and are not run for it. They join when you name them in sites, or automatically when Remote jobs only is on and the board list is at its default. RUN_SUMMARY.remote_boards_auto_added tells you when the auto-add happened.
When they do run — a remote-only python developer search on 2026-08-08 at 10 requested per board — The Muse delivered 10, Jobicy 10, Hacker News 5, Himalayas 6 and Remotive 2 rows, so the auto-add earns its rows on a remote search and would add nothing but cost on a city one.
Does it include salary data?
Yes, where the board publishes it: a measured run filled salary_min_annual on 74 of 89 rows (83%), and hour