How to pull job listings from Greenhouse, Lever and Ashby without scraping
If you track hiring at a list of companies — for a job board, a recruiting pipeline, or a "who is hiring" signal for sales — you quickly notice that a large share of tech companies host their careers pages on one of three applicant tracking systems: Greenhouse, Lever or Ashby.
The good news: you do not need to scrape their HTML. All three publish a public, documented job-board API meant for reading published postings. No login, no browser, no proxy.
The less good news: the three APIs return three different shapes, and the details that matter most — salary, remote status, posting date — live in different places in each one.
This post walks through the endpoints, the differences that bite, and the trade-offs between doing it yourself and using a ready Actor.
The three endpoints
| ATS | Public endpoint | Requests per company |
|---|---|---|
| Greenhouse | boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true | 1 |
| Lever | api.lever.co/v0/postings/{company}?mode=json | 1 per 500 jobs |
| Ashby | api.ashbyhq.com/posting-api/job-board/{name}?includeCompensation=true | 1 |
The {token} / {company} / {name} is the board name you see in the company's job URL, for example job-boards.greenhouse.io/discord or jobs.lever.co/spotify.
That is the whole "API key" story: there is none. One GET per company gives you every open posting.
Where it gets fiddly
1. Salary lives in three places. Ashby can return structured compensation when you ask for it. Lever has a salaryRange field on some postings. Greenhouse usually has nothing structured — but many US postings include a pay-transparency line in the description, such as "$139,200 - $235,200 USD". If you want salary across all three, you need a parser for the description text and a flag that says where each number came from.
2. Remote is not one field. Some boards set a workplace type, some only write "Remote" into the location string, some list several locations of which one is remote. Decide on one boolean and one rule for computing it.
3. Dates differ in precision. Treat "posted at" and "updated at" as separate fields and keep both, or your "new in the last 7 days" filter will lie.
4. Rate limits. Lever's robots.txt says Crawl-delay: 1. If you poll many Lever boards, keep to one request per second per host for the whole run — not per board.
5. Finding the board from a careers page. Users rarely give you a board token; they give you company.com/careers. Often the page embeds the ATS and the board name is in the HTML. Sometimes the page is JavaScript-only and you can only guess the board name from the domain — and a guess can hit a board with the same name that belongs to another company. Mark guessed rows as unverified.
6. "Only new since last run". For monitoring, you want each scheduled run to return only postings you have not seen before. That needs state: a store of job IDs per watch-list, keyed so that the same board written two different ways is still the same board.
None of this is hard. It is just a few days of edge cases, and then maintenance.
The ready-made option
We packaged the above as an Apify Actor: Greenhouse, Lever & Ashby Jobs API — https://apify.com/sipki/ats-jobs-api
Input is a plain list. You can mix board URLs, careers pages, ats:token, or bare tokens:
{
"companies": [
"https://job-boards.greenhouse.io/discord",
"lever:spotify",
"https://linear.app/careers"
],
"keywords": ["engineer", "developer"],
"excludeKeywords": ["intern"],
"remoteOnly": true,
"postedAfter": "30 days",
"descriptionFormat": "text",
"includeDescriptions": true,
"onlyNewSinceLastRun": false
}
Output is one flat row per job, the same fields for all three systems. A real row:
{
"ats": "greenhouse",
"company": "GitLab",
"title": "Senior Software Engineer, Security Factory: Code Security",
"location": "Remote, Canada; Remote, United States",
"remote": true,
"department": "Sec Engineering",
"salary_min": 139200.0,
"salary_max": 235200.0,
"salary_currency": "USD",
"salary_interval": "year",
"salary_source": "description",
"posted_at": "2026-10-10T00:31:32Z",
"url": "https://job-boards.greenhouse.io/gitlab/jobs/8738179002",
"apply_url": "https://job-boards.greenhouse.io/gitlab/jobs/8738179002"
}
(Abridged; the full row also has locations, workplace_type, team, employment_type, requisition_id, updated_at and the description as text or HTML.)
salary_source tells you whether the range came from structured ATS data or from the description text. Every row also says how its board was found (detected_by: url, explicit, page_html, token_probe or domain_guess), so you can drop guessed boards if you need certainty.
For monitoring, set onlyNewSinceLastRun: true and schedule it. The Actor stores job IDs in a key-value store in your own account, and each run returns — and charges for — only postings it has not returned before.
What we measured (and what we did not)
- A cloud run on 16 October 2026 over three boards (Discord on Greenhouse, Binance on Lever, Notion on Ashby, 20 jobs each) returned 60 jobs. 36 of the 60 had a salary range; all 36 were parsed from the description text. The 20 Lever postings in that sample had none.
- A local run on 16 October over 3 boards, 5 jobs each, descriptions off: 15 jobs in 1.3 s of run time; the Lever one-request-per-second limit dominated.
- A local test of the "only new" mode: the second run over the same boards returned 0 of the 422 jobs returned by the first.
These are small runs. We have no figures for very large boards with descriptions on, and we do not claim any.
Price, and how it compares
The Actor charges $0.002 per job returned — $2 per 1,000 jobs. No start fee. Jobs removed by your filters and boards that fail are free, and the run stops exactly at your spending limit. The prefilled example (up to 60 jobs) costs at most $0.12.
Honest comparison of the options:
- Do it yourself. The APIs are free. You pay in engineering time for the edge cases above and in upkeep when a vendor changes something. If you only need one ATS and no salary parsing, this is the cheapest path.
- Other per-ATS or multi-ATS Actors in the Apify Store. On 15–17 October 2026, comparable "list of companies → jobs" Actors charged roughly $1.50 to $3 per 1,000 jobs, some with an extra start fee. We are in the middle of that range, not the cheapest. What we focus on: one schema for three systems, salary with its source, careers-page detection, and the "only new" mode.
- Search-a-database products. Some services let you search a pre-built index of hundreds of thousands of companies; they cost more per job (we saw up to about $12 per 1,000 on entry tiers) but you do not need to know the companies in advance. That is a different job — use it if you need discovery rather than a fixed watch-list.
Limits
- Only Greenhouse, Lever and Ashby. Workday, iCIMS, SmartRecruiters and others are not supported.
- Fields a board does not publish come back as
null(Greenhouse rarely publishes employment type). - For company websites, the Actor reads
robots.txtfirst and stops for that entry if it is disallowed or the site refuses; no retries, no proxies. Vendor board URLs go straight to the public job-board APIs. - Job descriptions belong to the employers. Use them for search, filtering and analysis; republishing full text may need permission. Set
includeDescriptions: falseif you only need structured fields.
If you build it yourself, the endpoint table and the six gotchas above should save you a day. If you would rather not, the Actor is here: https://apify.com/sipki/ats-jobs-api
Not affiliated with Greenhouse Software, Inc., Lever (Employ Inc.) or Ashby. Names identify the public APIs only.