How to get a company's open jobs straight from Greenhouse, Lever or Ashby
You have a list of companies and want their open roles in one table, with no browser and no HTML parsing. Most careers pages built on Greenhouse, Lever or Ashby are rendered from a public JSON endpoint that anyone can call, and the three are similar enough to merge into one shape. The board token is

the last part of the careers URL: job-boards.greenhouse.io/<token>, jobs.lever.co/<token>, jobs.ashbyhq.com/<token>. An unknown token returns 404 on all three. Greenhouse: GET https://boards-api.greenhouse.io/v1/boards/<token>/jobs. One request returns the whole board (674 jobs for the board I tried, in about a second).
You get id, title, location.name, first_published, updated_at and absolute_url. Descriptions need ?content=true, which made that same response about 10 MB. Pay is not in the list; /jobs/<id>?pay_transparency=true adds pay_input_ranges when the company publishes one, one request per job. Lever: GET https://api.lever.co/v0/postings/<site>?mode=json.
Descriptions are included, createdAt is epoch milliseconds, and the location sits in categories.location. Large boards are slow: an 85-posting board took about 10 seconds today. Lever also pages with skip and limit, and has an EU host, api.eu.lever.co. Ashby: GET https://api.ashbyhq.com/posting-api/job-board/<name>.
One request for the board. Add ?includeCompensation=true for pay ranges where published, which made one board's response about 3 MB. import json, urllib.request from datetime import datetime, timezone def get(url): req = urllib.request.Request(url, headers={"User-Agent": "careers-feed-example/1.0 (personal script)"}) with urllib.request.urlopen(req, timeout=60) as r: return json.load(r) def greenhouse(board): for j in get(f"https://boards-api.greenhouse.io/v1/boards/{board}/jobs")["jobs"]: yield dict(ats="greenhouse", id=str(j["id"]), title=j["title"].strip(), location=j["location"]["name"], posted=j["first_published"], url=j["absolute_url"]) def lever(site): for j in get(f"https://api.lever.co/v0/postings/{site}?mode=json"): yield dict(ats="lever", id=j["id"], title=j["text"].strip(), location=j["categories"].get("location"), posted=datetime.fromtimestamp(j["createdAt"] / 1000, timezone.utc).isoformat(), url=j["hostedUrl"]) def ashby(board): for j in get(f"https://api.ashbyhq.com/posting-api/job-board/{board}")["jobs"]: yield dict(ats="ashby", id=j["id"], title=j["title"].strip(), location=j["location"], posted=j["publishedAt"], url=j["jobUrl"]) Output (one run): greenhouse 674 jobs; first: 3P Revenue Accounting & Reporting | San Francisco, CA | Seattle, WA | 2026-10-19T11:17:24-04:00 lever 10 jobs; first: Customer Success Manager | Atlanta, Georgia | 2026-08-31T18:12:19.703000+00:00 ashby 176 jobs; first: Security Engineer, Cloud | New York, NY (HQ) | 2026-04-07T17:12:35.753+00:00 The .strip() on titles is there because one Ashby title came back with a leading space. The Lever board here is Lever's own demo board, leverdemo, which is why it has only 10 jobs.
Postings have stable ids, so a daily diff is a set comparison. Save the previous ids, print what is new, and print what disappeared. Save the first script as ats.py; this one imports from it. import json from ats import greenhouse, lever jobs = {f'{j["ats"]}:{j["id"]}': j for j in [*greenhouse("anthropic"), *lever("leverdemo")]} try: old = json.load(open("jobs_last.json")) except FileNotFoundError: old = None if old is not None: for key in jobs.keys() - old.keys(): print("NEW ", jobs[key]["title"], "|", jobs[key]["url"]) for key in old.keys() - jobs.keys(): print("REMOVED", old[key]["title"], "|", old[key]["url"]) json.dump(jobs, open("jobs_last.json", "w")) print(len(jobs), "open jobs saved") To test it I ran it once, deleted one entry from jobs_last.json, added a fake one, and ran it again: NEW 3P Revenue Accounting & Reporting | https://job-boards.greenhouse.io/anthropic/jobs/5456619008 REMOVED Ghost role | https://example.com/1 684 open jobs saved A reposted job usually gets a new id, so it shows up as one removal and one new posting.
If that matters, match on title and location as well. Each API exposes different fields. Lever has no company name, no updated date and no requisition id. Ashby has no company name or updated date.
Greenhouse has no employment type or pay in the list. Ashby's isRemote is unreliable. It is set on many hybrid jobs, so read workplaceType instead. Placeholders pass through.
Some Greenhouse boards fill requisition_id with a literal like "See Opening ID" on every job. Greenhouse descriptions are HTML-escaped, so decode them before converting to text. Board names are first-come slugs. The same name on two systems can be two different companies, and companies leave stale boards behind when they switch.
Check the job URL before trusting a match. Coverage is partial. Workday, iCIMS, Taleo and custom careers sites have no public feed like this. Be gentle.
One request per board per day is enough. These endpoints exist so careers pages and aggregators can read them; keep to that use, collect job data only, and leave applicant or recruiter details alone. Disclosure: I'm the developer of an Apify Actor, ATS Jobs Scraper, published under Cedarfetch Labs. It covers seven systems instead of three (it adds SmartRecruiters, Workable, Recruitee and Personio), returns one schema for all of them, and tracks changes between scheduled runs, labelling each job new, updated, unchanged or removed.
A job is only marked removed after a complete, error-free read of the board. It is pay per event: $0.0015 per job row (removed rows included) plus $0.00005 per run start, as shown on the listing on 2026-11-11. Check the Pricing tab for current figures. Listing: https://apify.com/cedarfetch/ats-jobs-scraper Runnable Python, Node.js and cURL examples for the Actor: https://github.com/cedarfetch/ats-jobs-scraper-examples Not affiliated with Greenhouse, Lever or Ashby.
Key Takeaways
- β’An unknown token returns 404 on all three.
- β’This story was reported by Dev.to, covering developments in the dev space.
- β’AI advancements continue to reshape industries β read the full article on Dev.to for complete coverage.
π Continue reading the full article:
Read Full Article on Dev.to βShare this article


