Career Page Scraper — Company Jobs by Domain, 9 ATS Boards avatar

Career Page Scraper — Company Jobs by Domain, 9 ATS Boards

Pricing

from $0.80 / 1,000 jobs

Go to Apify Store
Career Page Scraper — Company Jobs by Domain, 9 ATS Boards

Career Page Scraper — Company Jobs by Domain, 9 ATS Boards

Career page jobs: paste a company domain and get every live opening from its Greenhouse, Lever, Ashby, Workday or other ATS board — nine platforms, one box, no board tokens needed. From $0.80 per 1,000 jobs. Never pay for a domain we couldn't resolve.

Pricing

from $0.80 / 1,000 jobs

Rating

0.0

(0)

Developer

Steadyfetch Team

Steadyfetch Team

Maintained by Community

Actor stats

0

Bookmarked

3

Total users

2

Monthly active users

2 hours ago

Last modified

Share

A career page scraper that takes a domain, not a board token — from $0.80 per 1,000 jobs. Paste a company domain and we find its Greenhouse, Lever, Ashby or Workday board and return every live opening from that company's own career page. Thirteen ATS platforms, one box.

Never pay for a domain we couldn't resolve.

Using an AI agent? Pin this actor in Apify's MCP server with one link: https://mcp.apify.com?tools=steadyfetch/company-jobs-by-domain

  • Actor id: steadyfetch/company-jobs-by-domain
  • Input: { "domains": ["stripe.com"] } — the one field you have to set.
  • Cap the bill: set maxTotalChargeUsd on the run (a run option, not Actor input), e.g. 0.50 — the run stops when it reaches it.

From $0.80 per 1,000 jobs — all-inclusive pay per event, nothing charged for starting a run, charged only on delivery.

Running the same domains again? An opening already delivered to your account is never charged a second time — see Never pay for the same opening twice (a run started with a scoped API token needs key-value store permission for that check to work).

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-opening sample — vercel.com's own hiring board — charged like any run, at most $0.02 on the Apify free plan and less on paid plans. Set only the filters — a posted-within window, remote-only, a row cap — and no companies, 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 domains already in the form and you get those companies for real, up to your row caps.

Company Jobs Scraper input form in the Apify console: company domains, hiring-platform filter, posted-within and a remote-only toggle

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 real row

Five live openings from a real run, in the Apify console's dataset table — the company's own board, the department, the posted date and both the job and apply links, one row per opening:

Dataset table of a real Company Jobs Scraper run: five Vercel Account Executive openings read from the company's Greenhouse board, with department, posted date, job URL and apply URL

And one delivered row in full:

{
"source": "greenhouse",
"sourceJobId": "6136160004",
"jobUrl": "https://job-boards.greenhouse.io/vercel/jobs/6136160004",
"applyUrl": "https://job-boards.greenhouse.io/vercel/jobs/6136160004",
"title": "Account Executive, Commercial",
"company": "Vercel",
"companyDomain": "vercel.com",
"location": { "raw": "Hybrid - London", "city": "Hybrid - London", "region": null, "postcode": null, "country": null },
"isRemote": null,
"employmentType": null,
"department": "Account Executive",
"salary": null,
"postedAt": "2026-08-06T16:50:10.000Z",
"updatedAt": "2026-08-18T22:06:19.000Z",
"scrapedAt": "2026-08-27T10:00:00.000Z",
"descriptionText": "About Vercel:\n\n Vercel is the agentic infrastructure company. We free people and agents to ship what's next…",
"descriptionHtml": "<div class=\"content-intro\"><h2>About Vercel:</h2>…",
"atsPlatform": "greenhouse",
"atsBoardToken": "vercel",
"resolvedVia": "careers-link",
"dedupeKey": "greenhouse:vercel:6136160004",
"charged": true,
"missReason": null
}

Plus one company row per domaindomain, companyName, atsPlatform, boardUrl, openRoleCount, sharedRoleCount (openings already delivered — and charged once — under another company in the same run), totalOpenRoles, resolved, resolvedVia — and one summary row with delivered, stoppedBy, resumeCursor, datasetId (paste it into resumeFromDatasetId to continue this run later), carriedOver and chargedEvents.

See a whole run's output before you spend anything: real dataset from a verified run — six live openings from vercel.com and linear.app, their two company rows, and the run summary, exactly as this actor returned them.

fieldnotes
title · company · companyDomaincompany is filled when the board publishes it
location{raw,city,region,postcode,country} · isRemoteas the board states it — country is an ISO-2 or null, never a state or a postcode
employmentType · department
salary{min,max,currency,period,isEstimate}null unless the company published a range
postedAt · updatedAt · scrapedAtISO 8601; null when the board only gives relative text
descriptionText · descriptionHtmlfull description where the board's API includes it
jobUrl · applyUrlthe company's own posting, not a re-hosted copy
atsPlatform · atsBoardToken · resolvedViahow we found the board
charged · missReason · dedupeKeythe reconciliation trio

Agent / API paste-block

Actor: steadyfetch/company-jobs-by-domain
Required: domains (array of company domains, careers URLs, board URLs or names)
Optional: atsFilter (array: greenhouse | lever | ashby | workday | workable |
smartrecruiters | rippling | personio | teamtailor |
breezy | bamboohr | adp | phenom)
keywords (array of words — keep only openings whose job TITLE
matches one of them; whole words and the start of a
word, so "engineer" keeps "Engineering Manager"; up to
25; filtered openings are never charged)
location (string — ONE place, e.g. "New York" or "Germany"; keep
only openings whose location matches it, on the same
whole-word rule; filtered openings are never charged)
remoteOnly (boolean, default false — keep only openings the board
marks remote; filtered openings are never charged)
postedWithinDays (integer — keep only openings posted that recently;
up to 30 days, the furthest back these boards publish
dates; filtered openings are never charged)
maxItems (integer, default 1000 — hard cap for the whole run;
up to 100,000 per run)
maxItemsPerCompany (integer, default 200 — up to 5,000 per company)
maxRunSeconds (integer, default 900 — clean stop, never a timeout kill;
30 to 3,600 seconds)
includeSeen (boolean, default false — OFF: an opening already delivered
to your account is skipped, uncharged. ON: it comes back
marked repeat:true with firstSeenRunId, still uncharged)
resumeFromDatasetId (string — an earlier run's dataset ID; every opening already
in it is skipped and never charged again)
Charges: job once per delivered opening
company-checked once per company with a definitive answer (including a
verified zero-openings result)
Note: sending null (or an empty value) for an optional field is the same as leaving
it out — the default is used, so templated callers need no special-casing.
Build spec: https://apify.com/steadyfetch/company-jobs-by-domain/api
Token: https://console.apify.com/settings/integrations
curl -X POST "https://api.apify.com/v2/acts/steadyfetch~company-jobs-by-domain/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"domains":["stripe.com","linear.app"],"maxItemsPerCompany":50}'

Hiring platforms

Greenhouse · Lever · Ashby · Workday · Workable · SmartRecruiters · Rippling · Personio · Teamtailor · Breezy · BambooHR · ADP WorkforceNow · Phenom People. Each is read from that platform's own public job-board feed, so the data is whatever the company published, unchanged.

On the last two the company's own careers site is the board — there is no separate board address to find — so they are reached from the company's careers page rather than guessed from the domain, as SmartRecruiters, Workday and BambooHR already are.

How a domain becomes a board (resolvedVia tells you which one happened):

resolvedViawhat it means
board-urlyou pasted the board URL — no lookup needed
careers-linkthe company's own careers page links its board
token-patternthe domain name matched a board token and that board returned live openings
name-probesame, from a company name instead of a domain

A board found by guessing the token only counts when real openings come back. An empty board reached by guesswork is not a match, so it is never charged.

What you are never charged for

situationmissReasonrun status
No hiring board found for the domainUNRESOLVED_DOMAINSUCCEEDED — the row lists every platform checked
The board rate-limited or dropped usTHROTTLEDSUCCEEDED — re-runnable, and the row says so
The board answered with an access challengeCHALLENGEDSUCCEEDED — re-runnable
The board was unavailable this runSOURCE_UNAVAILABLESUCCEEDED — re-runnable
The board answered and listed nothingCLEAN_EMPTYSUCCEEDED
A domain, URL or name we could not readUSER_INPUTSUCCEEDED — the row names the fix
A hiring platform name we do not recognise in "Only these hiring platforms"USER_INPUTSUCCEEDED — the row names it; the run stops rather than bill you for every platform
The note on a run where you set filters but no companies, so the sample ran under themSAMPLE_SETTINGS_KEPTSUCCEEDED — the row names which settings were yours
An opening your account already has from an earlier runskipped, never charged twice (the check needs key-value store permission)
Your own maxItems / maxRunSeconds / cost capSTOPPED_AT_LIMITSUCCEEDED — summary row with resumeCursor and the datasetId to continue from

The one exception: COMPANY_NO_OPENINGS charges the company-checked event and no job rows. Confirming a company is not hiring is a real answer, and it is the answer that costs you the least.

We never guess. A rate limit is reported as a rate limit, not as "this company has no board" — a temporary problem is never dressed up as a permanent verdict.

This actor may fail when a hiring platform changes things. The two you are most likely to see are UNRESOLVED_DOMAIN (we checked every platform and found no board) and THROTTLED (a board rate-limited us). Both ship as a row with charged: false, the run still SUCCEEDS, and the row says whether re-running can help.

Never pay for the same opening twice

Every run remembers the openings it delivered for your account, in a key-value store called company-jobs-watch-account in your own Apify account. Run the same domains 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; each company's own row says how many of its openings you already had.

The memory is a key-value store in your own Apify account, so a run started with a scoped API token in restricted-access mode needs key-value store Read, Write and Create permission (or Actor runs set to Full access) under Settings → API & Integrations; without it the run says so on its status line and on an uncharged row at the top of your dataset, and cannot skip what you already have (Write is what lets the run remember what it delivered; without it every run pays again).

The company answer is different on purpose: "this board has N live openings today" is a fresh fact about that company right now, so it is delivered and charged on every run — that is what you are asking for when you re-run a domain.

Want one complete dataset per run instead of only what changed? Turn on Include openings 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. A run started with an API token whose permissions are limited to this Actor's scope can only open it if that token has key-value store Read, Write and Create — the store is created on your first run, so Create matters too.
  • Entries older than 90 days count as new again, so an opening you collected last quarter is collected (and charged) again if it is still live.
  • Only delivered rows are remembered. An unresolved domain, a throttled board or a board that listed 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.

Filters you can set, and what they cost you

Only these hiring platforms is a hard filter on where we look. The console's dropdown and Apify's own input check refuse a name we do not know before a run starts; if one ever reaches the actor another way, the run ships one uncharged row naming it and stops — it is never quietly ignored, because that would read as "check all of them" and bill you for exactly the boards you asked to leave out. A single name works as well as a list ("greenhouse" or ["greenhouse"]), and capitalisation does not matter.

keywords, location, remoteOnly and postedWithinDays narrow what is delivered, and an opening they leave out is never charged.

keywords matches the job title — any one of the words is enough. location matches the place the board published, and it takes one place, not a list ("New York, NY" is one location with a comma in it). Both match whole words and the start of a word, never a bare substring: "engineer" keeps "Engineering Manager", "new york" keeps "New York, NY", and "ny" keeps "NY" but not "Sunnyvale" — a substring match would keep the wrong openings and you would pay for them. Accents fold, so "Munchen" finds "München". Ask for more than 25 keywords and the run continues at 25 with one uncharged row saying so.

All four lean on what each board actually publishes: Greenhouse and Workday publish no remote flag at all, a few boards publish no posting date, and a few publish no location. An opening we cannot check against your filter is left out too — and the company's own row says how many and which field the board did not publish, so a filter can never read as "this company has no openings". A board emptied by your filters keeps missReason: null for exactly that reason.

Limits are hard limits

maxItems, maxItemsPerCompany and maxRunSeconds stop the run exactly where you set them. Ask for more than this actor can do and the run still starts. One run delivers up to 100,000 jobs, up to 5,000 of them from any one company, inside 30 to 3,600 seconds, and reads posting dates up to 30 days back. A larger (or smaller) number is not an error: the run continues at the nearest limit and writes one extra uncharged row saying what you asked for and what bound it — you are never left with no run at all. The run still finishes successfully, the summary row names which limit bound, and resumeCursor lists the companies that were not reached. A time limit ends the collecting, never the delivering: a board already fetched when the clock runs out is still delivered in full, so a run never pays for openings it does not hand you. To collect the rest without paying again for what you already have, re-run the same input with a higher cap and resumeFromDatasetId set to that run's dataset ID (it is on the summary row as datasetId): every opening 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.

And they hold across a restart. If the platform moves your run to another server mid-way — or you resurrect it yourself — the run reads back what it already delivered and picks up from there: no duplicate rows, no second charge for an opening you already have, and your row cap still means what it says. Resurrecting a run that already finished costs nothing and returns the same dataset.

FAQ

The company isn't on any ATS — what now? The company row lists every platform that was checked. If you know the board URL, paste it directly (https://jobs.lever.co/acme) and we skip the lookup entirely.

Can I search jobs across boards instead of by company? Not with this actor — it answers "is this company hiring, and for what". A job board URL (LinkedIn, Indeed, Glassdoor) is rejected with a guidance row rather than treated as an employer.

How do I chain this after a company-list scraper? Feed the domains array from any actor that outputs company domains. Same-company duplicates across input shapes are delivered once and charged once — the later company's row reports them in sharedRoleCount instead of claiming them again.

Why is company sometimes empty? Several board APIs do not publish the employer name in their listing payload. We leave the field null rather than invent one; companyDomain and the company row always identify it.

Does {} cost anything? Yes — like any run, and at most $0.02 on the Apify free plan (less on paid plans). With no input at all it reads one real hiring board (vercel.com's) and returns up to 5 openings, and you pay for the openings it delivers. If that board cannot be reached at that moment you get one uncharged company row saying so, never an empty result.


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 "every open role on stripe.com's careers page" and point it at this actor.

The one-link pin is at the top of this page.

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. Google Jobs adds one search fee of $0.004 per search that returns listings; a search that returns nothing pays nothing.

SourceActor
IndeedIndeed Jobs Scraper — Indeed API Alternative, Salary, Vacancies
A company's own career site (paste a domain)this actor
Glassdoor, with the employer ratingGlassdoor Jobs Scraper — Glassdoor API Alternative + Ratings
Several boards at once, de-duplicatedMulti Job Board Scraper — Career Pages, Listings Aggregator
Google Jobs, with the full panel and apply linksGoogle Jobs Scraper — Google for Jobs Listings, 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: keyword volume's fresh lookup, profile posts' profile lookup, Google Jobs' search fee — each README states it).

FamilyActors
Ad creative intelligenceFacebook · Google Ads video · TikTok · LinkedIn · Google Ads text & OCR
Trends & keywordsGoogle Trends · Trends Now · Breakout keywords · Autocomplete keywords · Keyword volume & CPC · Social trends
YouTube transcriptsYouTube videos · YouTube channels
InstagramReel transcripts · Profile posts
JobsIndeed · Career sites by domain · Glassdoor · Multi-board · Google Jobs
AmazonProducts · Search · Bestsellers · Sellers
Any media fileSpeech 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 it earned its keep, a rating helps other buyers find it, and saving the actor keeps it one click away.