Multi-ATS Jobs Scraper: One Normalized Feed for Greenhouse, Lever & Ashby
Greenhouse, Lever, and Ashby together power the hiring pipelines of a huge share of tech companies — but they're three different APIs with three different field names, and you don't always know upfront which one a given company runs on. If you're building a hiring-intel dataset, a job aggregator, or a "who's hiring" signal for sales, that ambiguity is the actual bottleneck, not the scraping itself. This post covers how the Multi-ATS Jobs Scraper collapses all three into one schema.
Why cross-ATS job data is worth pulling
Each platform publishes structured, public postings data — no login needed to see them, since they're built for candidates to browse:
-
Greenhouse:
boards-api.greenhouse.io/v1/boards/{slug}/jobs -
Lever:
api.lever.co/v0/postings/{slug}?mode=json -
Ashby:
api.ashbyhq.com/posting-api/job-board/{slug}
That's useful for:
- HR-tech builders assembling cross-company hiring datasets without hand-picking which API to call per employer
- SDR / sales-intel teams using "who is hiring for X" as a buying-intent signal
- Job-aggregator and ATS-analytics platforms that need one schema instead of three
The naive approach, and where it falls apart
Say you want Stripe, Palantir, and Ramp's open roles. Stripe runs Greenhouse, Palantir runs Lever, Ramp runs Ashby — and nothing about a bare company name tells you that upfront. You'd need to either maintain a lookup table of company-to-ATS mappings (which goes stale) or probe each API in turn and see what returns a 200. Then, even once you know which platform, the field names don't line up: Greenhouse has no team field distinct from department; Lever and Ashby do. Only Ashby exposes structured salary. Only Greenhouse reliably reports updated_at. Writing three separate parsers and reconciling them into one row shape is the actual work — not any single API call.
What we built instead
The Multi-ATS Jobs Scraper takes a bare company slug ("stripe"), a full careers URL ("https://jobs.lever.co/palantir"), or an explicit {atsType, companySlug} object if you already know the platform — and auto-detects which of the three it's dealing with when you don't tell it. Every row comes back in one consistent schema regardless of source platform, with curl-cffi fingerprint impersonation, retry-with-backoff on 429/5xx, and Apify Proxy session rotation absorbing the parts that make a large multi-company sync fragile.
Run it via the Apify Python SDK:
from apify_client import ApifyClient
client = ApifyClient("APIFY_TOKEN")
run = client.actor("DevilScrapes/multi-ats-jobs-scraper").call(run_input={
"companies": [
{"atsType": "greenhouse", "companySlug": "stripe"},
"https://jobs.lever.co/palantir",
"https://jobs.ashbyhq.com/ramp",
],
"maxItemsPerCompany": 5,
"includeDescription": True,
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item["company"], item["ats"], item["title"], item["salary_min"])
Or paste the same array into the Store page input form directly.
Sample output row:
{
"company": "ramp",
"ats": "ashby",
"job_id": "3b1c2e9a-0000-0000-0000-000000000000",
"title": "Senior Software Engineer, Payments",
"department": "Engineering",
"team": "Payments",
"location": "New York City",
"remote": false,
"salary_min": 211400,
"salary_max": 290600,
"salary_currency": "USD",
"url": "https://jobs.ashbyhq.com/ramp/3b1c2e9a-0000-0000-0000-000000000000",
"posted_at": "2026-05-02T18:11:00.000Z",
"updated_at": null,
"description_html": "<p>About the role...</p>"
}
The technically interesting bit
Auto-detection sounds like a simple try-each-API-in-turn loop, and mostly it is — but the edge case is a slug that happens to resolve on more than one platform. It's rare (we haven't hit it in practice), but when it happens we keep the platform with more open postings and log a warning rather than silently picking one arbitrarily. If you need a guarantee rather than a best-effort tie-break, the explicit {atsType, companySlug} form skips detection entirely and runs slightly faster. We'd rather document that edge case than pretend detection is infallible.
Pricing — no fine print
Pay-Per-Event: $0.005 per run plus $0.0015 per job posting written to the dataset — $1.50 / 1,000 results. A run collecting 500 postings across three companies costs roughly $0.75 in result charges plus the $0.005 start fee. Empty runs cost only the start fee. New Apify accounts get $5 of free credit, no card required.
Honest limitations
Salary is Ashby-only — Greenhouse and Lever don't publish a structured comp field, and we won't regex-guess one out of free-text descriptions, so salary_min/salary_max/salary_currency are honestly null for those platforms. team is Lever/Ashby only since Greenhouse doesn't split team from department. And this only reaches public, unauthenticated boards — internal/private listings need employer credentials this Actor doesn't support.
Try it, and the rest of the fleet
Greenhouse, Lever, and Ashby are three platforms in a bigger landscape. We also run the Workday Jobs Scraper, the SmartRecruiters Jobs Scraper, and the Workable Jobs Scraper — same keyless, pay-per-result approach, covering the rest of the major ATS market.
Grab the Multi-ATS Jobs Scraper on the Apify Store and try it with the free $5 credit. Found a company slug that returns wrong results, or hit an ATS quirk we don't handle? Open a support ticket or leave a review — we read every one. Browse the rest of the fleet at apify.com/DevilScrapes.
Which three companies would you point this at first?
Top comments (0)