Startup Jobs Aggregator - 3 ATS in One avatar

Startup Jobs Aggregator - 3 ATS in One

Pricing

from $4.00 / 1,000 job scrapeds

Go to Apify Store
Startup Jobs Aggregator - 3 ATS in One

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

Renzo Madueno

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

6 days ago

Last modified

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, not title
  • Greenhouse nests location as location.name; Ashby uses a flat location; Lever buries it in categories.location
  • Ashby's employment type is FullTime; Lever's is Full-time; Greenhouse has none at all
  • Ashby publishes structured salary numbers; the other two do not
  • Ashby has a real isRemote boolean; 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:

  1. Hard — same source, company and job id.
  2. 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):

FieldGreenhouseAshbyLever
title, jobId, location, locations[]100%100%100%
jobUrl, applyUrl, boardUrl100%100%100%
postedAt100%100%100%
descriptionText100%100%100%
department100%100%100%
team100%100%
employmentType100%98.7%
country99.8%100%
workplaceType25.1%77.6%100%
isRemote from the ATS itself76.7%100%
salaryMin57.0%79.9%64.3%
— of which structured0%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/currencyCode numbers the company typed into their ATS.
  • Greenhouse's public board API exposes no salary field at all. We checked pay_input_ranges on all 2,628 postings: populated on zero. The 57% figure is parsed out of the description text.
  • Lever has a salaryRange field 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 as false.

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 ATSs
https://job-boards.greenhouse.io/stripe pinned to Greenhouse
https://jobs.ashbyhq.com/ramp pinned to Ashby
https://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

OptionWhat it does
maxItemsHard ceiling on rows written across every company and ATS. This is your spend cap.
maxJobsPerCompanyKeeps a 798-posting Databricks board from consuming a 20-company run
sourcesRestrict to greenhouse, ashby, lever — fewer probes, faster and cheaper
stopAtFirstMatchOn by default; off catches mid-migration companies
salaryOnlyOnly rows with a populated salaryMin
remoteOnlyOnly isRemote: true
employmentTypesNormalised across all three: FULL_TIME, PART_TIME, INTERNSHIP, CONTRACT
titleKeywords / locationKeywords / departmentKeywordsSubstring matching
postedAfterISO date
dedupeCross-ATS deduplication, on by default
concurrencyCompanies 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_exhaustedmaxItems was 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.


  • Greenhouse Jobs Scraper — plus educationRequirement and internalJobId
  • Ashby Jobs Scraper — plus compensationPublic and unlisted pipelines
  • Lever Jobs Scraper — plus structured requirementLists
  • Workday Jobs Scraper — the enterprise side, by career-site URL

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.