DEV Community

clearpath-data
clearpath-data

Posted on Edited on

Watching a company's hiring week to week: snapshot and diff Greenhouse, Lever and Ashby job boards

"Spotify has 82 open jobs" is a weak signal. "Spotify opened 17 roles and closed 14 in ten days" is a much stronger one, and you can't get it from a careers page. You get it by taking snapshots and diffing them. Here's a small, dependency-light way to do that for companies on Greenhouse, Lever or Ashby.

Step 1: one snapshot function for three ATSs

All three ATSs publish public JSON job boards. For a diff you only need a stable key per posting (its public URL) and something human-readable (the title):

ATS Endpoint URL field Title field
Greenhouse boards-api.greenhouse.io/v1/boards/{slug}/jobs absolute_url title
Lever api.lever.co/v0/postings/{slug}?mode=json hostedUrl text
Ashby api.ashbyhq.com/posting-api/job-board/{slug} jobUrl title

The slug is the board name in the careers URL (jobs.lever.co/spotify → spotify).

"""Save today's open jobs for one company board as {job_url: title}."""
import datetime, json, pathlib, sys
import requests

FETCH = {
    "greenhouse": lambda s: [(j["absolute_url"], j["title"]) for j in
        requests.get(f"https://boards-api.greenhouse.io/v1/boards/{s}/jobs", timeout=30).json()["jobs"]],
    "lever": lambda s: [(j["hostedUrl"], j["text"]) for j in
        requests.get(f"https://api.lever.co/v0/postings/{s}", params={"mode": "json"}, timeout=30).json()],
    "ashby": lambda s: [(j["jobUrl"], j["title"]) for j in
        requests.get(f"https://api.ashbyhq.com/posting-api/job-board/{s}", timeout=30).json()["jobs"]],
}

def snapshot(ats, slug, folder="snapshots"):
    jobs = dict(FETCH[ats](slug))
    path = pathlib.Path(folder) / f"{ats}-{slug}-{datetime.date.today()}.json"
    path.parent.mkdir(exist_ok=True)
    path.write_text(json.dumps(jobs, indent=1, ensure_ascii=False))
    return path, len(jobs)

if __name__ == "__main__":
    print(*snapshot(sys.argv[1], sys.argv[2]))
Enter fullscreen mode Exit fullscreen mode

Running it on 19 Oct 2026:

$ python snapshot.py lever spotify
snapshots/lever-spotify-2026-10-19.json 82
$ python snapshot.py greenhouse stripe
snapshots/greenhouse-stripe-2026-10-19.json 729
$ python snapshot.py ashby openai
snapshots/ashby-openai-2026-10-19.json 814
Enter fullscreen mode Exit fullscreen mode

Run it from cron (or GitHub Actions) once a day. Each snapshot is a small JSON file, so a year of daily snapshots per company is easy to keep in git.

Step 2: diff two snapshots

"""Compare two snapshots: which postings opened, which closed."""
import json, sys

old, new = (json.load(open(p)) for p in sys.argv[1:3])
opened = sorted(set(new) - set(old), key=new.get)
closed = sorted(set(old) - set(new), key=old.get)
print(f"{len(old)} -> {len(new)} open jobs | +{len(opened)} opened | -{len(closed)} closed")
for url in opened[:5]:
    print("  +", new[url])
for url in closed[:5]:
    print("  -", old[url])
Enter fullscreen mode Exit fullscreen mode

I had a real Spotify baseline from 9 October (all 79 postings on its Lever board that day), so here is the 10-day diff:

$ python diff.py baseline-lever-spotify-2026-10-09.json snapshots/lever-spotify-2026-10-19.json
79 -> 82 open jobs | +17 opened | -14 closed
  + AV Engineer - Platform
  + Art Director - Mexico
  + Audiobook Licensing & Partnerships Manager - Japan
  + Client Partner ANZ
  + Client Partner, Emerging & Scaled
  - Android Engineer - Experience
  - C++ Engineer - Experience
  - Client Partner - Emerging & Scaled, Independent Agency (UK)
  - Director, Portfolio & Monetization Architecture
  - Growth Manager
Enter fullscreen mode Exit fullscreen mode

What the net number hides

The headline count moved by +3. Underneath, 31 postings changed: 17 opened and 14 closed, so roughly 40% of a 79-job board turned over in ten days. If you only track counts, you'd conclude nothing happened.

I also counted the same diff by title instead of URL. It gives 18 titles opened and 15 closed (some titles appear more than once on the board), so the churn isn't just the same job re-posted under a new URL.

A few practical notes from building this:

  • Key on the URL, not the title. Companies often run several postings with the same title in different cities. Title-keyed dicts silently collapse them.
  • "Closed" means "no longer on the board". That could mean filled, paused or moved. Treat it as a signal, not a fact.
  • Lever dates are epoch milliseconds (createdAt); Greenhouse and Ashby use ISO 8601. The diff approach sidesteps dates entirely, which is part of why I like it.
  • Be polite. These are companies' public career feeds. Once a day is plenty, and cache everything.

New postings vs. disappeared postings

Some job APIs let you filter to recently posted jobs. That tells you what's new, while the snapshot diff also tells you what disappeared. For hiring-trend work you usually want both.

All the code is in github.com/clearpath-data/ats-jobs-api.

This article was created with the help of AI and reviewed for accuracy before publishing.

Top comments (0)