Want a feed of new openings at a list of companies — for a niche job board, a recruiting pipeline, a hiring-trends dashboard
or just your own job hunt? You don't need Puppeteer. Almost every company publishes jobs through an applicant tracking
system (ATS), and every major ATS exposes a public, unauthenticated JSON feed for its career pages.
This post shows the endpoints, the gotchas I hit, and the small amount of code that turns 11 different formats into one.
Step 1: find out which ATS a company uses
Open the careers page and look at the links / embedded scripts:
| You see in the HTML | ATS |
|---|---|
boards.greenhouse.io/stripe | Greenhouse |
jobs.lever.co/palantir | Lever |
jobs.ashbyhq.com/openai | Ashby |
nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite | Workday |
apply.workable.com/huggingface | Workable |
jobs.smartrecruiters.com/BoschGroup | SmartRecruiters |
…plus Recruitee, Personio, Breezy HR, Teamtailor and Rippling. A regex per pattern over the page HTML detects
most companies automatically.
Step 2: call the public feed
The easy ones return everything in one request:
# Greenhouse (content=true adds descriptions) curl "https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true" # Lever curl "https://api.lever.co/v0/postings/palantir?mode=json" # Ashby (with structured salary!) curl "https://api.ashbyhq.com/posting-api/job-board/openai?includeCompensation=true" Workday is the odd one: a POST with a JSON body, 20 jobs per page:
curl -X POST -H "Content-Type: application/json" \ -d '{"limit":20,"offset":0,"searchText":"engineer","appliedFacets":{}}' \ "https://nvidia.wd5.myworkdayjobs.com/wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs" Tip: use searchText — paging through 2,000 NVIDIA jobs to find 30 matching ones is slow; Workday's own search isn't.
Step 3: the gotchas
- Greenhouse descriptions are double-encoded HTML (
<p>) — decode entities first, then strip tags. - Workday dates are text:
Posted Today,Posted 3 Days Ago,Posted 30+ Days Ago. The detail endpoint has the realstartDate.locationsTextmay literally be"2 Locations". - Workday
totalis only correct on the first page. - Recruitee dates look like
2026-10-13 10:58:56 UTC— not ISO. - Personio returns XML, and some companies live on
.jobs.personio.cominstead of.de. - SmartRecruiters and Rippling need one detail call per job for descriptions (Rippling even for the date).
- Teamtailor usually runs on a custom domain —
/jobs.rsson the careers domain gives you everything.
Step 4: normalize
Map every source into one shape:
{ company, ats, jobId, title, department, location, locations, country, remote, workplaceType, employmentType, postedAt, salary, descriptionText, applyUrl } Salary deserves a note: only Ashby, Lever, Recruitee and Rippling have structured pay. For the rest, US postings
usually state a range in the text, and one regex catches most of them:
const m = /([$€£])\s?(\d{1,3}(?:,\d{3})*|\d+)\s?([kK])?\s?(?:-|–|to)\s?[$€£]?\s?(\d{1,3}(?:,\d{3})*|\d+)\s?([kK])?/.exec(text); Reject yearly ranges under 10,000 so "$1,000–$2,000 signing bonus" doesn't become a salary. In my tests this finds
pay for roughly half of all jobs.
Step 5: only new jobs
Keep a map of ats:company:jobId → firstSeenAt. Each run:
- job not in the map → new
- job in the map but gone now → closed, but only if that company's fetch succeeded (otherwise one timeout looks like a mass closure)
- make the very first run a silent baseline, so your "new jobs" feed doesn't start with 3,000 items.
Run it daily on a schedule and you have a new-jobs feed for any list of companies.
Full endpoint reference
I documented all 11 endpoints, fields and quirks here:
👉 github.com/Mersetti/ats-job-board-apis
Don't want to maintain it?
I packaged all of the above — 11 ATS, auto-detection from careers URLs, salary extraction, filters and new/closed
monitoring — as an Apify Actor: Career Sites Jobs Feed. Pay per job,
schedule it, export to CSV/Sheets or call it from an AI agent.
Which ATS should I add next? Let me know in the comments.