Most "job API" tutorials show you a toy call and stop before the part where you need 20,000 rows. This is the other version: the exact request, the exact response, the exact bill, and the four ways it will fail on you.
What you get
One call returns jobs from 259 companies across Greenhouse, Ashby, Lever, Workable, Personio and Workday. Strip the ones without pay and you are left with about 6,826 postings that state an annual salary range, normalised to USD.
Each record looks like this:
{
"company": "openai",
"ats": "ashby",
"title": "Research Scientist / Research Engineer - Foundations, Search",
"location": "San Francisco, CA",
"remote": false,
"salaryLow": 380000,
"salaryHigh": 850000,
"salaryCurrency": "USD",
"salaryUsdLow": 380000,
"salaryUsdHigh": 850000,
"postedAt": "2025-06-16T22:35:00.347000+00:00",
"url": "https://jobs.ashbyhq.com/openai/…"
}
salaryUsdLow/High are the interesting fields: they exist because I convert hourly and monthly bands into a year and convert currencies, and because I keep the original numbers next to the normalised ones so you can second-guess me.
The call
No SDK, no pagination, no auth dance beyond an Apify token:
curl -X POST "https://api.apify.com/v2/acts/contributory_garnet_hyz~ats-job-feed/runs?token=$APIFY_TOKEN&waitForFinish=600" \
-H "Content-Type: application/json" \
-d '{
"useVerifiedPool": true,
"salaryOnly": true,
"minSalaryUsd": 150000,
"maxPerCompany": 3,
"maxJobs": 100
}'
That run takes about four minutes and returns 100 jobs. The request-level knobs:
| input | what it does | default |
|---|---|---|
useVerifiedPool |
read the bundled 259-company list instead of the companies you pass |
false |
companies |
your own slugs or careers URLs (Workday needs the full URL) | 13 well-known slugs |
salaryOnly |
drop postings without a published range | false |
minSalaryUsd |
drop below this annual midpoint | 0 |
keywords |
substring match on title | none |
maxPerCompany |
stop one company eating the list | 20 |
maxJobs |
hard cap on returned rows | 200 |
maxWorkers |
parallel board requests (max 10) | 6 |
includeDescriptions |
read Greenhouse job bodies (needed for their salaries) | true |
onlyNew |
only return postings you have not seen in previous runs | false |
remoteOnly, excludeSenior, excludeJunior, excludeNonTech, maxAgeDays
|
obvious filters | off |
The bill, itemised
This is the part nobody publishes. Real numbers from my own runs:
- $0.0005 per run start
- $0.0015 per job returned
- platform usage: about $0.006 with job descriptions on, about $0.0008 with them off
So: 100 jobs with descriptions ≈ $0.006 + $0.0005 + $0.15 = $0.157. 50 jobs ≈ $0.08. A daily diff of ~20 new postings ≈ $0.04/day.
That platform-usage line is 8x higher when includeDescriptions is on, because Greenhouse descriptions are a 144 MB download. If you do not need salary for Greenhouse companies — or you are happy with the ~18% coverage that structured fields give you — turn it off and the run is twice as fast and an order of magnitude cheaper.
But note the catch: salaryOnly + includeDescriptions: false will mostly return Ashby jobs, because Greenhouse publishes pay in prose, not in metadata. You are choosing between cheap and complete.
Four ways it breaks
-
Slugs rot. 527 candidate company slugs tested → 259 answered. Next month some of the 259 will 404. Every run returns
failedCompanieswith the reason per board, e.g."greenhouse: empty; lever: HTTP 404; ashby: HTTP 404". It does not silently return fewer rows. - Workable is a shell. Its public endpoint answers for 4 companies and returns an empty list with a 200. Treated as a failure, not as "this company is not hiring".
-
Workday cannot be guessed. Its board host is
{tenant}.wd{N}.myworkdayjobs.comand the root path returns HTTP 406, so there is no redirect to discover the site name. I tested 24 large companies × 2 host numbers × 4 site names = 192 guesses and got exactly one hit. That is why Workday needs a URL. Its list API also has no salary field at all, so a range can only come from the job description. -
Greenhouse salaries are prose. "The base salary range is 196,000 USD - 310,500 USD" — currency after each number, plus postings that list Level 4 and Level 5 bands in one sentence. My first parser turned that into
196000–196000. If you build your own, test on NVIDIA's postings.
If you only want the snapshot
You do not need an account for the frozen version — 1,459 rows of it, CC BY 4.0:
- CSV: https://sjdfi0wjvosdju9.github.io/data/salary-ranges.csv
- Method, per-company medians, publication rates per board: https://sjdfi0wjvosdju9.github.io/salary-data.html
The live actor is https://apify.com/contributory_garnet_hyz/ats-job-feed — $0.0015 per job, and it is a one-person project, so if the parsing looks wrong somewhere I do want to hear about it.
Two honest caveats: this is published pay, not actual pay, and roughly 44% of postings publish nothing at all. Also, some companies post evergreen roles that stay up for a year — maxAgeDays will not save you from those, because their posting date is genuinely old and they are genuinely still open.
Numbers revision (2026-10-24): the pipeline was merging distinct city-level requisitions under one title, so row counts here are lower than the current dataset. After fixing the dedupe key the same 259 companies yield **18,567 postings / 7,984 with published pay* (median $200,000). The current figures live on the dataset page; the bug is written up here.*
Top comments (0)