Startup Jobs Aggregator - 3 ATS in One
Pricing
from $4.00 / 1,000 job scrapeds
Startup Jobs Aggregator - 3 ATS in One
Give it company handles; it finds each one on Greenhouse, Ashby or Lever and returns every open role in one deduplicated, normalised schema. No API key, no login, no proxy.
Pricing
from $4.00 / 1,000 job scrapeds
Rating
0.0
(0)
Developer
Renzo Madueno
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
6 days ago
Last modified
Categories
Share
Startup Jobs Aggregator — Greenhouse, Ashby & Lever in One Run
Give it company names. It works out which applicant tracking system each one uses, pulls every open role, and returns them all in one normalised, deduplicated schema. No API key, no login, no cookies, no proxy, no browser.
{ "companies": ["stripe", "ramp", "spotify", "notion", "linear"] }
That is the whole input. Stripe resolves on Greenhouse, Ramp and Notion and Linear on Ashby, Spotify on Lever — and you get one dataset where salaryMin means the same thing in every row.
The problem this solves
Scraping job boards means knowing which ATS a company uses before you can ask for its jobs. That is a research task per company, and it changes when companies migrate. Then each ATS returns a different shape:
- Lever calls the job title
text, nottitle - Greenhouse nests location as
location.name; Ashby uses a flatlocation; Lever buries it incategories.location - Ashby's employment type is
FullTime; Lever's isFull-time; Greenhouse has none at all - Ashby publishes structured salary numbers; the other two do not
- Ashby has a real
isRemoteboolean; Greenhouse has nothing of the kind - Dates arrive as ISO strings, as epoch milliseconds, and as different field names
This Actor probes all three platforms per handle, then flattens everything into one schema with normalised enums, so a downstream filter like employmentType === "FULL_TIME" && isRemote works identically across all three sources.
How the discovery works
A plain handle like ramp is tried against every enabled ATS, because a company's Greenhouse token, Ashby board name and Lever slug are almost always the same word. A full ATS URL pins the handle to that one platform and skips the probing.
By default the Actor stops at the first ATS that returns jobs (stopAtFirstMatch: true), because a company almost always lives on exactly one. Turn it off to catch companies mid-migration that still have roles live on two — the cross-ATS deduplication below is built for exactly that case.
Every company that matches nothing is reported as no_ats_match in FAILURES, and a COMPANY_MAP record tells you which platform each handle resolved to:
{"stripe": { "jobs": 10, "foundOn": ["greenhouse"] },"ramp": { "jobs": 10, "foundOn": ["ashby"] },"spotify": { "jobs": 10, "foundOn": ["lever"] },"zzznotarealcompany": { "jobs": 0, "foundOn": [] }}
That map is worth keeping. Run it once over your company list and you have an ATS census you never have to rebuild.
Deduplication across platforms
Two layers:
- Hard — same source, company and job id.
- Soft — same company, title and location, even when the ids differ and the platforms differ.
The soft layer is what catches a company that migrated from Lever to Ashby and left the same twelve roles live on both. Without it you would silently double-count. duplicatesDropped in the FAILURES record tells you how many were removed, and you are never charged for a dropped duplicate.
What you get, with the fill rate actually measured
Fill rates depend on which ATS a row came from, and pretending otherwise would be dishonest. Measured across 4,316 live postings — 2,628 on Greenhouse (9 boards), 1,234 on Ashby (7 boards), 454 on Lever (3 sites):
| Field | Greenhouse | Ashby | Lever |
|---|---|---|---|
title, jobId, location, locations[] | 100% | 100% | 100% |
jobUrl, applyUrl, boardUrl | 100% | 100% | 100% |
postedAt | 100% | 100% | 100% |
descriptionText | 100% | 100% | 100% |
department | 100% | 100% | 100% |
team | — | 100% | 100% |
employmentType | — | 100% | 98.7% |
country | — | 99.8% | 100% |
workplaceType | 25.1% | 77.6% | 100% |
isRemote from the ATS itself | — | 76.7% | 100% |
salaryMin | 57.0% | 79.9% | 64.3% |
| — of which structured | 0% | 71.8% | 0% |
Read the last two rows carefully, because they are the whole story of this category:
- Ashby is the only one of the three with a structured pay field. 71.8% of its postings carry exact
minValue/maxValue/currencyCodenumbers the company typed into their ATS. - Greenhouse's public board API exposes no salary field at all. We checked
pay_input_rangeson all 2,628 postings: populated on zero. The 57% figure is parsed out of the description text. - Lever has a
salaryRangefield that nobody populates — 0% across 454 postings. Its 64.3% is also parsed from text.
Every row carries salarySource, so you always know which kind you have: ats_structured, ats_summary or description. Filter on it if your use case needs certainty. Set salaryOnly: true to drop rows with no range.
Fields absent for a given source are absent, not null-filled — Greenhouse genuinely has no employment type or country to give.
Remote flags, and what we refuse to guess
isRemote is on every row, with remoteSource recording how we know:
ats_field— the platform's own answer. 100% of Lever rows, 76.7% of Ashby rows, none on Greenhouse.inferred_location— the location text says so ("US-Remote","Remote in the US"). 27% of Greenhouse rows.unknown— returned asfalse.
We do not infer remoteness from the description body. Measured on 2,245 Greenhouse postings, that method flagged 95 roles as remote whose location was Singapore, Dublin or Bengaluru, because it was matching company boilerplate about being "remote-friendly". That describes the employer, not the job. The inference was removed rather than shipped with a caveat.
Input
{"companies": ["stripe", "ramp", "spotify", "notion", "linear"],"sources": ["greenhouse", "ashby", "lever"],"maxItems": 1000,"salaryOnly": false,"remoteOnly": false,"titleKeywords": ["engineer"],"stopAtFirstMatch": true}
Accepted company formats
ramp probed on all enabled ATSshttps://job-boards.greenhouse.io/stripe pinned to Greenhousehttps://jobs.ashbyhq.com/ramp pinned to Ashbyhttps://jobs.lever.co/spotify pinned to Lever
Input aliases
- companies:
companies,company,boards,companyUrls,startUrls,tokens - limit:
maxItems,maxResults,limit,maxJobs - sources:
sources,source,ats,platforms - keywords:
titleKeywords,keywords,searchTitle - locations:
locationKeywords,locations,location - departments:
departmentKeywords,departments,department - salary filter:
salaryOnly,onlyWithSalary - descriptions:
includeDescription,includeContent,fullDescription
Filters, applied identically across all three sources
| Option | What it does |
|---|---|
maxItems | Hard ceiling on rows written across every company and ATS. This is your spend cap. |
maxJobsPerCompany | Keeps a 798-posting Databricks board from consuming a 20-company run |
sources | Restrict to greenhouse, ashby, lever — fewer probes, faster and cheaper |
stopAtFirstMatch | On by default; off catches mid-migration companies |
salaryOnly | Only rows with a populated salaryMin |
remoteOnly | Only isRemote: true |
employmentTypes | Normalised across all three: FULL_TIME, PART_TIME, INTERNSHIP, CONTRACT |
titleKeywords / locationKeywords / departmentKeywords | Substring matching |
postedAfter | ISO date |
dedupe | Cross-ATS deduplication, on by default |
concurrency | Companies probed in parallel, 1–15, default 6 |
Output sample
Every row has the same keys regardless of which platform it came from:
{"source": "ashby","companyToken": "ramp","companyName": "ramp","jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245","title": "Security Engineer, Cloud","department": "Engineering","departments": ["Engineering", "Backend"],"team": "Backend","employmentType": "FULL_TIME","location": "New York, NY (HQ)","locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],"country": "USA","isRemote": true,"workplaceType": "HYBRID","remoteSource": "ats_field","salaryMin": 211400,"salaryMax": 290600,"salaryCurrency": "USD","salaryInterval": "YEAR","salaryText": "$211.4K – $290.6K • Offers Equity","salarySource": "ats_structured","postedAt": "2026-04-07T17:12:35.753+00:00","jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245","applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application","boardUrl": "https://jobs.ashbyhq.com/ramp","descriptionText": "ABOUT RAMP ...","scrapedAt": "2026-08-22T04:16:40.117Z"}
Four saved dataset views ship with the Actor: Job overview, Salary ranges, Remote & locations and By ATS source.
What you can build with this
A startup job board. One run over a few hundred portfolio companies gives you every open role with apply links, refreshed on a schedule.
A compensation dataset. Set salaryOnly: true and filter salarySource === "ats_structured" for a clean corpus of company-published ranges with currency and interval attached.
Recruiting and competitive intelligence. Run the same list weekly and diff on jobId. New ids are new reqs; disappeared ids are filled or pulled. Hiring velocity by department, without a vendor.
An ATS census. The COMPANY_MAP record tells you which platform each company uses — useful in its own right if you sell to recruiting teams.
Lead lists. department, team and location tell you how a company is structured and where it is growing.
How errors are handled
Errors never enter your dataset. Charging for a row that says "this failed" is charging for an error message. Everything goes to the FAILURES record in the run's key-value store:
{"runFailed": false,"companiesRequested": 5,"companiesMatched": 4,"companiesUnmatched": 1,"jobsDelivered": 40,"jobsBySource": { "greenhouse": 10, "ashby": 20, "lever": 10 },"duplicatesDropped": 0,"byReason": { "no_ats_match": 1 }}
Reasons:
no_ats_match— not found on any enabled platform. Either the company uses a different ATS (Workday, SmartRecruiters, Taleo) or its handle differs from its name. This is the normal outcome for a chunk of any real company list, not a bug.board_empty— found on that ATS, but zero open roles right now. Distinct from not being found at all.unresolvable_input— could not derive a handle.budget_exhausted—maxItemswas reached before this company was written. Nothing is skipped silently.fetch_failed— network or upstream error after retries.
If nothing was delivered, the run ends FAILED with the reason breakdown in the message.
Pricing and the free tier
Pay per event:
- Actor start — one small charge per gigabyte of memory
- Job scraped — charged after the row is written to the dataset
Rows are pushed in batches and the per-job event fires only for rows that landed. Probing a company that turns out not to be on any of the three platforms costs you nothing — no row, no charge. Same for filtered-out postings and deduplicated repeats.
The free tier returns real data. No API key, no proxy, no credentials of any kind — all three upstreams are public JSON endpoints. A free account running {"companies": ["ramp"], "maxItems": 10} gets ten real jobs with real structured salary ranges. Nothing in the code path throws because you lack a paid feature.
Speed
Each company costs at most one HTTP request per enabled ATS, and with stopAtFirstMatch on it is usually one or two. All requests are gzip-compressed and run with bounded concurrency. Measured: 1,234 Ashby postings across 7 boards in 1.4 seconds, and a mixed 5-company probe across all three platforms in about 3 seconds.
If you are running hundreds of companies with descriptions on, raise memory to 2 GB and consider includeDescription: false when you only need the metadata.
Common questions
What if a company is on Workday or SmartRecruiters? It comes back no_ats_match. This Actor covers the three startup ATSs. For enterprise Workday sites, use the Workday Jobs Scraper in this same fleet.
Should I use this or the single-ATS Actors? Use this when you have company names and do not know their ATS. Use a single-ATS Actor when you already know, or when you want a platform-specific field this one does not normalise — Greenhouse's educationRequirement, Lever's structured requirementLists, Ashby's compensationPublic.
Why did a company match nothing when I know it uses Greenhouse? Its board token probably differs from its name. Pass the full board URL instead of the bare handle.
Can a company appear on two platforms? Yes, during a migration. Set stopAtFirstMatch: false to collect both; soft deduplication removes the roles that are genuinely the same.
Are the enums really consistent? Yes. employmentType normalises FullTime and Full-time to FULL_TIME; workplaceType is always REMOTE, HYBRID or ONSITE; salaryInterval is always YEAR, MONTH, WEEK, DAY or HOUR. Non-standard Lever labels like PERMANENT are passed through uppercased rather than forced into a bucket where they would be wrong.
How fresh is the data? Live on every run. Nothing is cached.
Related Actors in this fleet
- Greenhouse Jobs Scraper — plus
educationRequirementandinternalJobId - Ashby Jobs Scraper — plus
compensationPublicand unlisted pipelines - Lever Jobs Scraper — plus structured
requirementLists - Workday Jobs Scraper — the enterprise side, by career-site URL
Legal
This Actor reads public, unauthenticated JSON endpoints that Greenhouse, Ashby and Lever each publish so job boards and aggregators can syndicate postings. It sends no credentials, solves no challenges and bypasses no access control. You are responsible for how you use the data, including any applicable data-protection rules.