跳到正文
DEV Community· Scott Williams·· 3 小時前AI 評分56

如何透過公開 JSON 端點讀取 Greenhouse、Lever 和 Ashby 職位列表

Reading Greenhouse, Lever and Ashby job boards from their public JSON endpoints

AI 導讀

作者以 Python 讀取 Greenhouse、Lever 和 Ashby 的公開職位 JSON 端點,並將三者資料整理成統一格式。端點分別為 https://boards-api.greenhouse.io/v1/boards/{board}/jobs?

正文

AI disclosure: this post was written with AI assistance (drafting and editing). I ran every request and code sample below myself on October 28, 2026 and checked the output. Select "AI-Assisted" in the DEV editor before publishing.

If you want a list of open jobs at a set of companies, you usually do not need a headless browser. Most tech companies host their career page on an applicant tracking system (ATS), and three of the most common ones, Greenhouse, Lever and Ashby, expose the same postings their career pages render as plain JSON. No API key, no login.

This post covers the three endpoints, the fields worth keeping, and a small reader that turns all three into one shape.

1. Find the board name

The board name is in the career page URL:

ATS Career page looks like Board name
Greenhouse boards.greenhouse.io/airbnb or job-boards.greenhouse.io/airbnb airbnb
Lever jobs.lever.co/spotify spotify
Ashby jobs.ashbyhq.com/ramp ramp

If a company uses its own domain, view the page source and search for greenhouse, lever.co or ashbyhq. The embed script or the apply links usually give it away.

2. The endpoints

Greenhouse (Job Board API, docs):

GET https://boards-api.greenhouse.io/v1/boards/{board}/jobs?content=true

Returns {"jobs": [...], "meta": {"total": N}}. Each job has id, title, location.name, absolute_url, updated_at, first_published and, with content=true, an HTML-escaped description in content. Drop content=true if you only need titles; the response gets much smaller.

Lever (Postings API, docs):

GET https://api.lever.co/v0/postings/{site}?mode=json

Returns a bare JSON array. Useful fields: id, text (the title), categories.location, categories.team, categories.commitment, workplaceType, hostedUrl, createdAt (epoch milliseconds). Companies on Lever's EU instance use api.eu.lever.co instead. An unknown site returns {"ok": false, "error": "Document not found"} with a 404, so check for a list before iterating.

Ashby (public job posting API, docs):

GET https://api.ashbyhq.com/posting-api/job-board/{board}?includeCompensation=true

Returns {"jobs": [...]}. Fields: id, title, department, team, location, secondaryLocations, isRemote, workplaceType, employmentType, publishedAt, jobUrl, applyUrl. With includeCompensation=true, many jobs carry compensation.compensationTierSummary, a ready-to-display string such as $211.4K – $290.6K • Offers Equity. Salary data is the main reason I reach for Ashby boards first.

3. One reader for all three

Standard library only:

import json
import urllib.request

UA = {"User-Agent": "job-board-reader/1.0 (+you@example.com)"}


def get_json(url):
    req = urllib.request.Request(url, headers=UA)
    with urllib.request.urlopen(req, timeout=20) as r:
        return json.load(r)


def greenhouse(board):
    data = get_json(f"https://boards-api.greenhouse.io/v1/boards/{board}/jobs?content=true")
    for j in data["jobs"]:
        yield {
            "source": "greenhouse", "id": str(j["id"]), "title": j["title"],
            "location": (j.get("location") or {}).get("name"),
            "posted": j.get("first_published") or j.get("updated_at"),
            "url": j["absolute_url"],
        }


def lever(site):
    for j in get_json(f"https://api.lever.co/v0/postings/{site}?mode=json"):
        cats = j.get("categories") or {}
        yield {
            "source": "lever", "id": j["id"], "title": j["text"],
            "location": cats.get("location"),
            "posted": j.get("createdAt"),  # epoch milliseconds
            "url": j["hostedUrl"],
        }


def ashby(board):
    data = get_json(f"https://api.ashbyhq.com/posting-api/job-board/{board}?includeCompensation=true")
    for j in data["jobs"]:
        yield {
            "source": "ashby", "id": j["id"], "title": j["title"],
            "location": j.get("location"),
            "posted": j.get("publishedAt"),
            "url": j["jobUrl"],
        }


if __name__ == "__main__":
    jobs = [*greenhouse("airbnb"), *lever("spotify"), *ashby("ramp")]
    print(len(jobs), "jobs")

When I ran it, it printed 456 jobs (195 Greenhouse, 89 Lever, 172 Ashby), in under a second end to end.

4. Things that bite

  • Dates come in three formats. Greenhouse uses ISO 8601 with a local offset, Ashby uses ISO 8601 in UTC, and Lever uses epoch milliseconds. Normalize before you sort or filter by "posted in the last N days".
  • Greenhouse updated_at is not the posting date. It changes when a recruiter edits the job. Prefer first_published when it is present.
  • Locations are free text. "Remote - USA", "United States" and "New York, NY (HQ)" all appear. Ashby's isRemote and Lever's workplaceType are more reliable than parsing strings.
  • IDs are only unique per ATS. Prefix them (ashby:<id>) before you dedupe across sources.
  • Descriptions are HTML. Greenhouse's is HTML-escaped HTML, so unescape first, then strip tags.
  • Detecting new jobs is just a set difference: store the prefixed IDs you have seen, and on the next run keep only IDs you have not seen.

5. Be a polite client

These endpoints exist so career pages can render, and that is the reason they are open. Treat them that way:

  • Send a descriptive User-Agent with a contact address.
  • Check robots.txt. On the day I wrote this, api.lever.co/robots.txt allowed everything with Crawl-delay: 1, and boards-api.greenhouse.io/robots.txt disallowed only /embed/.
  • One request per board is enough. Each endpoint returns the whole board, so polling once or twice a day covers almost every use case.
  • Only collect what the company has already published. Do not try to reach application data or anything behind a login.

Workday career sites are the big platform missing from this list. They also serve JSON, but through a per-tenant POST endpoint with pagination and facets, which deserves a post of its own.

This article was created with the help of AI. The endpoints, numbers and code were tested by me before publishing.

來源:DEV Community · dev.to