If you're building a job board, a hiring-signal tool, or a "who's hiring" newsletter, the good news is that you usually don't need to scrape HTML at all. The five biggest applicant tracking systems (ATSs) — Workday, Greenhouse, Lever, Ashby, SmartRecruiters, and Recruitee — all publish public, no-authentication JSON APIs for their job boards. Companies want these to be read; it's how their jobs get syndicated.
The bad news: all six APIs are completely different — different shapes, different pagination, different date formats, and some genuinely surprising quirks. I recently built an aggregator across all six and tested against dozens of live boards. Here's the field guide I wish I'd had.
Five endpoints at a glance
| ATS | Endpoint | Auth | Pagination | Salary data |
|---|---|---|---|---|
| Greenhouse | boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true |
none | no (one response) | rarely |
| Lever | api.lever.co/v0/postings/{company}?mode=json |
none | no | sometimes |
| Ashby | api.ashbyhq.com/posting-api/job-board/{board}?includeCompensation=true |
none | no | usually |
| SmartRecruiters | api.smartrecruiters.com/v1/companies/{company}/postings |
none | yes (limit/offset) |
rarely |
| Recruitee | {company}.recruitee.com/api/offers/ |
none | no | rarely |
Try one right now:
curl "https://boards-api.greenhouse.io/v1/boards/stripe/jobs" | jq '.jobs | length'
# 500+ open roles, in one response
Greenhouse: the workhorse (with one weird trick)
Greenhouse powers a huge share of tech job boards. One GET returns every job — no pagination even for 500+ role boards. Add ?content=true for full descriptions.
The quirk: the content field is HTML… but HTML-escaped. You'll receive <p>We are hiring</p> and need to run it through html.unescape() before parsing. Nearly every Greenhouse scraper tutorial misses this.
Dates come as ISO-8601 with offsets (2026-06-02T08:58:57-04:00), and department/office arrays are consistently populated — the most structured taxonomy of the five.
Lever: cleanest API, millisecond timestamps
Lever returns a flat JSON array — no wrapper object. Everything interesting hides in categories (department, team, commitment, location) plus a top-level workplaceType (remote / hybrid / on-site) that's more reliable than parsing location strings.
The quirks: createdAt is epoch milliseconds, not ISO. And unknown company slugs correctly return HTTP 404 — remember that; it becomes relevant below.
Ashby: the one with salary data
Ashby is the youngest of the five and increasingly common among well-funded startups (OpenAI, Linear, Ramp, Notion). Its killer feature: ?includeCompensation=true returns structured salary ranges — min, max, currency, and pay interval — on most postings.
How rich is that in practice? In a July 2026 pull of 24 well-known tech companies across all five ATSs, about 700 of 4,265 postings had structured salary ranges — and nearly all of them came from Ashby boards. (Median posted midpoint for US salaried roles: $286,000. Highest max: $585,000 for a research engineer role.)
The compensation object nests salary inside summaryComponents — sometimes at the top level, sometimes inside compensationTiers[].components. Parse both paths defensively.
SmartRecruiters: the only one that paginates (and lies about 404s)
SmartRecruiters — common among European enterprises — is the most work:
- The list endpoint paginates (
limitmax 100,offset), returning{"totalFound": N, "content": [...]}. - The list response has no descriptions. Full text requires one extra request per posting (
/postings/{id}), where the description arrives split into titled sections (companyDescription,jobDescription,qualifications) you must stitch back together.
The trap: request postings for a company that doesn't exist and you get… HTTP 200 with totalFound: 0. Not a 404. If you're auto-detecting which ATS a company uses by probing endpoints, SmartRecruiters will happily "confirm" every company name you throw at it. Require totalFound > 0 before believing it.
Recruitee: the subdomain one
Recruitee (popular with EU scale-ups) serves each company's API from its own subdomain: {company}.recruitee.com/api/offers/. That means a wrong company slug isn't a 404 — it's a DNS resolution failure. Your HTTP client raises a connection error, not an HTTP error; handle both.
Dates come as "2026-05-29 14:07:42 UTC" — not ISO-8601. And employment types arrive as composite codes like fulltime_fixed_term that need your own normalization table.
The real cost isn't fetching — it's normalizing
Any of these APIs is maybe 20 lines of Python to fetch. The actual engineering is making five schemas agree:
-
Dates: ISO with offsets (Greenhouse), epoch ms (Lever), ISO-ish (Ashby),
"... UTC"strings (Recruitee). -
Remote: a boolean (
isRemote, Ashby), an enum (workplaceType, Lever), a location flag (SmartRecruiters), or nothing but the word "Remote" in a location string (Greenhouse). -
Employment type:
full-timevsFullTimevspermanentvsfulltime_fixed_term. - Stable IDs for deduplication and change tracking across runs.
Plus retries with backoff on 429/5xx, per-company error isolation (one dead slug shouldn't kill a 200-company run), and — if you run on a schedule — delta logic so you only process new/changed postings.
Or: one call for all six
I packaged all of the above into a single Apify actor — Job Scraper API — Workday, Greenhouse, Lever, Ashby + More. You give it company names or careers-page URLs; it auto-detects each company's ATS (including the SmartRecruiters false-positive trap and the Recruitee DNS dance), fetches everything, and returns one normalized schema:
{
"id": "ashby:openai:8fb1615c-...",
"title": "Technical Program Manager, Compute Infrastructure",
"company": "openai",
"locations": ["San Francisco"],
"remote": false,
"employmentType": "full-time",
"compensation": { "min": 257000, "max": 335000, "currency": "USD", "interval": "year" },
"postedAt": "2026-03-12T16:38:15+00:00",
"url": "https://jobs.ashbyhq.com/openai/8fb1615c-..."
}
It costs about $1.50 per 1,000 jobs, and a delta mode returns only new/changed postings between scheduled runs — the thing you actually want if you're building alerts or tracking hiring velocity.
If you'd rather build it yourself, everything above is the map. Either way: skip the HTML scraping — the JSON was there all along.
Data points in this post are from live API pulls in July 2026. The ATS platforms can change their APIs at any time — verify against a live board before shipping.
Top comments (0)