Last checked
How to find which hiring platform a company uses, and pull its jobs
Most tech companies post jobs through one of a few hiring platforms, and Greenhouse, Lever and Ashby each publish a public job-board API that needs no key. You usually don’t know which one a company uses. The quickest way to find out is to try the company’s slug on all three. The platform that answers with jobs is the one it uses, and that answer is already the job list.
Checked on 19 October 2026:
| Slug | Greenhouse | Lever | Ashby |
|---|---|---|---|
stripe | 200 (723 jobs) | 404 | 404 |
spotify | 404 | 200 (82 jobs) | 404 |
linear | 404 | 404 | 200 (29 jobs) |
In short
- Guess the slug: the company name in lowercase, or the slug from a board link on its careers page.
- GET
https://boards-api.greenhouse.io/v1/boards/{slug}/jobs. - GET
https://api.lever.co/v0/postings/{slug}?mode=json. - GET
https://api.ashbyhq.com/posting-api/job-board/{slug}. - Keep the one that returns 200. A 404 just means “not here”.
Check all three in one script
import json
import time
from urllib.error import HTTPError
from urllib.request import Request, urlopen
ENDPOINTS = {
"greenhouse": "https://boards-api.greenhouse.io/v1/boards/{slug}/jobs",
"lever": "https://api.lever.co/v0/postings/{slug}?mode=json",
"ashby": "https://api.ashbyhq.com/posting-api/job-board/{slug}",
}
def get_json(url):
request = Request(url, headers={"Accept": "application/json", "User-Agent": "LepidataGuide/1.0 (+https://lepidata.com; [email protected])"})
try:
with urlopen(request, timeout=30) as response:
return json.load(response)
except HTTPError as error:
if error.code == 404:
return None
raise
def find_platform(slug):
for platform, template in ENDPOINTS.items():
data = get_json(template.format(slug=slug))
if data is None:
time.sleep(1)
continue
jobs = data if platform == "lever" else data.get("jobs", [])
return platform, jobs
return None, []
for slug in ["stripe", "spotify", "linear"]:
platform, jobs = find_platform(slug)
print(slug, platform, len(jobs))
The three responses don’t match
Each platform names the same things differently, so if you combine them you’ll need a mapping:
| Greenhouse | Lever | Ashby | |
|---|---|---|---|
| Job list | jobs | the top-level array | jobs |
| Title | title | text | title |
| Link | absolute_url | hostedUrl | jobUrl |
| Date | updated_at, first_published | createdAt (epoch milliseconds) | publishedAt |
| Description | only with ?content=true | descriptionPlain | descriptionPlain |
Greenhouse leaves out descriptions unless you add ?content=true, which makes the response much larger on a big board. Lever’s createdAt is a number of milliseconds, not a date string. Ashby includes an isListed flag, so skip jobs where it’s false.
Be a polite client
One request per platform returns the whole board, so you never need to page through results. Cache the answer, wait a second between companies, and back off on a 429. These are the public posting APIs that the platforms provide for career pages. Don’t use the private recruiting APIs (Greenhouse Harvest, Lever’s authenticated API), which need the company’s own key.
When to stop writing this yourself
Guessing slugs, telling a real 404 from an outage, mapping three formats, and remembering what changed since yesterday is where a quick script turns into a project. We publish that tool on Apify. The Career Site Jobs Scraper takes up to 200 company names or career-site URLs per run, checks Greenhouse, Lever and Ashby for each, and returns every job in one format. With onlyNewSinceLastRun it returns only jobs added or changed since your last run. A company it can’t find comes back as a free board-error row. It runs on Apify and is billed per job delivered. For Workday career sites, use the Workday Jobs Scraper.
Call it from code or an agent
The Career Site Jobs Scraper is the Apify Actor lepidata/career-site-jobs. Give it company names like stripe or career-site URLs like https://jobs.ashbyhq.com/linear, mixed across platforms in one run. The call below runs it and returns the result rows in one response. Keep your Apify token in an environment variable or a credential store, and send it in the Authorization header, never in a URL.
curl -s -X POST "https://api.apify.com/v2/acts/lepidata~career-site-jobs/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
-d '{"boards": ["https://jobs.ashbyhq.com/linear"], "titleKeyword": "engineer", "maxJobsPerBoard": 100}'
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("lepidata/career-site-jobs").call(run_input={"boards": ["https://jobs.ashbyhq.com/linear"], "titleKeyword": "engineer", "maxJobsPerBoard": 100})
items = client.dataset(run["defaultDatasetId"]).list_items().items
results = [row for row in items if row.get("changeType") in ("new", "updated")]
To run it as a daily monitor, run once with "startFromNow": true to record what’s there now for free, then schedule this input with the same stateKey:
{
"boards": [
"stripe",
"https://jobs.lever.co/spotify"
],
"onlyNewSinceLastRun": true,
"stateKey": "my-companies",
"includeRemoved": true
}
Job rows have changeType set to new or updated, or removed when you turn on includeRemoved. Status rows such as board-error are free and carry an error or message field. Apify Free-plan accounts get up to 100 jobs per run.
Written with AI assistance and checked by the Lepidata team.