How-to
How to run ATS & Career Page Jobs Scraper (Greenhouse, Lever, Workday)
Step-by-step: the exact input, what each field does, and the rows that come back from the source portal.
You have a list of employers and you want to know what every one of them is hiring for. Done by hand that is: find each company’s careers page, work out which applicant-tracking system it runs, page through the listings, and copy them into a sheet — then repeat the whole thing next week to see what changed. The systems are the reason it is tedious. Stripe’s board is Greenhouse, Notion’s is Ashby, Gopuff’s is Lever, NVIDIA’s is Workday, and each one presents jobs differently. This actor takes the company domain and does the identification for you, returning every open role from all five systems in one shape.
What you’ll need
Nothing but an Apify account. The actor talks to the source portal directly.
Nothing else. No API key, no login, no CAPTCHA solver and no proxy — the proxy option in the input is off by default and you can leave it that way. Each of these five systems publishes its job board over an open endpoint, which is why this runs in seconds rather than driving a browser.
Step 1 — Open the actor
Open ATS & Career Page Jobs Scraper (Greenhouse, Lever, Workday) on the Apify Store and press Try for free. The actor’s own page on this site lists its live health and pricing.
Step 2 — Fill in the input
| Field | Type | Required | What it does |
|---|---|---|---|
company | string | no | A company domain (acme.com), a careers URL, or an explicit board such as ‘greenhouse:stripe’ or ‘lever:gopuff’. The Actor works out which applicant-tracking system the company… |
companies | array | no | Look up many companies in ONE run instead of starting a run per company. Takes the same values as ‘Company’. Combined with ‘Company’ if you fill both, and duplicates are… (default []) |
maxResults | integer | no | Cap on jobs returned PER COMPANY, not per run — so a batch cannot silently return only the first rows overall and read as ‘these companies aren’t hiring’. When a board holds… (default 1000) |
postedAfter | string | no | Keep only jobs published on or after this date (YYYY-MM-DD) — this is what turns the Actor into a weekly new-openings feed. Jobs whose board states no publish date (Workday… |
includeDescription | boolean | no | Off by default: descriptions are the bulk of the payload (one large board is ~16 KB per job) and most buyers filter on title, team and location first. Turn it on when you need… |
Two fields decide everything.
companies takes whatever you have. A plain domain (stripe.com) is the normal case. A
careers URL works too, and for Workday it is required — a Workday board is addressed by a tenant,
a numbered pod and a site name, none of which can be derived from a company domain, so paste
https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite rather than nvidia.com. If
you already know the board you can name it outright as lever:gopuff or greenhouse:stripe, and
that always wins over any detection.
maxResults is per company, not per run. This matters more than it sounds: boards are large.
On 6 September 2026 Stripe’s board held 617 open roles, Gopuff’s 765, and one NVIDIA Workday tenant
2,000. Leave it high to pull whole boards, or set it low while you are experimenting. Whatever you
set, the run log states the board’s own total next to the number returned, so a cap never quietly
looks like the whole picture.
A working input:
{
"companies": [
"stripe.com",
"notion.so",
"lever:gopuff"
],
"maxResults": 100,
"postedAfter": "2026-08-01"
}
Step 3 — Run it
Press Start. Rows are pushed to the dataset as they’re found, so the run log fills in as it works.
Fast. Four companies capped at 25 jobs each returned 100 rows in 12.8 seconds on 7 September 2026, because every request is a plain API call with no browser involved. Whole boards take longer only because there is more to page through.
An empty result is not silence. If a company cannot be resolved to a board you get one free,
unbilled row explaining why and naming the input that would work — usually the board reference, as
in lever:acme. That distinction is deliberate: “we could not find the board” and “this company
is not hiring” are different answers, and only the first one is our problem.
Step 4 — Read the output
Each run returns a labelled table, not raw JSON:
Every row looks like this — a real row from a real run:
{
"source_url": "https://job-boards.greenhouse.io/vercel/jobs/6136160004",
"company": "vercel.com",
"company_name": "Vercel",
"ats": "greenhouse",
"ats_slug": "vercel",
"detected_by": "slug_match",
"job_id": "6136160004",
"title": "Account Executive, Commercial",
"department": "Account Executive",
"team": null,
"location": "Hybrid - London",
"locations": [
"Office - London"
],
"remote": null,
"employment_type": null,
"posted_at": "2026-08-06T12:50:10-04:00",
"posted_text": null,
"updated_at": "2026-08-18T18:06:19-04:00",
"apply_url": "https://job-boards.greenhouse.io/vercel/jobs/6136160004",
"requisition_id": null,
"description": null
}
| Field | Example value | Meaning |
|---|---|---|
company | vercel.com | The value you submitted — domain, careers URL or explicit board |
company_name | Vercel | Employer name as the board states it (Greenhouse and SmartRecruiters publish it; Ashby, Lever and Workday do not, so it is null there) |
ats | greenhouse | greenhouse, lever, ashby, smartrecruiters or workday |
ats_slug | vercel | The board identifier on that ATS |
detected_by | slug_match | How the board was matched: explicit, careers_page, or slug_match |
job_id | 6136160004 | The board’s own job identifier — stable across runs, so it de-duplicates |
title | Account Executive, Commercial | Posted job title |
department / team | — | Department and team or job function, where the board publishes them |
Most people come for title, location and apply_url, and those are filled on every row.
Three fields need a word of warning, all for the same reason — boards differ in what they publish, and this actor returns what the board said rather than filling gaps with guesses.
company_name is the employer as the board names it. Greenhouse and SmartRecruiters publish it;
Ashby, Lever and Workday do not, so it is null there.
posted_at is a real timestamp on Greenhouse, Lever, Ashby and SmartRecruiters. Workday does not
publish a date at all — it says “Posted Today” — so those rows leave posted_at null and keep the
board’s own wording in posted_text. Nothing invents a date from that phrase.
remote comes from the board’s own flag on Ashby and SmartRecruiters. Greenhouse and Workday
publish no such flag, so it is read from their location wording instead: “Remote - US” gives true,
“Hybrid - London” gives false, and a posting that says only “Dublin” gives null, because it has not
actually said either way.
Step 5 — Export it
Open the Dataset tab and export to CSV, JSON, or Excel — or pull the same rows from the API, which is what you want if this is going to run on a schedule.
What it costs
$0.00125 per job returned — $1.25 per 1,000 — and you are charged per job, not per company. Fifty companies with 1,000 roles between them cost the same as one company with 1,000 roles. That price takes effect on 21 September 2026; until then it is $0.001 per job, or $1 per 1,000.
Free: companies that resolve to no board, boards with no open jobs, and the marker rows that explain either. A realistic run of 40 employers averaging 60 open roles each is about 2,400 jobs, so roughly $3.00 plus Apify’s own platform usage, which for an actor this light is pennies.
Where the data comes from
This reads the source portal directly. Every night a canary runs this actor against that live source and diffs the result against a frozen fixture — what “verified” means.
The source is whichever applicant-tracking system the employer actually runs — Greenhouse, Lever, Ashby, Workday or SmartRecruiters. The annoying part is not reading a board, it is knowing which board to read: most careers pages render their listings in JavaScript, so there is nothing in the page to tell you. Reading the careers page for an ATS link identified only 4 of 14 test companies; deriving a likely board name from the domain and asking the vendor to confirm it got 11, without ever returning another company’s jobs.
See it used
Every open job at a company, from its career page — the straight case: domains in, open roles out.
New job postings this week across a company list — the same run on a schedule, returning only what has appeared since you last looked.
If you need to reach the employers rather than list their openings, the Business Email & Phone Contact Details Scraper takes the same company domains and returns contact details instead of jobs.
Last updated 2026-09-06