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:

SystemJob page address looks likeBoard name
Greenhousejob-boards.greenhouse.io/gitlab/jobs/… or boards.greenhouse.io/gitlabgitlab
Leverjobs.lever.co/spotify/…spotify
Ashbyjobs.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.

  1. Open the Actor's page and start it in Apify Console (you need an Apify account; the free plan is enough to try it).
  2. Paste your companies in Companies, one per line. Add Job title keywords if you only care about some roles.
  3. Tick Only new or changed jobs, and give the watch list a name in History name, for example weekly-engineering.
  4. 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.
  5. 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:

TitleBoardLocationWorkplaceSalary
Backend Engineer, Knowledge Graph (Rust)gitlab (Greenhouse)Remote, CanadaremoteUSD 115,200 to 162,000 (interval not given)
Machine Learning Engineer - Subscriptionsspotify (Lever)Londonhybridnot published
Software Engineer, Enterprise Productramp (Ashby)New York, NY (HQ)hybridUSD 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

Which way to choose

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 Remote instead.

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.