Pulling open jobs from Greenhouse, Lever and Ashby without scraping HTML A developer built an Apify actor that pulls open job listings directly from the public JSON feeds of applicant tracking systems including Greenhouse, Lever, Ashby, Workable, SmartRecruiters and Recruitee, avoiding HTML scraping of JavaScript-heavy career pages. The actor accepts company board slugs or careers URLs and supports filtering by keywords, location, department, employment type, remote status, posting date, salary and skills, plus a monitor mode that returns only new or changed jobs. The developer notes that field sets differ across vendors — Greenhouse's board feed omits remote, workplace type and employment type, Lever lacks an updatedAt field, and most boards publish no salary — so the seniority field is only a title-based heuristic. I built an Apify actor that reads job boards from several applicant tracking systems ATS , so keep that bias in mind. An AI assistant helped me draft this article, and I reviewed and edited it. The free route comes first, because for a handful of companies a short script is enough. Recruiters, job-board builders and sales teams that watch hiring signals all want the same thing: a clean list of open roles at specific companies. Career pages are often JavaScript-heavy, but most companies run their hiring on an ATS that exposes a public JSON feed so the career page can render. You do not need to scrape HTML. You need the company's board slug, which is the last part of the careers URL for example boards.greenhouse.io/airbnb , jobs.lever.co/spotify , jobs.ashbyhq.com/ramp . python import requests def greenhouse slug : r = requests.get f"https://boards-api.greenhouse.io/v1/boards/{slug}/jobs", timeout=30 r.raise for status return j "title" , j "location" "name" , j "absolute url" for j in r.json "jobs" def lever slug : r = requests.get f"https://api.lever.co/v0/postings/{slug}", params={"mode": "json"}, timeout=30 r.raise for status return j "text" , j "categories" .get "location" , j "hostedUrl" for j in r.json def ashby slug : r = requests.get f"https://api.ashbyhq.com/posting-api/job-board/{slug}", timeout=30 r.raise for status return j "title" , j "location" , j "jobUrl" for j in r.json "jobs" for name, fn, slug in "greenhouse", greenhouse, "airbnb" , "lever", lever, "spotify" , "ashby", ashby, "ramp" : jobs = fn slug print name, slug, len jobs , jobs 0 I checked these against the live endpoints. Greenhouse returns jobs with title , location.name , absolute url , updated at and first published . Lever returns a list with text , categories including location , department , commitment , hostedUrl , applyUrl and createdAt . Ashby returns jobs with title , department , location , isRemote , workplaceType , publishedAt and jobUrl . Field sets differ between vendors and can change, so check each response's keys before depending on one. For descriptions, Greenhouse needs ?content=true on the request. The actor covers Greenhouse, Lever, Ashby, Workable, SmartRecruiters and Recruitee, plus a schema.org JobPosting fallback for single career pages. You can paste company slugs or careers URLs. The input below is the README's real test input: { "companies": "airbnb", "https://jobs.lever.co/spotify", "https://jobs.ashbyhq.com/ramp" , "onlyNewSinceLastRun": false, "maxJobsPerCompany": 3, "maxJobsTotal": 3, "includeDescription": false } Other real fields include keywords , excludeKeywords , locations , departments , employmentTypes , remoteOnly , workModels , postedAfter , minSalary , skillsInclude and monitorMode return only new or changed jobs, optionally also removed ones . Excerpt of real sample output description removed, a few fields kept : { "company": "Spotify", "ats": "lever", "title": "Senior Product Manager, Consumer - Audiobooks", "department": "Product", "location": "London", "remote": false, "workplaceType": "hybrid", "employmentType": "FULL TIME", "postedAt": "2026-10-12T13:42:44.230Z", "jobUrl": "https://jobs.lever.co/spotify/ddc10181-6ecc-4dfc-9d02-2c5ce820cfdd", "salaryMin": null, "seniority": "senior" } The Greenhouse row in the same file shows the vendor differences: Airbnb's "Senior Software Engineer, Payments Transaction Risk" has remote , workplaceType and employmentType all null , because Greenhouse's board feed does not carry them. remote: true only when the location says remote , Lever has no updatedAt , and most boards publish no salary. seniority is a heuristic from the title only. Treat it as a best guess. For a few boards, run the script. For a watchlist with filters and a daily new-jobs feed, the actor is here: https://apify.com/transparent meteorite/ats-open-roles https://apify.com/transparent meteorite/ats-open-roles Disclosure again: I built the actor, and an AI assistant helped draft this article. I reviewed it and checked the three endpoints before writing the snippet.