ATS Job Feed — Greenhouse, Lever, Ashby, Workday & More avatar

ATS Job Feed — Greenhouse, Lever, Ashby, Workday & More

Pricing

from $1.50 / 1,000 jobs

Go to Apify Store
ATS Job Feed — Greenhouse, Lever, Ashby, Workday & More

ATS Job Feed — Greenhouse, Lever, Ashby, Workday & More

Jobs straight from six public ATS APIs (Greenhouse, Lever, Ashby, Workable, Personio, Workday) — no scraping, no login, no keys. Normalises salaries to annual USD, de-duplicates across boards, and can return only jobs you have not seen yet.

Pricing

from $1.50 / 1,000 jobs

Rating

0.0

(0)

Developer

Radar Labs

Radar Labs

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

6 minutes ago

Last modified

Share

ATS Job Feed — jobs straight from company career pages

Most "job scraper" actors screen-scrape a job board. This one doesn't scrape at all: it reads the same public JSON endpoints the companies' own career pages use, on six applicant-tracking systems, all of which are keyless and login-free.

Numbers, limits and public sample data: https://sjdfi0wjvosdju9.github.io/

ATSEndpoint usedTypical companies
Greenhouseboards-api.greenhouse.io/v1/boards/{slug}/jobsStripe, GitLab, Figma, Datadog, Duolingo
Leverapi.lever.co/v0/postings/{slug}many mid-size startups
Ashbyapi.ashbyhq.com/posting-api/job-board/{slug}OpenAI, Notion, Linear
Workableapply.workable.com/api/v1/widget/accounts/{slug}SMBs
Personio{slug}.jobs.personio.de/xmlGerman-speaking mid-market
Workday{tenant}.wd{number}.myworkdayjobs.com/wday/cxs/...large enterprises (NVIDIA, Salesforce, Intel)

You give it company names. It works out which of the six each company uses (404 on one, try the next), and returns a single flat table.

Look at real output before you pay

No sign-up needed — this is a live 200-row sample from a default run, readable anonymously:

Every row carries company, ats, title, location, remote, department, postedAt, and — where the company publishes one — an annual salaryLow/salaryHigh in local currency plus salaryUsdLow/salaryUsdHigh.

Why it is built this way

I wrote up the measurements behind this Actor — 527 guessed slugs, 259 live boards, 18,825 postings, 8,071 with a pay range, and the two bugs that published salaries off by 12x and 2,080x — in a post you can read before spending anything:

Six APIs, zero keys: what 18,825 real job postings contain

What makes the output usable

  • Salary is normalised, not guessed. Ashby and Lever publish salary as structured numbers, so they are read as numbers, never regex-guessed: currency, interval and min/max come from the API. Greenhouse and Workday publish pay inside the job description instead, so those are read from the text — but only from a small window around a pay cue (salary range, base pay, compensation range, per hour, …), and only when the unit word sits right next to the amount. On a full sweep of 260 companies this lifted salary coverage from 15.6% to 44% of all rows (4,207 Greenhouse rows alone). Everything is converted to an annual range (per hour × 2080, per month × 12) and also reported in USD using a fixed exchange-rate table.
  • No naked numbers. When a salary has to be read out of text, the number must sit next to a currency symbol or currency code. Without that rule, a job description mentioning "970 employees" becomes a $970 salary — which is how most scrapers produce garbage salary columns.
  • De-duplicated across boards. The same role posted under two locations, or listed twice by the company, collapses into one row, keeping the most complete version.
  • Failure is reported, not hidden. Companies that return nothing land in failedCompanies in the summary with the reason. One dead slug never aborts the run.

Input

FieldDefaultNotes
companies13 well-known slugsslugs or full career-page URLs (Workday needs the URL)
keywordsnonematch title, department or summary
atsautoforce one ATS if you already know it
remoteOnlyfalselocation or workplace type says remote
minSalaryUsd0drops jobs without a published salary when set
includeDescriptionstruedownload Greenhouse job bodies — the only source of Greenhouse salaries, and the main cost
salaryOnlyfalsedrop every job with no published pay range — see below
excludeSenior / excludeJunior / excludeNonTechfalsetitle-based role filters
maxAgeDays00 keeps evergreen postings
maxJobs200output cap after ranking
maxPerCompany20stops one high-paying employer from flooding the feed (0 = off)
onlyNewfalseskip every job this account already received in an earlier run
resetSeenfalseforget the history first, so the next run returns everything again
workdayPayRangestrueopen up to 20 Workday postings per company to recover the pay range
useVerifiedPoolfalsescan the bundled 266 verified companies instead of your own list
maxWorkers6companies fetched in parallel (1–10); output keeps the input order

The 266 companies behind useVerifiedPool

We scanned 527 candidate slugs against all six boards, kept the 259 that answered, then re-probed 267 misses with spelling variants and added 7 more — 266 boards in total. The latest full scan read 18,670 jobs, of which 8,040 (43%) carried a pay range, across 165 companies that publish pay at least once. Every number below is regenerated weekly by the Actor's own pipeline (docs/COMPANIES.md is generated from the same pool file), so it cannot drift from what the code actually ships.

BoardCompaniesJobsWith salaryShare
Greenhouse13911,7545,10143%
Ashby1005,3352,73551%
Lever231,8101649%
Workable41210see note

Workable's four boards are real but their public widget endpoint rate-limits by IP; at the time of this scan they answered 429 and are reported as rate-limited: … in failedCompanies instead of being counted as "no pay". The other three boards are measured, not estimated.

Top 40 by number of salary-bearing jobs:

CompanyBoardJobsWith salary
openaiAshby808644
anthropicGreenhouse679589
databricksGreenhouse902486
elasticGreenhouse391250
snowflakeAshby354241
brazeGreenhouse331235
oscarGreenhouse245232
brexGreenhouse265206
affirmGreenhouse224196
coinbaseGreenhouse225182
rampAshby170163
airwallexAshby510160
klaviyoGreenhouse164160
plaidAshby132126
robinhoodGreenhouse156125
mongodbGreenhouse388124
scaleaiGreenhouse173118
instacartGreenhouse123113
fivetranGreenhouse166108
perplexityAshby127107
gitlabGreenhouse231102
basetenAshby105100
figmaGreenhouse13981
headwayAshby8981
cohereAshby11477
clickhouseAshby20276
adyenGreenhouse24475
chimeGreenhouse7371
mercuryGreenhouse7271
asanaGreenhouse10570
vercelGreenhouse9570
faireGreenhouse7170
temporalAshby7067
lyrahealthLever47364
flexportGreenhouse19364
deepgramAshby8964
rippleGreenhouse9661
tailscaleGreenhouse5953
swordhealthGreenhouse5450
vantaAshby8448

…and 219 more, including zocdoc, lightningai, sofi, pagerduty, sardine, smartsheet, sentry, modal, kong, togetherai, onetrust, hex, …

Nothing here is a partnership or an endorsement — every number comes from the companies' own public job feeds, measured on a single scan day. Companies add and remove boards, so a slug that works today can 404 tomorrow; the Actor reports those per company instead of hiding them.

A full-pool scan measured 18,825 rows in ~80 seconds with maxWorkers: 6 and only one dead slug (calm, which 404s on all six boards). Because each pool entry already knows which ATS answers, the Actor skips the probing step, so a wide scan costs one request per company instead of up to four.

That is a lot of rows, so pair it with the filters:

{
"useVerifiedPool": true,
"keywords": ["engineer"],
"minSalaryUsd": 120000,
"maxJobs": 100,
"maxPerCompany": 5
}

If you do not need job descriptions (includeDescriptions)

Greenhouse is the only one of the six boards that hides salary ranges inside the job body, and those bodies are also where most of the payload sits. Set includeDescriptions: false to skip them.

Measured on the bundled pool at the time of that test (259 boards, before the pool was widened to 266 and before the de-duplication fix — use the ratios, not the absolute counts):

run time (local, 6 workers)jobs with a salaryplatform cost (same input)
includeDescriptions: true (default)138 s6,838 / 15,530 (44%)$0.00451
includeDescriptions: false66 s2,834 / 15,531 (18%)$0.00083

Two honest consequences:

  • Greenhouse salaries disappear (Ashby and Lever publish salaries as structured fields, so they are unaffected).
  • Because ranking prefers jobs with a salary, the top of the list shifts towards Ashby. That is not a bug; it is what "cheap mode" costs.

salaryOnly and minSalaryUsd force this back on — filtering by pay needs the pay.

If you only care about jobs that state a salary (salaryOnly)

Paid feeds are noisy: roughly 43% of the postings in our 266-company pool publish a pay range, so a wide scan returns mostly jobs you cannot price. Set salaryOnly: true and the Actor drops everything else before ranking:

{
"useVerifiedPool": true,
"salaryOnly": true,
"keywords": ["engineer"],
"maxJobs": 100
}

Two things make this worth knowing:

  • It makes runs cheaper, not just cleaner. You are charged per returned job, so 100 priced jobs cost the same as 100 unpriced ones — the filter costs nothing and saves you reading rows you cannot use.
  • It is honest about coverage. If a board publishes almost no pay (Workable, and Greenhouse outside the ~45% that write it in the description), salaryOnly simply returns nothing from it. SUMMARY reports scraped vs. survived, so you can see which companies are actually worth asking for.

The per-board publication rates and a cost calculator (rows in, dollars out) live here: https://sjdfi0wjvosdju9.github.io/salary-only.html. It is a static page with no tracking, and the numbers come from the same run that produces the free CSV, so you can check our arithmetic before paying us.

Only pay for jobs you have not seen (onlyNew)

Put this Actor on a schedule with onlyNew: true and it remembers which postings it already gave you. Every later run returns only the roles that appeared since the last run — you are not charged again for the same 200 rows every morning.

How it behaves, measured on the live platform:

RunSetupRows returnedJobs skipped as already seen
1stonlyNew + resetSeen100
2ndonlyNew, nothing changed upstream010
3rdonlyNew, nothing changed upstream010
  • The fingerprint of a job is company + normalised title + URL, so punctuation and casing changes do not fake a "new" posting.
  • The first run returns everything and records it. If you raise maxJobs later, the rows you did not get yet still count as new, so nothing is lost.
  • History keeps the 50,000 most recent fingerprints per account (a few hundred KB in your key-value store).
  • resetSeen: true wipes the history before running — handy after a long pause.
  • If the state store cannot be opened, the run still succeeds and simply behaves as if onlyNew was off, rather than failing.

Output

One dataset row per job:

id · company · ats · title · url · location · department · employmentType · postedAt · remote · isSenior · isJunior · isNonTech · salaryLow · salaryHigh · salaryCurrency · salaryUsdLow · salaryUsdHigh · summary

Plus a SUMMARY key-value record with scraped, afterFilters, returned, failedCompanies, onlyNew, skippedSeen, seenTracked, and byAts / byCompany breakdowns.

Free bonus: because it re-reads the company's live board on every run, a schedule turns this into a change feed — new roles show up as new rows. Add onlyNew: true and the change feed is also the cheap option: you only pay for rows that are actually new.

Workday needs a URL, and its list API has no salary

Workday is the ATS most large enterprises use, but a company name is not enough to find its board: it needs a tenant, a wd cluster and a site name. Pass the careers URL instead, for example:

https://acme.wd5.myworkdayjobs.com/en-US/AcmeCareers

A short built-in registry covers nvidia, salesforce, visa and intel, so those plain names work too.

The Workday list endpoint also refuses limit above 20, so the Actor pages through in 20s. It publishes no salary field in the list response, but the pay range is almost always written in the posting itself, so the Actor opens up to 20 postings per Workday company (newest first), reads the description, and recovers the range there. On NVIDIA that fills in 10 of the 20 opened postings, e.g. 196,000 – 310,500 USD. Turn this off with workdayPayRanges: false if you only want the list and would rather not spend the extra requests.

Two things worth knowing about the prose extraction:

  • Only numbers next to a pay cue (salary range, base pay, compensation range, hourly rate, …) count, so a $5,000 referral bonus is not mistaken for a salary.
  • When a posting lists two bands ("184,000 USD – 287,500 USD for Level 4, and 224,000 USD – 356,500 USD for Level 5"), the first band is reported. The row is still honest — it is a range the employer published for that role — but the upper band is not merged in.

Currency conversion uses live rates, and never guesses

Postings quote pay in whatever the company uses. We keep the original numbers (salaryLow, salaryHigh, salaryCurrency) and add a salaryUsd* value converted with a dated rate table fetched once per run and cached for 24 hours. The rate date is reported as fxDate in the SUMMARY record, so a number can always be traced to a day.

When a currency has no rate, the USD fields stay null instead of being estimated. Until this change only ten currencies converted, so a posting quoted in PLN, AED, MXN or RON came back looking like it had no salary at all - and salaryOnly / minSalaryUsd quietly dropped it. On the bundled 266-company pool that hid about 1,170 real postings.

Honest limits

  • Greenhouse publishes no salary field — pay, when a company discloses it, lives in the description. We parse it there (about 45% of Greenhouse rows on our sweep), so a Greenhouse row is either correctly priced or empty, never a guess. Ashby and Lever are still the most consistent: nearly every row has a range.
  • We drop implausible numbers instead of shipping them. A range that annualises below $5,000 or above $5,000,000 (or $1.5M in USD) is thrown away and the job goes out with an empty salary. Two real examples that this guard caught: a monthly figure labelled USD that annualised to $1,536,000, and an annual figure that got ×2080 because the word "hourly" appeared elsewhere in a long description.
  • Workable's public widget endpoint is often an empty shell (it returns {"jobs": []} for some accounts). Those companies will show up in failedCompanies rather than pretending to have no openings.
  • Evergreen roles exist. Some boards leave a posting up for years, so postedAt is not a freshness guarantee.
  • No salary negotiation data, no headcount, no recruiter contacts. This is the company's own published feed, nothing more.
  • Most company slugs work out of the box, but a company that uses none of these six ATSes comes back as failed. A wider sweep we ran over 527 candidate slugs hit 260 companies; on the earlier metadata-only sweep nearly all salary-bearing rows came from Ashby boards, but reading Greenhouse descriptions brings its 135 boards to 4,207 salary-bearing rows, so both are worth adding.

Pricing

Pay per event: job-returned — one charge per job that survives your filters. Filtered-out and duplicate jobs are not charged. A apify-actor-start charge applies once per run.

Compliance

All four endpoints are the public JSON APIs that the companies' own career pages call. Nothing here logs in, submits applications, or touches anything behind a login. Respect the terms of the ATS providers if you redistribute the data at scale.

Local development

cp .env-example .env # APIFY_TOKEN=...
python -m src # same entrypoint the container uses
python tests/test_ats_core.py # offline unit tests, no network

The same data as a repo

The CSV behind this Actor is also published as an open dataset (CC BY 4.0) at https://github.com/sjdfi0wjvosdju9/job-salary-open-data.