DEV Community

Chaz Eden
Chaz Eden

Posted on

Comparing the job-posting APIs of Workday, Greenhouse, Lever, Ashby, SmartRecruiters, and Recruitee (2026)

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. The list endpoint paginates (limit max 100, offset), returning {"totalFound": N, "content": [...]}.
  2. 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-time vs FullTime vs permanent vs fulltime_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-..."
}
Enter fullscreen mode Exit fullscreen mode

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)