ATS Jobs Scraper - Greenhouse, Lever & Ashby in One Schema avatar

ATS Jobs Scraper - Greenhouse, Lever & Ashby in One Schema

Pricing

from $1.05 / 1,000 job scrapeds

Go to Apify Store
ATS Jobs Scraper - Greenhouse, Lever & Ashby in One Schema

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

John J. K. Omolo

Maintained by Community

Actor 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, not title, and nests location, department, team and commitment under categories.
  • 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: 1754006400000 becomes "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 nested compensationTiers, SmartRecruiters three levels deep inside jobAd.sections, and not at all in Greenhouse's API. One set of salaryMin / salaryMax / salaryCurrency fields 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: true to 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

FieldRequiredDescription
companiesyesBoard slugs, one per line. A full board URL works too — the slug is taken from it. For explicit control: {"slug": "stripe", "ats": "greenhouse"}
maxJobsPerCompanynoCaps 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. jobAd only exists on their detail endpoint, confirmed against the OpenAPI spec — set fetchSmartRecruitersDescriptions: true to fetch it via a second request per job.
  • department will 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.