Multi-ATS Jobs: Greenhouse, Lever, Ashby, SmartRecruiters avatar

Multi-ATS Jobs: Greenhouse, Lever, Ashby, SmartRecruiters

Pricing

from $1.00 / 1,000 job returned with descriptions

Go to Apify Store
Multi-ATS Jobs: Greenhouse, Lever, Ashby, SmartRecruiters

Multi-ATS Jobs: Greenhouse, Lever, Ashby, SmartRecruiters

Greenhouse, Lever, Ashby and SmartRecruiters in one normalized schema, with real delta tracking: each job is marked new, updated, or unchanged since your last run. Includes Ashby salary bands. $1 per 1,000 jobs, descriptions included never billed for a job that did not reach your dataset.

Pricing

from $1.00 / 1,000 job returned with descriptions

Rating

0.0

(0)

Developer

Donovan

Donovan

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

20 hours ago

Last modified

Share

Multi-ATS Job Scraper

Scrape job postings from Greenhouse, Lever, Ashby, and SmartRecruiters in one run, normalized into a single schema. Point it at a list of company boards and get back consistent job records regardless of which platform each company uses.

Built for hiring-signal feeds, recruiting tools, job aggregators, and sales teams targeting companies that are actively hiring.

What sets this apart

There are several Multi-ATS scrapers on the store covering the same four platforms. Where this one is different:

  • Real change tracking, not just a fresh dump. deltaMode marks every job new, updated, or unchanged against the previous run, using each platform's own update timestamp where one exists and a content fingerprint where it doesn't (Lever and Ashby expose neither). Schedule it daily and you get a stream of what actually changed, not the same thousand postings re-processed every morning.
  • Salary data where it exists. Ashby's API exposes a compensation band on most postings; this actor surfaces it as a structured { currency, min, max, period } object instead of dropping it or leaving it buried in the description text.
  • $1 per 1,000 jobs, descriptions included. Comparable actors in this category typically charge a flat $2–3 per 1,000 jobs regardless of whether you want descriptions. Here, descriptions cost the same as titles-only, and the per-board fee is separate so delta-mode reruns stay cheap.
  • You're not billed for data you didn't get. If a run hits your spending cap mid-scrape, it stops cleanly instead of continuing to "scrape" jobs it can no longer charge for or deliver — pushes and charges are computed from the same number, and stopped_at_charge_limit in the run summary tells you when a result set is partial. In delta mode, anything not delivered stays unmarked so it comes back on your next run instead of vanishing.

Why this instead of four separate scrapers

Every ATS returns a different shape. Greenhouse nests locations under offices, Lever puts them in categories.allLocations, Ashby uses secondaryLocations, SmartRecruiters splits city/region/country. This actor flattens all of that into one job record, so your downstream code never branches on which platform a posting came from.

It also uses each platform's public JSON API directly. No headless browsers, no proxies, no anti-bot workarounds, which keeps runs fast and cheap.

Delta mode: turn it into a hiring-signals feed

Set deltaMode: true and the actor returns only jobs that are new or changed since your last run. State persists between runs in a key-value store keyed by job ID.

Schedule it daily and you get a clean stream of "who started hiring for what today" instead of re-processing the same thousands of postings every morning. Every returned record carries a delta_status of new or updated.

Input

{
"companies": [
{ "ats": "greenhouse", "slug": "stripe", "name": "Stripe" },
{ "ats": "lever", "slug": "ro", "name": "Ro" },
{ "ats": "ashby", "slug": "ramp", "name": "Ramp" },
{ "ats": "smartrecruiters", "slug": "BoschGroup", "name": "Bosch" }
],
"deltaMode": true,
"includeDescription": true,
"filterKeywords": ["engineer", "security"],
"filterScope": "title",
"maxJobsPerCompany": 500
}
FieldTypeDefaultWhat it does
companiesarrayrequiredBoards to scrape. Each needs ats and slug; name is optional labelling.
deltaModebooleanfalseReturn only new or changed postings since the last run.
includeDescriptionbooleantrueInclude the full job description.
filterKeywordsstring[][]Keep only jobs matching at least one keyword (case-insensitive).
filterScopeenumtitleMatch against title, or title_and_description.
maxJobsPerCompanyintegerunlimitedCap jobs per board. Useful for testing and cost control.

Finding a company's slug

It's in the company's careers URL:

ATSCareers URLSlug
Greenhouseboards.greenhouse.io/stripestripe
Leverjobs.lever.co/roro
Ashbyjobs.ashbyhq.com/rampramp
SmartRecruitersjobs.smartrecruiters.com/BoschGroupBoschGroup

SmartRecruiters identifiers are case-sensitive and must match exactly. BoschGroup returns results; bosch silently returns zero. If a SmartRecruiters board comes back empty, check capitalization first.

Output

Every record, from every platform, looks like this:

{
"id": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"ats": "ashby",
"company": { "slug": "ramp", "name": "Ramp" },
"title": "Security Engineer, Cloud",
"department": "Engineering",
"team": "Backend",
"locations": ["New York, NY (HQ)", "Remote (US)", "Remote (Canada)", "Miami, FL"],
"employment_type": "FullTime",
"remote": true,
"posted_at": "2026-04-07T17:12:35.753Z",
"updated_at": null,
"url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"description_html": "<div>...</div>",
"description_text": "About Ramp\n\nRamp is a financial operations platform...",
"compensation": {
"currency": "USD",
"min": 211400,
"max": 290600,
"period": "1 YEAR"
},
"raw_source": "ashby",
"scraped_at": "2026-08-01T22:14:03.118Z",
"delta_status": "new"
}

Fields a platform doesn't provide come back as null rather than being omitted, so the schema is stable across sources.

Platform coverage and limitations

Being upfront about what each source actually gives you:

GreenhouseLeverAshbySmartRecruiters
DescriptionsInline, freeInline, freeInline, freeExtra request per job
CompensationNot exposedNot exposedYesNot exposed
updated_atYesNoNoNo
Requests per company1111 per 100 jobs, plus 1 per description

Two things worth knowing:

Only Greenhouse exposes a real update timestamp. For the other three, delta mode detects changes using a content fingerprint (title, locations, department, employment type, compensation, description length). That reliably catches meaningful edits, but a change confined to description wording of identical length won't register.

SmartRecruiters is the expensive one. Its list endpoint omits the ad body, so each description needs its own request. Set includeDescription: false for SmartRecruiters-heavy runs if you only need titles and locations.

Pricing

Pay per event:

EventPrice
Actor started$0.00005
Company board crawled$0.002
Job returned (no description)$0.001
Job returned (with description)$0.001

That works out to $1 per 1,000 jobs, descriptions included. A 200-job Greenhouse board with full descriptions costs about $0.20. In delta mode after the first run you pay the per-board fee plus only the postings that actually changed, so a daily feed over a dozen boards usually lands in the low cents.

Jobs are charged only after they're written to the dataset, so you're never billed for a record you can't see.

If you set a maximum cost for the run, the actor stops cleanly once it's reached rather than scraping boards whose results would be discarded. The run summary reports stopped_at_charge_limit: true so you know the result set is partial, and in delta mode the jobs it couldn't deliver stay unrecorded and come back on the next run instead of being silently skipped.

Reliability

  • Exponential backoff with jitter on 429 and 5xx, honoring Retry-After.
  • A bad company slug fails that board only, with a clear error naming the slug. Other boards in the same run continue.
  • Individual SmartRecruiters description fetches that fail emit the job without a description instead of dropping it.
  • Every run writes a RUN_SUMMARY record to the key-value store with counts, per-board failures, and warnings.
  • The run only fails outright if every board failed, so a partial result is never silently reported as success.

Local development

bun install
bun run typecheck
bun run src/main.ts # reads storage/key_value_stores/default/INPUT.json