A company's careers page is rarely custom software. It is a board hosted by an applicant tracking system, and the page loads its openings from a public endpoint that needs no key, because the company wants those openings indexed.
I keep the endpoint for 17 of those systems in one place, each with a curl that was run before it was written down, and a script that reruns all of them every week: github.com/danielhagever/job-board-apis.
Most of them are one GET:
curl -s "https://boards-api.greenhouse.io/v1/boards/figma/jobs" | jq '.meta.total'
curl -s "https://api.lever.co/v0/postings/palantir?mode=json" | jq 'length'
curl -s "https://api.ashbyhq.com/posting-api/job-board/openai?includeCompensation=true" | jq '.jobs | length'
The interesting part is where a reasonable loop returns the wrong answer and nothing tells you. Seven of those, each measured on 2026-10-25.
1. Workday caps a page at 20 and answers 400 above it
curl -s -X POST "https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs" \
-H "Content-Type: application/json" \
-d '{"limit":20,"offset":0,"searchText":"","appliedFacets":{}}' | jq '.total'
"limit":50 gets HTTP 400 with an empty message, which reads like a malformed request rather than a paging limit.
2. Workday's total is only real on page one
NVIDIA said 2,000 at offset 0 and 0 at offset 20. A loop written as while offset < total stops after the first page and reports success. Keep the first page's number.
3. Oracle returns counts and no rows unless you expand
Oracle Recruiting Cloud (Marriott, Kroger, JPMorgan Chase) answers recruitingCEJobRequisitions with a count and an empty body unless the query carries expand=requisitionList.... With it, 200 rows a page, and nothing at or past the 10,000th record of one query: offset 9,800 returned rows, 9,990 returned none. Marriott had 13,437 open jobs, so a big site has to be split by category.
4. An unknown Oracle site is not an error
Ask for a siteNumber that does not exist and Oracle answers 200 with some default site's jobs. List a host's real sites first (recruitingCESites).
5. Ashby hides salary behind a flag
Leave out includeCompensation=true and the compensation key is simply absent. With it, 631 of OpenAI's 793 postings carried a salary summary.
6. ADP wants a header you get from a second call
myjobs.adp.com job lists answer 400 "Missing orgoid header" until you read the career site's settings first and send its orgoid back.
7. Some careers sites refuse cloud servers
A Phenom careers site (careers.rtx.com) answered 100 jobs to a home connection and 403 to a GitHub Actions runner in the same hour. If it works on your laptop and fails in production, that is why.
And one that is closed on purpose
SmartRecruiters publishes a postings API, but api.smartrecruiters.com/robots.txt allows it to LinkedInBot and disallows everyone else, so it is not in the list.
The rest
Recruitee, Workable, Rippling, Personio (XML), Breezy, Teamtailor (a JSON Feed with a schema.org JobPosting per item), BambooHR, Jibe (which asks for a five-second crawl delay), Eightfold (ten rows a page, fixed) and Amazon's own search are in the repo, with the token you need and where to find it in a careers URL.
If you would rather not keep seventeen parsers alive, I run them as hosted Actors on Apify with these traps handled and one output schema: Company Career Page Jobs Scraper takes a company name or domain, and Workday Jobs Scraper does the same for any Workday site. Both have a quick start on GitHub: company-career-page-scraper and workday-jobs-scraper.
Top comments (0)