Job Board Scraper — Boards, Career Pages, Listings Aggregator
Pricing
from $1.80 / 1,000 job listings
Job Board Scraper — Boards, Career Pages, Listings Aggregator
One keyword across Indeed and any company's own hiring board, merged and de-duplicated into one feed — the same role on two boards is one row and one charge. From $1.80 per 1,000 job listings. Blocked boards, empty searches and merged duplicates are never charged.
Pricing
from $1.80 / 1,000 job listings
Rating
0.0
(0)
Developer
Steadyfetch Team
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
15 hours ago
Last modified
Categories
Share
Never pay for a job we didn't deliver. One keyword across Indeed and any company's own hiring board, merged and de-duplicated into one feed. The same role found on two boards is one row — and one charge.
From $1.80 per 1,000 job listings — all-inclusive pay per event, nothing charged for starting a run, charged only on delivery. One price for the whole feed: no per-source fee, no per-search fee.
Just want to see it work? Run it with no input at all ({} from the API or an AI agent) and
you get a real 5-job sample — "software engineer" on Indeed US plus vercel.com's own board,
merged — charged like any run, a cent or two at most. Set only the filters — a country, a sort
order, a row cap — and nothing to search for, and that same sample runs under them, charged like
any run; a limit larger than the sample's own is capped at it. In the console, Start with the
example keyword already in the form and you get that search for real, up to your row cap.
Running the same search again? A job already delivered to your account is never charged a second time — see Never pay for the same job twice.

Unofficial. This actor is not affiliated with, endorsed by, or sponsored by Indeed or any hiring-board provider. "Indeed" is a trademark of Indeed, Inc.; Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters, Rippling, Personio and Teamtailor are trademarks of their respective owners. It reads only the public search and hiring pages an ordinary browser sees.
Every row carries charged and missReason, so you can reconcile the invoice from the
dataset itself without opening the console. Only rows with charged: true were billed.
Output — a merged row
Five merged jobs from a real run, in the Apify console's dataset table — every row shows which boards it came from, how many duplicates it absorbed, and the department, type and posted date:

And one merged row in full:
{"title": "Senior Backend Engineer","company": "Acme","location": { "raw": "Berlin, Germany", "city": "Berlin", "region": null, "postcode": null, "country": "DE" },"salary": { "min": 85000, "max": 115000, "currency": "EUR", "period": "year" },"descriptionText": "…full description from the company's own board…","source": "greenhouse","sources": [{ "board": "greenhouse", "jobUrl": "https://job-boards.greenhouse.io/acme/jobs/123", "applyUrl": "…", "postedAt": "2026-08-24T09:00:00.000Z", "salary": null },{ "board": "indeed", "jobUrl": "https://www.indeed.com/viewjob?jk=…", "applyUrl": null, "postedAt": null, "salary": { "min": 85000, "max": 115000, "currency": "EUR", "period": "year" } }],"merged": true,"duplicateCount": 1,"dedupeConfidence": "high","dedupeKey": "mb:…","charged": true,"missReason": null}
Plus one coverage row per source and per company board (source, board, status,
delivered, resolved, missReason) and one summary row with delivered, uniqueFound,
duplicatesMerged, stoppedBy, datasetId (paste it into resumeFromDatasetId to continue this run
later), carriedOver and chargedEvents.
See a real run's full output →
— eight live jobs pulled from four different company boards in one run, plus the coverage row for
each board and the summary. Every charged row is marked charged: true; nothing else is.
Agent / API paste-block
Actor: steadyfetch/multi-job-board-scraperRequired: query (job title/keyword, or a full Indeed search URL)Optional: companies (array of company domains / careers URLs / board URLs / names)sources (array: ["indeed","ats"], default both)countries (array of ISO-2, default ["US"] — each is its own Indeed search)location (string, e.g. "Austin, TX")radiusKm (integer 1-160, needs a location — Indeed only)dedupe (boolean, default true — merge the same role across boards)postedWithinDays (integer, 1-30)sortBy (relevance | date, default relevance — Indeed only; theoption's own name works too, e.g. "Date posted")remoteOnly (boolean, default false — filtered rows are never charged)maxItems (integer, default 100 — hard cap on UNIQUE jobs)maxItemsPerCompany (integer, default 200)maxRunSeconds (integer, default 1200 — clean stop, never a timeout kill)includeSeen (boolean, default false — OFF: a job already delivered to youraccount is skipped, uncharged. ON: it comes back markedrepeat:true with firstSeenRunId, still uncharged)resumeFromDatasetId (string — an earlier run's dataset ID; every job already in itis skipped and never charged again)Charges: job-listing once per UNIQUE job after de-duplicationNote: sending null (or an empty value) for an optional field is the same as leavingit out — the default is used, so templated callers need no special-casing.Every number above falls back to its default if it arrives empty — never toits smallest value.Token: https://console.apify.com/settings/integrations
curl -X POST "https://api.apify.com/v2/acts/steadyfetch~multi-job-board-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H 'Content-Type: application/json' \-d '{"query":"backend engineer","companies":["stripe.com","https://jobs.lever.co/leverdemo"],"countries":["US"],"maxItems":50}'
What it does
You give one keyword and, optionally, the companies you care about. The actor:
- searches Indeed for your keyword (across the countries you pick), and
- pulls each company's own hiring board — Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters, Rippling, Personio or Teamtailor — filtered to roles that match your keyword, then
- merges the same role found on more than one board into a single row, keeping every source link, and charges you once.
Company boards are how you reach roles that never appear on a job aggregator. About half of companies resolve from a bare domain (mid-market best; large enterprises mostly run their own Workday/Oracle, so paste a board URL for those). You are never charged for a company we cannot resolve.
The de-duplication, in plain terms
A "duplicate" is the same role — same company, same title, in an overlapping location — found
on two different sources. When we find one, you get one row with a sources array listing
every board it was on, duplicateCount, and a dedupeConfidence of high (matched on a
shared city) or medium (matched on a country-wide or remote posting).
We tuned this on a corpus of real jobs to never delete a distinct job. When two roles at
one company genuinely can't be told apart from an aggregator's single row, we keep them
separate — tagged with a shared dedupeGroupId — rather than merge them and silently drop
one. A wrong merge is a job you never see, so we always err on the side of showing you more.
Turn dedupe off and each board's copy is its own row (the identical posting fetched twice is
still never charged twice).
Sources
- Indeed — needs a keyword. United States, United Kingdom and Germany are verified end to end; any listing that comes back outside the country you asked for is reported, never charged.
- Company boards (ATS) — need company domains. Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters, Rippling, Personio, Teamtailor — each the company's own documented public board.
Narrow with sources (for example ["ats"] to skip Indeed). Google Jobs is deliberately not a
source — its jobs tab returns only ~10 rows per query with no way to page deeper, so it would
add cost without adding coverage.
What you are never charged for
| situation | run status |
|---|---|
| A board answered with an access check or rate limit | SUCCEEDED — re-runnable, the coverage row says so |
| A company we could not resolve to a board | SUCCEEDED — uncharged, lists what was tried |
| A source ran and matched nothing | SUCCEEDED — a real answer |
| A duplicate merged into another row | SUCCEEDED — charged once, not per board |
| A job your account already has from an earlier run | SUCCEEDED — skipped, never charged twice |
| An input we could not read | SUCCEEDED — the row names the fix |
| The note on a run where you set filters but nothing to search for, so the sample ran under them | SUCCEEDED — the row names which settings were yours |
Your own maxItems / maxRunSeconds / cost cap | SUCCEEDED — summary row names the knob and the datasetId to continue from |
We never guess. An access check is reported as an access check, not as "no jobs found" — a temporary problem is never dressed up as a permanent verdict, and it is never charged.
This actor may fail when a board changes things. The two you are most likely to see are
a board answering with an access check or rate limit, and a company we could not resolve to
a hiring board. Both ship as a coverage row with charged: false, the run still
SUCCEEDS, and the row says whether re-running helps.
When Indeed turns every connection away and the run still has time, it does not stop at the first refusal: it varies the connection type and device shape, reaches a rendered fallback, and then waits a couple of minutes and tries the whole ladder again — up to three times. How much of the run's remaining time one blocked search may spend waiting follows the size of what you asked for: a third of it while there are still other searches and company boards to fetch, so a wide block leaves them their share, and up to all of it when this search is the only thing left to fetch. Only then does the uncharged coverage row ship, and it says what was tried: how many connections, over how many minutes, whether it waited, and how much time was left. A run with a short time limit makes one attempt and reports at once; a "not found" answer is answered at once and never waited on.
Never pay for the same job twice
Every run remembers the jobs it delivered for your account, in a key-value store called
multi-job-watch-account in your own Apify account — under every identity a job carries, one
per board that saw it. Run the same search again and what you already have is skipped before it
takes a row slot: no row, no charge, and no bite out of your row cap. The status line says how
many (18 already in your account, skipped, not charged) and OUTPUT.skippedSeen holds the
number.
Storing every identity is what makes it work here: a role you first got from Indeed alone is still recognised next week when a company board sighting merges with it, and the other way round — so a merged row can never read as new and bill you again.
Want one complete dataset per run instead of only what changed? Turn on Include jobs you already
have (includeSeen: true) and those rows come back anyway — marked repeat: true with
firstSeenAt and firstSeenRunId naming the run that first delivered them, charged: false.
- The store lives in your account, one record (
SEEN). Open it in Storage → Key-value stores to see everything the actor has delivered you, or delete the record to start over. This actor runs with limited permissions and touches nothing else in your account. - Entries older than 90 days count as new again, so a job you collected last quarter is collected (and charged) again if it is still live.
- Only delivered rows are remembered. A blocked board, an unresolved company or a source that matched nothing leaves nothing behind, so the next run tries it again.
- If the store cannot be read on some run, the run still runs: it delivers and charges as usual and the status line says the repeat check was unavailable, so you know a repeat may have been charged that once.
- The no-input sample is outside all of this. Pressing Start with nothing filled in reads both boards live every time and neither checks nor updates this list, so you always see a real result rather than a hand-back of the last one — and those few sample rows are charged like any run's, each time you press it.
FAQ
I hit my cap — how do I get the rest without paying twice? Re-run the same input with a higher maxItems and resumeFromDatasetId set to that run's
dataset ID (it is on the summary row as datasetId): every job already in that dataset is
skipped — never fetched, never charged — and the new summary's carriedOver says how many.
A dataset ID that cannot be read stops the run before it fetches anything, uncharged.
Under a small cap, delivery rotates across boards, so Indeed plus two company boards returns rows
from all three rather than the first few Indeed cards. A time limit ends the collecting, never
the delivering: every job already collected when the clock runs out is still delivered, so a run
never pays for jobs it does not hand you.
Does {} charge anything? Yes — like any run, and a cent or two at most. With no input at all
the run is a real 5-job sample — "software engineer" on Indeed US plus vercel.com's own board,
merged — and you pay for the jobs it delivers. Set only filters and nothing to search for and the
same sample runs under them, charged the same way, with one extra uncharged row naming which
settings were yours. A source that does not answer gets an uncharged
coverage row saying so, never an empty result.
Can I just scrape a company's whole board? Yes — pass the companies with sources: ["ats"].
For deep, unfiltered board pulls by domain, the sibling Company Jobs Scraper — Career Pages by Domain, 9 ATS Boards
is purpose-built for exactly that.
Why did a company return nothing? Either it has no live openings matching your keyword, or its board could not be positively identified from the domain — the coverage row tells you which, and neither is charged. Paste the board URL directly if you know it.
Run it from an AI agent (MCP)
Works out of the box from any MCP client (Claude, Cursor, …) via Apify's MCP server — ask for "remote data engineer jobs across Indeed and company career pages, de-duplicated" and point it at this actor.
Pin this actor in one click — add this server to your MCP config:
{"mcpServers": {"job-board": {"url": "https://mcp.apify.com?tools=steadyfetch/multi-job-board-scraper","headers": { "Authorization": "Bearer YOUR_APIFY_TOKEN" }}}}
Steadyfetch jobs suite
One actor per source, one job each, the same row shape — so a pipeline built on one source takes the next without a rewrite. All-inclusive pay per event, nothing charged for starting a run, charged only on delivery. From 16 September 2026 Google Jobs adds one search fee of $0.004 per search that returns listings; a search that returns nothing pays nothing.
| Source | Actor |
|---|---|
| Indeed | Indeed Jobs Scraper — Salary, Apply Links, Exact Country |
| A company's own career site (paste a domain) | Company Jobs Scraper — Career Pages by Domain, 9 ATS Boards |
| Glassdoor, with the employer rating | Glassdoor Jobs Scraper — Employer Star Rating on Every Row |
| Several boards at once, de-duplicated | this actor |
| Google Jobs, with the full panel and apply links | Google Jobs Scraper — Full Panel, Salary, Apply Links |
The rest of the steadyfetch shelf — same contract everywhere: all-inclusive pay per event, nothing charged for starting a run, charged only on delivery (three actors add a small delivery-conditional fee from 16 September 2026: keyword volume's fresh lookup, profile posts' profile lookup, Google Jobs' search fee — each README states it).
| Family | Actors |
|---|---|
| Ad creative intelligence | Facebook · Google Ads video · TikTok · LinkedIn · Google Ads text & OCR |
| Trends & keywords | Google Trends · Trends Now · Breakout keywords · Autocomplete keywords · Keyword volume & CPC · Social trends |
| YouTube transcripts | YouTube videos · YouTube channels |
| Reel transcripts · Profile posts | |
| Jobs | Indeed · Career sites by domain · Glassdoor · Multi-board · Google Jobs |
| Amazon | Products · Search · Bestsellers · Sellers |
| Any media file | Speech to Text · any link or file |
Feedback & support
Found an issue? Open it on the Issues tab — we usually reply within a couple of hours, always within a day. Feature requests welcome. If this actor earned its keep, a rating helps other buyers find it, and saving it keeps it one click away in your console.
