ATS Job Feed — Greenhouse, Lever, Ashby, Workday & More
Pricing
from $1.50 / 1,000 jobs
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
6 minutes ago
Last modified
Categories
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/
| ATS | Endpoint used | Typical companies |
|---|---|---|
| Greenhouse | boards-api.greenhouse.io/v1/boards/{slug}/jobs | Stripe, GitLab, Figma, Datadog, Duolingo |
| Lever | api.lever.co/v0/postings/{slug} | many mid-size startups |
| Ashby | api.ashbyhq.com/posting-api/job-board/{slug} | OpenAI, Notion, Linear |
| Workable | apply.workable.com/api/v1/widget/accounts/{slug} | SMBs |
| Personio | {slug}.jobs.personio.de/xml | German-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:
- JSON: https://api.apify.com/v2/datasets/foLhhiBMhnWcmxZJv/items?format=json&clean=true
- CSV: https://api.apify.com/v2/datasets/foLhhiBMhnWcmxZJv/items?format=csv&clean=true
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
failedCompaniesin the summary with the reason. One dead slug never aborts the run.
Input
| Field | Default | Notes |
|---|---|---|
companies | 13 well-known slugs | slugs or full career-page URLs (Workday needs the URL) |
keywords | none | match title, department or summary |
ats | auto | force one ATS if you already know it |
remoteOnly | false | location or workplace type says remote |
minSalaryUsd | 0 | drops jobs without a published salary when set |
includeDescriptions | true | download Greenhouse job bodies — the only source of Greenhouse salaries, and the main cost |
salaryOnly | false | drop every job with no published pay range — see below |
excludeSenior / excludeJunior / excludeNonTech | false | title-based role filters |
maxAgeDays | 0 | 0 keeps evergreen postings |
maxJobs | 200 | output cap after ranking |
maxPerCompany | 20 | stops one high-paying employer from flooding the feed (0 = off) |
onlyNew | false | skip every job this account already received in an earlier run |
resetSeen | false | forget the history first, so the next run returns everything again |
workdayPayRanges | true | open up to 20 Workday postings per company to recover the pay range |
useVerifiedPool | false | scan the bundled 266 verified companies instead of your own list |
maxWorkers | 6 | companies 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.
| Board | Companies | Jobs | With salary | Share |
|---|---|---|---|---|
| Greenhouse | 139 | 11,754 | 5,101 | 43% |
| Ashby | 100 | 5,335 | 2,735 | 51% |
| Lever | 23 | 1,810 | 164 | 9% |
| Workable | 4 | 121 | 0 | see 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:
| Company | Board | Jobs | With salary |
|---|---|---|---|
openai | Ashby | 808 | 644 |
anthropic | Greenhouse | 679 | 589 |
databricks | Greenhouse | 902 | 486 |
elastic | Greenhouse | 391 | 250 |
snowflake | Ashby | 354 | 241 |
braze | Greenhouse | 331 | 235 |
oscar | Greenhouse | 245 | 232 |
brex | Greenhouse | 265 | 206 |
affirm | Greenhouse | 224 | 196 |
coinbase | Greenhouse | 225 | 182 |
ramp | Ashby | 170 | 163 |
airwallex | Ashby | 510 | 160 |
klaviyo | Greenhouse | 164 | 160 |
plaid | Ashby | 132 | 126 |
robinhood | Greenhouse | 156 | 125 |
mongodb | Greenhouse | 388 | 124 |
scaleai | Greenhouse | 173 | 118 |
instacart | Greenhouse | 123 | 113 |
fivetran | Greenhouse | 166 | 108 |
perplexity | Ashby | 127 | 107 |
gitlab | Greenhouse | 231 | 102 |
baseten | Ashby | 105 | 100 |
figma | Greenhouse | 139 | 81 |
headway | Ashby | 89 | 81 |
cohere | Ashby | 114 | 77 |
clickhouse | Ashby | 202 | 76 |
adyen | Greenhouse | 244 | 75 |
chime | Greenhouse | 73 | 71 |
mercury | Greenhouse | 72 | 71 |
asana | Greenhouse | 105 | 70 |
vercel | Greenhouse | 95 | 70 |
faire | Greenhouse | 71 | 70 |
temporal | Ashby | 70 | 67 |
lyrahealth | Lever | 473 | 64 |
flexport | Greenhouse | 193 | 64 |
deepgram | Ashby | 89 | 64 |
ripple | Greenhouse | 96 | 61 |
tailscale | Greenhouse | 59 | 53 |
swordhealth | Greenhouse | 54 | 50 |
vanta | Ashby | 84 | 48 |
…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 salary | platform cost (same input) | |
|---|---|---|---|
includeDescriptions: true (default) | 138 s | 6,838 / 15,530 (44%) | $0.00451 |
includeDescriptions: false | 66 s | 2,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),
salaryOnlysimply returns nothing from it.SUMMARYreports 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:
| Run | Setup | Rows returned | Jobs skipped as already seen |
|---|---|---|---|
| 1st | onlyNew + resetSeen | 10 | 0 |
| 2nd | onlyNew, nothing changed upstream | 0 | 10 |
| 3rd | onlyNew, nothing changed upstream | 0 | 10 |
- 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
maxJobslater, 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: truewipes 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
onlyNewwas 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 bonusis 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 infailedCompaniesrather than pretending to have no openings. - Evergreen roles exist. Some boards leave a posting up for years, so
postedAtis 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 usespython 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.