How to monitor new jobs at a list of companies (Greenhouse, Lever, Ashby)
Last updated: 24 October 2026 · By Quicksloth
You follow 20, 200 or 1,000 companies and want to know when one of them posts a new job: to apply early, to watch competitors hire, or because a new data-engineering role means a company may need your product. Many tech companies, especially startups and scale-ups, publish their jobs on Greenhouse, Lever or Ashby, and all three have a public API. This guide shows two ways to watch them: a free script you run yourself, and a scheduled cloud run that does the bookkeeping for you.
Short version: find each company's job board, read it through its public API on a schedule, and keep a list of the job IDs you have already seen. Anything not on the list is new.
Step 1: find each company's job board
Open the company's careers page and click any job. The address of the job page tells you the system and the board name:
| System | Job page address looks like | Board name |
|---|---|---|
| Greenhouse | job-boards.greenhouse.io/gitlab/jobs/… or boards.greenhouse.io/gitlab | gitlab |
| Lever | jobs.lever.co/spotify/… | spotify |
| Ashby | jobs.ashbyhq.com/ramp/… | ramp |
Lever boards on jobs.eu.lever.co are read by the Actor (lever-eu:<board>) but not by the script below. Some careers pages show the board inside the company's own site. Then look for greenhouse, lever or ashbyhq in the page source (in most browsers, right-click and View page source), or in the address of the Apply button. Companies on Workday, SmartRecruiters and other systems are not covered by this guide.
Write the list as system:board, for example greenhouse:gitlab, lever:spotify, ashby:ramp.
Way 1: a free Python script
The three public APIs need no key. Greenhouse says it plainly: "Job Board data is publicly available, so authentication is not required for any GET endpoints" (Greenhouse Job Board API, read on 13 October 2026). Lever documents its Postings API and Ashby its public job posting API (fields, salaries and tests on two companies each in our guides to the Greenhouse Job Board API, the Lever Postings API and the Ashby job board API). The script below reads each board, prints the jobs it has not seen before and saves their IDs in seen_jobs.json. It uses only Python's standard library (Python 3.8 or later).
"""Print jobs that are new since the last run, for a list of Greenhouse, Lever and Ashby boards."""
import json
import pathlib
import time
import urllib.request
BOARDS = ["greenhouse:gitlab", "lever:spotify", "ashby:ramp"]
SEEN_FILE = pathlib.Path("seen_jobs.json")
def get_json(url):
request = urllib.request.Request(url, headers={"User-Agent": "new-jobs-script"})
with urllib.request.urlopen(request, timeout=60) as response:
return json.load(response)
def fetch(ats, board):
if ats == "greenhouse":
data = get_json(f"https://boards-api.greenhouse.io/v1/boards/{board}/jobs")
return [(f"gh:{j['id']}", j["title"], (j.get("location") or {}).get("name"), j["absolute_url"]) for j in data["jobs"]]
if ats == "lever":
data = get_json(f"https://api.lever.co/v0/postings/{board}?mode=json")
return [(f"lv:{j['id']}", j["text"], j["categories"].get("location"), j["hostedUrl"]) for j in data]
if ats == "ashby":
data = get_json(f"https://api.ashbyhq.com/posting-api/job-board/{board}")
return [(f"ab:{j['id']}", j["title"], j["location"], j["jobUrl"]) for j in data["jobs"]]
raise ValueError(f"Unknown ATS: {ats}")
seen = set(json.loads(SEEN_FILE.read_text())) if SEEN_FILE.exists() else set()
for entry in BOARDS:
ats, board = entry.split(":", 1)
for job_id, title, location, url in fetch(ats, board):
if job_id not in seen:
print(f"NEW {board} {title} ({location}) {url}")
seen.add(job_id)
time.sleep(1) # be gentle: Lever's robots.txt asks for one second between requests
SEEN_FILE.write_text(json.dumps(sorted(seen)))
What we got when we ran it on 24 October 2026: the first run printed 496 jobs (239 at GitLab, 90 at Spotify, 167 at Ramp), because every job is new the first time. The second run, a minute later, printed nothing. Three lines from the first run:
NEW gitlab Account Executive - France (Remote, France) https://job-boards.greenhouse.io/gitlab/jobs/8860302002
NEW gitlab AI Transformation Owner, CRO (Remote, United States) https://job-boards.greenhouse.io/gitlab/jobs/8638232002
NEW gitlab AI Transformation Owner, Product & Design (Remote, Canada; Remote, United Kingdom; Remote, United States) https://job-boards.greenhouse.io/gitlab/jobs/8716179002
To run it every day, use cron on Linux or macOS (for example 0 8 * * 1-5 cd /path/to/folder && python3 new_jobs.py >> new_jobs.log) or Task Scheduler on Windows. The computer has to be on at that time.
What the script does not do, on purpose, to stay short: it does not notice when a job changes (a new title or salary), it does not filter by title or location, it stops at the first board that fails, and then it does not save seen_jobs.json, so the next run prints again the new jobs of the boards before it; and the three systems return different fields, so you get title, location and link only. Each of these is a few more lines; together they are most of the work.
Way 2: a scheduled Apify Actor
If you would rather not keep a script running, our ATS Jobs Scraper on the Apify Store does the same in the cloud and covers the gaps above: one schema for the three systems, title, location and remote filters, salary where the employer publishes it, and an Only new or changed jobs mode that keeps the list of seen jobs for you. It accepts board URLs, system:board names, careers-page URLs or plain company names, up to 1,000 per run. A company that fails is reported and the others continue.
- Open the Actor's page and start it in Apify Console (you need an Apify account; the free plan is enough to try it).
- Paste your companies in Companies, one per line. Add Job title keywords if you only care about some roles.
- Tick Only new or changed jobs, and give the watch list a name in History name, for example
weekly-engineering. - Click Start once. The first run returns the matching jobs up to Max jobs in total, which the form prefills at 500 (clear it for no limit), and up to 100 jobs on Apify's free plan. With Only new or changed jobs on, the jobs left out by a limit come in the next runs. That is your starting point.
- Schedule it: in Apify Console, click Schedules in the navigation menu, then Create new. Set how often it runs, then open Add, choose the Actor and paste the same input JSON, with the same History name (with a different one, the first scheduled run returns everything again). New schedules start disabled: click Enable (Apify docs: Actor and task schedules, read on 13 October 2026).
This is the input we used, in JSON (the JSON view of the input form, or the body of an API call):
{
"companies": ["https://job-boards.greenhouse.io/gitlab", "https://jobs.lever.co/spotify", "ashby:ramp"],
"titleKeywords": ["engineer"],
"onlyNewJobs": true,
"stateKey": "guide-test"
}
What we got on 24 October 2026, running the Actor's code (version 0.3, the one on the Apify Store) on our machine: the first run saved 159 jobs with "engineer" in the title (88 at GitLab, 32 at Spotify, 39 at Ramp); 74 of them had a published salary range. The second run, right after, saved 0. Three rows from the first run:
| Title | Board | Location | Workplace | Salary |
|---|---|---|---|---|
| Backend Engineer, Knowledge Graph (Rust) | gitlab (Greenhouse) | Remote, Canada | remote | USD 115,200 to 162,000 (interval not given) |
| Machine Learning Engineer - Subscriptions | spotify (Lever) | London | hybrid | not published |
| Software Engineer, Enterprise Product | ramp (Ashby) | New York, NY (HQ) | hybrid | USD 189,000 to 330,000 a year |
Each row also has the department, employment type, posted date and the job link. The full list of fields is on the Actor's page.
What it costs
The Actor charges per job a run saves, plus Apify's small start charge for each run. The current prices are in the Pricing tab of the Actor's page. With Only new or changed jobs on, later runs save, and charge, only the jobs that are new or changed: in our test the second run saved 0 jobs, so on Apify it would cost only the start charge. On Apify's free plan a run saves up to 100 jobs; with Only new or changed jobs on, the rest come in the next runs, as long as they are still posted. You can also set a maximum cost for each run in Apify Console.
Send the new jobs somewhere
- Script: replace the
printline with a call to a Slack or Microsoft Teams incoming webhook, or append the line to a CSV file you open in a spreadsheet. - Actor: the jobs of each run are a dataset you can download as JSON, CSV or Excel, or read through the Apify API. Apify's integrations and webhooks can send it on after each run, and workflow tools such as n8n, Make and Zapier have Apify integrations that can start the Actor and pass the jobs to Google Sheets, Slack or a database.
Which way to choose
- The script if you follow a few companies, are fine with title, location and link, and have a computer that is on every day. It costs nothing.
- The Actor if you follow many companies, need filters, salaries and one schema across the three systems, want changed jobs as well as new ones, or want it to run without your computer.
Common problems
- The first run sends hundreds of jobs.
- That is expected: every job is new the first time. Run it once by hand before you connect alerts, or keep the first run's output as your starting list.
- A company returns no jobs or an error.
- Check the board name in the job page address: it is often not the company name (for example a short name or the old name of the company). Companies also move from one system to another; when the board is empty, look at the careers page again.
- Greenhouse boards on
job-boards.eu.greenhouse.io. - The script above and the Actor read the main Greenhouse API only, so EU-hosted boards are not covered.
- The same job shows up again as new.
- Some employers close and repost a job, which gives it a new ID. In the Actor, a job that was missing from the board during a run and is posted again later is also returned as new.
- "Remote" jobs that are really hybrid.
- Each system has its own remote field, and employers fill them in differently. The Actor's Remote jobs only option keeps jobs whose work type is remote; to keep every job with "Remote" anywhere in its location, use Location keywords with
Remoteinstead.
Questions and answers
- Is it allowed to read these job boards?
- These are the public APIs the three systems document for publishing job postings, and they need no login. Keep a gentle pace (one request per board, with a pause), and check that your use of the job texts is allowed where you are.
- Can I search jobs across all companies on Greenhouse, Lever or Ashby?
- No. Their public APIs work one board at a time, and neither the script nor the Actor has a directory of companies. You start from your own list.
- How often should I check?
- Once a day is enough for most uses, such as applying early or tracking hiring signals. With the Actor's only-new mode, a job already returned is charged again only if it changes or comes back after disappearing from the board, so each extra check adds the start charge plus the jobs that are new or changed since the last run.
- Does it work for Workday career sites?
- Not with this script or this Actor. Workday has its own guide: how to export job postings from a Workday career site, with a free script and our Workday Jobs Scraper, which has the same output fields and its own only-new mode.
Script and Actor tested by Quicksloth on 24 October 2026 on the public job boards of GitLab, Spotify and Ramp; job counts change every day. Apify Console names come from Apify's documentation read on 13 October 2026. Written with AI tools and checked by Quicksloth. Quicksloth is an independent developer and is not affiliated with Greenhouse, Lever, Ashby, Apify or the companies named. Greenhouse, Lever, Ashby and Apify are trademarks of their respective owners.