ATS Jobs Scraper - Greenhouse, Lever & Ashby in One Schema
Pricing
from $1.05 / 1,000 job scrapeds
ATS Jobs Scraper - Greenhouse, Lever & Ashby in One Schema
Pull open jobs from six applicant tracking systems — Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters — into one normalised schema. Auto-detects which ATS a company uses. Official public APIs, no browser, no proxy, no blocking.
Pricing
from $1.05 / 1,000 job scrapeds
Rating
0.0
(0)
Developer
John J. K. Omolo
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
ATS Jobs Scraper — 6 Sources, One Schema
Give it a list of company board slugs. Get back every open job, from any of six applicant tracking systems, in one consistent schema.
Why this exists
Almost every tech company's job board runs on one of a handful of platforms — Greenhouse, Lever, Ashby, Workable, Recruitee, or SmartRecruiters — and every one of them exposes a public, documented JSON API. So this was never a scraping problem. It's a normalisation problem: six different response shapes, board-slug discovery, HTML-encoded descriptions in one source and raw HTML in another, epoch timestamps in milliseconds in some and ISO strings in others, and salary data structured differently everywhere it exists at all.
That's the work this does.
No browser. No proxy. No anti-bot. These are the platforms' own public endpoints, which means nothing here breaks when a page layout changes, and runs are fast and cheap.
The ATS is auto-detected
None of these platforms publishes a directory of which companies use it. Without detection you'd need to know each company's ATS in advance — so this simply tries each one and uses whichever responds. Set ats explicitly only if you want to skip the extra lookups, since detection can now mean up to five extra requests per company rather than two.
What normalisation actually means here
Concrete examples of what you don't have to handle yourself:
- Lever calls the job title
text, nottitle, and nests location, department, team and commitment undercategories. - Greenhouse returns HTML-encoded descriptions (
<p>) while Lever and Ashby return real HTML. You get real HTML from every source, plus a clean plain-text version. - Lever's dates are epoch milliseconds, confirmed live —
createdAt: 1754006400000becomes"2026-08-01T00:00:00+00:00"in the output, not a raw number sitting in a field that expects a string. Every date field across every source runs through the same conversion, whatever shape it arrives in. - Salary lives in a different place for each source that has it — Lever's
salaryRange, Ashby's nestedcompensationTiers, SmartRecruiters three levels deep insidejobAd.sections, and not at all in Greenhouse's API. One set ofsalaryMin/salaryMax/salaryCurrencyfields regardless of source. When the structured field is empty, prose is checked too — confirmed necessary on Lever, where at least one major employer discloses salary as a sentence in the description ("$60,000 - $97,000/year") rather than in the structured field. This never overrides a structured value that's actually present; it only fills a genuine gap. - SmartRecruiters descriptions need an opt-in second request per job. Confirmed against their own API spec: the description field only exists on the single-job detail endpoint, never the list endpoint this Actor normally uses. Set
fetchSmartRecruitersDescriptions: trueto fetch it — off by default, since it changes the request count (and therefore run time) for SmartRecruiters companies specifically. The other five sources already include descriptions with no extra requests.
Input
| Field | Required | Description |
|---|---|---|
companies | yes | Board slugs, one per line. A full board URL works too — the slug is taken from it. For explicit control: {"slug": "stripe", "ats": "greenhouse"} |
maxJobsPerCompany | no | Caps billable results per company. Default 500 |
The slug is the identifier in the board URL, not always the company name: boards.greenhouse.io/stripe, jobs.lever.co/palantir, jobs.ashbyhq.com/openai.
Output
One record per job:
ats · companySlug · jobId · title · location · isRemote · department · team · employmentType · descriptionHtml · descriptionText · salaryMin · salaryMax · salaryCurrency · salaryInterval · applyUrl · jobUrl · postedAt · updatedAt
Every key is always present, even when null — a field that only sometimes exists is awkward to consume programmatically.
RUN_STATS records which ATS each company resolved to, and for any company that returned nothing, which platforms were tried and what each one said — so an empty result tells you whether the board is private, empty, or on an unsupported ATS.
Pricing
Billed per job returned. A company that returns no jobs costs you nothing, and a run that finds nothing at all is never billed.
Limits, honestly
- Confidence isn't equal across all six sources. Greenhouse, Lever, Ashby, and now Workable and Recruitee have run against live company data, in this build, with real bugs found and fixed against real responses. SmartRecruiters has too, though one of its gaps (see below) turned out to be the source's own data, not this Actor's code.
- SmartRecruiters descriptions are null by default, on purpose.
jobAdonly exists on their detail endpoint, confirmed against the OpenAPI spec — setfetchSmartRecruitersDescriptions: trueto fetch it via a second request per job. departmentwill be null for some SmartRecruiters companies, and that's the source's data, not a parsing gap. Confirmed against three separate official examples that this Actor's field path is correct; a company that simply doesn't fill in the department field on its postings will show null regardless.- SmartRecruiters' public feed can be switched off per customer. Unlike the other five, a 404 from SmartRecruiters doesn't only mean "wrong slug" — it can mean the company's plan tier doesn't expose the public Postings API at all. There's no way to tell these apart from outside; both look identical.
- You need the slug. No platform offers a directory of its customers, so there's no way to enumerate companies. This turns slugs into jobs; it doesn't discover slugs.
- Only published, public jobs. Anything behind a private board or a vanity careers domain isn't visible to these endpoints.
- Salary appears only where the employer published it in a structured field. Greenhouse's API doesn't expose compensation at all, and at least one employer is confirmed to disclose salary as prose in the description instead of in Lever's structured field — this Actor doesn't parse free text for numbers.
For maintainers
Greenhouse, Lever and Ashby have live confirmation behind them — real bugs (a location field leaking a dict instead of a string, epoch-millisecond timestamps failing dataset validation) were found and fixed against real company data during this build. Workable, Recruitee and SmartRecruiters were built from published API documentation only, with no network path to those APIs from the build environment — field names for these three are the published or third-party-documented shape, not a captured live payload. Every field on every source still goes through the same tolerant lookup (pick, pick_list) that caught the Lever surprise, precisely because that gap is real and this is how it gets closed the next time it matters.
The first live run of each new source is the real test. If fields come back null, check RUN_STATS.noJobsFound first — it names which ATS were tried and what each returned.