How do I get live job listings straight from company ATS boards (Greenhouse, Lever, Ashby) via API?
Short answer: most tech and many enterprise employers publish their openings through an applicant-tracking system (ATS) that has a free, public, documented job-board API: Greenhouse (boards-api.greenhouse.io/v1/boards/{company}/jobs), Lever (api.lever.co/v0/postings/{company}) and Ashby (api.ashbyhq.com/posting-api/job-board/{company}). No key is needed. Reading jobs there gives you listings that are currently published on the employer's own hiring system, not copies on an aggregator. That is not proof the role is being filled, so check age and reposts too. For many companies, history and repost signals in one call, the Fairquery Company Jobs Feed returns normalized JSON rows for $0.99 per 1,000 jobs + $0.0005 per board scanned, and its MCP server has four free local tools.
Why read jobs straight from the company's ATS?
Job aggregators and scraped job-board copies lag behind the source. A role can be filled and removed from the employer's ATS while copies of it stay up elsewhere. If you are building a job alert, a hiring-signal feed, a sales trigger ("company X opened 12 data roles") or an agent that answers "who is hiring Rust engineers in Berlin right now?", the employer's own ATS board is the closest public source of truth: if the job is listed there now, the employer is publishing it now.
It is not a ghost-job detector on its own, though. Greenhouse itself reports that 18–22% of jobs posted on its platform in a given quarter are ghost jobs, so "published on the ATS" and "actively hiring" are different things. What you can measure from public data is how old a posting is, whether it has been open unusually long, and whether the same role was closed and posted again. Those are the signals this guide uses.
The three public job-board APIs
| ATS | Endpoint (no key) | What you get | Docs |
|---|---|---|---|
| Greenhouse | https://boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true | All published jobs with title, location, departments, offices, first_published, updated_at, HTML description. Per-job: /jobs/{id} (404 when closed) | Job Board API |
| Lever | https://api.lever.co/v0/postings/{company}?mode=json | Postings with team, department, location, workplaceType, createdAt, optional salary range. Per-job: /{id} | Postings API |
| Ashby | https://api.ashbyhq.com/posting-api/job-board/{name}?includeCompensation=true | Jobs with department, location, remote flag, publishedAt, compensation summary. No per-job endpoint | Job Postings API |
The {company} part is the board slug from the company's careers links (boards.greenhouse.io/stripe, jobs.lever.co/palantir and jobs.ashbyhq.com/ramp). Unknown slugs return 404.
What we left out and why. SmartRecruiters has a public Posting API, but the robots.txt on its API host allows only LinkedIn's crawler, so we don't call it. Workday has no public job API. Recruitee's Careers Site API needs a token from the employer. Workable documents its public endpoints only for an account's own careers page. LinkedIn is not used at all. We stick to APIs that are documented for outside use.
Option 1 (free): three ATS APIs in 25 lines of Python
Standard library only. It reads Stripe (Greenhouse), Palantir (Lever) and Ramp (Ashby), and prints engineering roles with their age:
# Free: read live jobs straight from three ATS job-board APIs (Python standard library only).
import json, urllib.request
from datetime import datetime, timezone
UA = {"User-Agent": "my-job-research/1.0 (you@example.com)"}
def get(url):
with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=15) as r:
return json.load(r)
now = datetime.now(timezone.utc)
def age(iso):
return (now - datetime.fromisoformat(iso.replace("Z", "+00:00"))).days if iso else None
gh = get("https://boards-api.greenhouse.io/v1/boards/stripe/jobs")["jobs"] # Greenhouse
lv = get("https://api.lever.co/v0/postings/palantir?mode=json") # Lever
ab = get("https://api.ashbyhq.com/posting-api/job-board/ramp")["jobs"] # Ashby
rows = [("greenhouse", j["title"], j["location"]["name"], j.get("first_published")) for j in gh]
rows += [("lever", j["text"], j["categories"].get("location"),
datetime.fromtimestamp(j["createdAt"] / 1000, timezone.utc).isoformat()) for j in lv]
rows += [("ashby", j["title"], j["location"], j.get("publishedAt")) for j in ab if j.get("isListed", True)]
print(len(gh), "Stripe /", len(lv), "Palantir /", len(ab), "Ramp jobs")
for ats, title, loc, posted in [r for r in rows if "engineer" in r[1].lower()][:4]:
print(f"{ats:10} {age(posted):>4}d {title[:48]:48} {loc}")
Real output (October 17, 2026, about 1 second for the three requests):
732 Stripe / 310 Palantir / 167 Ramp jobs
greenhouse 38d Abuse Research Engineer Remote from the US
greenhouse 10d AI Engineer Chicago
greenhouse 18d Android BSP Engineer Taipei, Taiwan
greenhouse 5d Android Engineer, Terminal - CA Toronto, CA
That works for a handful of boards. At scale you run into the parts that take the time:
- Three schemas. Dates are ISO strings on Greenhouse and Ashby and epoch milliseconds on Lever. Remote is a flag, a
workplaceTypeor free text depending on the system. Countries are structured on Lever and Ashby and free text on Greenhouse. - Finding the board. You need the slug, and many companies link to it only from a careers page.
- Change tracking. "New since yesterday", "closed", and "reposted" need stored history per board. A repost usually comes back under a new job id, so id-based diffs miss it.
- Politeness. Lever's robots.txt asks for a 1-second crawl delay, very large Greenhouse boards can exceed 40 MB with descriptions, and any 403 or 429 means you should stop, not retry harder.
Ghost jobs and dead listings: the signals you can measure
| Signal | How it is computed | What it does not prove |
|---|---|---|
verified_open_at | Time the job was read from the employer's ATS (or re-checked on its per-job endpoint) | That the role is still being filled |
posted_at, age_days | The ATS publish date (null when the ATS doesn't give one, never guessed); age falls back to first-seen and says so in age_basis | Some employers refresh dates when they edit a posting |
long_open | age_days above a threshold you set (default 60) | Evergreen and hard-to-fill roles are legitimately open for months |
reposted, repost_of | Same company + normalized title + location as a job that closed in the last 90 days, under a new id | Needs at least two tracked runs |
change, first_seen_at, last_seen_at | Per-board history: new, updated, unchanged, closed | Closures are only reported when the whole board was read |
Example from our test runs: Hightouch's Ashby board had one listing, a "Backend Engineer, Platform" role with posted_at 2021-03-16, so age_days 2041 and long_open: true. That is very likely an evergreen posting, which is exactly what the flag is for.
Option 2 (API or MCP): one feed across companies
The Fairquery Company Jobs Feed Actor (fairquery/company-jobs-feed) calls only the three documented API hosts above. You give it company names, domains, careers URLs or exact boards; it finds the board (careers-page links with robots.txt respected, then at most six slug guesses), reads it, normalizes every job into one schema, applies your filters, de-duplicates, and keeps per-board history in a key-value store in your own Apify account.
We ran it on October 17, 2026 on Stripe's Greenhouse board, filtered to engineering roles in San Francisco or remote, with descriptions and a per-job re-check for the first 10:
{
"boards": [
"greenhouse:stripe"
],
"titleIncludes": [
"engineer"
],
"locations": [
"remote",
"san francisco"
],
"includeDescription": "text",
"confirmOpen": "detail",
"maxOpenChecks": 10
}
Real output, abridged to the first Stripe job, one Ramp job from a second run (remote: "only", hasSalary: true) and the Stripe board row:
[
{
"type": "job",
"job_key": "greenhouse:stripe:8172503",
"company": "Stripe",
"title": "Abuse Research Engineer",
"department": "8611 Security Analytics",
"locations": [
"Remote from the US"
],
"countries": [
"US"
],
"remote": true,
"workplace_type": "remote",
"posted_at": "2026-09-09T14:52:09.000Z",
"age_days": 38,
"age_basis": "posted_at",
"long_open": false,
"verified_open_at": "2026-10-17T22:54:04.083Z",
"first_seen_at": "2026-10-17T22:54:03.320Z",
"change": "new",
"reposted": false,
"open_check": "detail_open",
"compensation": null,
"apply_url": "https://stripe.com/jobs/search?gh_jid=8172503",
"fingerprint": "d7127148fd018f2c"
},
{
"type": "job",
"job_key": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
"company": "Ramp",
"title": "Security Engineer, Cloud",
"department": "Engineering",
"locations": [
"New York, NY (HQ)",
"Remote (Canada)",
"Remote (US)",
"Miami, FL"
],
"countries": [
"US",
"CA"
],
"remote": true,
"workplace_type": "hybrid",
"posted_at": "2026-04-07T17:12:35.753Z",
"age_days": 193,
"age_basis": "posted_at",
"long_open": true,
"verified_open_at": "2026-10-17T22:54:03.924Z",
"first_seen_at": "2026-10-17T22:54:03.924Z",
"change": "new",
"reposted": false,
"open_check": "listed",
"compensation": {
"summary": "$211.4K – $290.6K • Offers Equity",
"min": 211400,
"max": 290600,
"currency": "USD",
"interval": "1 YEAR"
},
"apply_url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
"fingerprint": "646e3148231ba20d"
},
{
"type": "board",
"ats": "greenhouse",
"board": "stripe",
"company": "Stripe",
"jobs_total": 732,
"jobs_matched": 32,
"jobs_emitted": 32,
"complete": true,
"new": 732,
"updated": 0,
"closed": 0,
"reposted": 0,
"requests": 1,
"checked_at": "2026-10-17T22:54:03.320Z"
}
]
The Stripe run took 3.1 seconds, read 732 jobs in one request, matched 32 and re-checked 10 on Greenhouse's per-job endpoint (open_check: "detail_open"). It cost 32 × $0.00099 + 1 × $0.0005 + 10 × $0.00029 = $0.035.
"What's new since yesterday?" with onlyNew
Give each saved search its own stateStoreName and set onlyNew: true. The first run is the baseline; later runs return only new or changed jobs, plus free closed rows. Re-running Stripe and Ramp a few minutes later returned no job rows, only the two board rows ($0.0005 each):
{
"type": "board",
"ats": "greenhouse",
"board": "stripe",
"jobs_total": 732,
"jobs_emitted": 0,
"new": 0,
"updated": 0,
"closed": 0,
"reposted": 0,
"checked_at": "2026-10-17T23:04:29.186Z"
}
Company discovery
Input {"companies": ["gem.com", "vanta.com", "sentry.io"]}: Sentry's board was found from links on sentry.io's own careers page (confidence: "high"), Vanta's by a slug probe ("medium": the slug exists but could in principle be another company with the same name), and gem.com returned a free NO_BOARD_FOUND row because it doesn't use one of the three systems publicly:
[
{
"type": "company",
"input": "sentry.io",
"company": null,
"domain": "sentry.io",
"boards": [
{
"ats": "ashby",
"slug": "sentry",
"via": "careers_page",
"confidence": "high"
}
],
"careers_pages_fetched": [
"https://sentry.io/welcome/",
"https://sentry.io/careers/"
],
"probes": 0,
"cached": false,
"source_url": "https://sentry.io/welcome/",
"checked_at": "2026-10-18T00:11:58.387Z"
},
{
"type": "error",
"checked_at": "2026-10-18T00:11:58.570Z",
"code": "NO_BOARD_FOUND",
"message": "No Greenhouse, Lever or Ashby board found for \"gem.com\" (3 careers pages, 3 API probes).",
"retryable": false,
"input": "gem.com",
"source_url": null
},
{
"type": "company",
"input": "vanta.com",
"company": null,
"domain": "vanta.com",
"boards": [
{
"ats": "ashby",
"slug": "vanta",
"via": "slug_probe",
"confidence": "medium"
}
],
"careers_pages_fetched": [
"https://www.vanta.com/",
"https://www.vanta.com/company/careers"
],
"probes": 3,
"cached": false,
"source_url": "https://www.vanta.com/",
"checked_at": "2026-10-18T00:12:01.280Z"
}
]
From Python, curl or an AI agent
With the official apify-client package, as a daily US engineering monitor with a spend cap:
# pip install apify-client (set APIFY_TOKEN)
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("fairquery/company-jobs-feed").call(run_input={
"companies": ["stripe.com", "ramp.com", "https://jobs.lever.co/palantir"],
"titleIncludes": ["engineer"],
"countries": ["US"],
"postedWithinDays": 30,
"stateStoreName": "my-us-engineering-search",
"onlyNew": True, # from the 2nd run on: only new or changed jobs (+ free closed rows)
"maxJobs": 2000, # spend cap: at most 2,000 billed job rows
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["type"] == "job":
print(row["company"], row["title"], row["age_days"], row["long_open"], row["apply_url"])
Or one synchronous HTTP call that returns the rows directly (Palantir's Lever board, US engineering jobs posted in the last 180 days; our test run read 310 Palantir postings in one request and matched 36):
curl -s -X POST "https://api.apify.com/v2/acts/fairquery~company-jobs-feed/run-sync-get-dataset-items" \
-H "Authorization: Bearer $APIFY_TOKEN" -H "Content-Type: application/json" \
-d '{"companies": ["https://jobs.lever.co/palantir"], "countries": ["US"],
"titleIncludes": ["engineer"], "postedWithinDays": 180}'
For agents there are two routes. The Apify MCP server scoped to this Actor (https://mcp.apify.com/?tools=fairquery/company-jobs-feed) works in Claude Desktop, Cursor and VS Code. Or run the stdio MCP server @fairquery/company-jobs-feed-mcp (source in the GitHub repo): jobs_get_board, jobs_find_board, jobs_check_open and jobs_list_seed_boards run free on your machine with no token, and jobs_search_companies runs the Actor with your APIFY_TOKEN.
{
"mcpServers": {
"fairquery-jobs-feed": {
"command": "npx",
"args": ["-y", "@fairquery/company-jobs-feed-mcp"],
"env": { "APIFY_TOKEN": "only needed for the paid jobs_search_companies tool" }
}
}
}
Then ask, for example: "Which remote engineering jobs does Ramp have open right now, with salary ranges? Skip anything open for more than 90 days."
Filters
titleIncludes/titleExcludes(whole words, soengineerdoesn't match "Engineering Manager" unless you ask for it), or a safetitleRegexlocations(substring),countries(ISO codes, structured data where the ATS has it),remote:any/only/excludecompanyFilter,departments,employmentTypes,hasSalarypostedWithinDaysandmaxAgeDays;excludeStaledrops long-open and reposted rowsdedupe:exact(one row per job id; the same role on several boards is grouped by a fingerprint and markedduplicate_of, never silently dropped) orrole(one board's rows with the same title and department become one row listing all locations; merged rows are free)
Pricing
| Option | Price |
|---|---|
| The ATS APIs directly (your own code) | Free |
Fairquery MCP server, local tools (jobs_get_board, jobs_find_board, jobs_check_open, jobs_list_seed_boards) | Free |
| Fairquery Company Jobs Feed (Apify, pay per event) | $0.99 per 1,000 jobs delivered ($0.00099 each), $0.0005 per board scanned, $0.00029 per optional per-job re-check, $0.0019 per company found by discovery. Error, closed and merged rows, cached and starter-list lookups, and runs with empty input are free. |
Examples: every open job at Stripe, Palantir and Ramp (1,209 in our unfiltered test) costs 1,209 × $0.00099 + 3 × $0.0005 = about $1.20. A daily monitor of 50 companies with onlyNew that finds 20 new jobs a day costs 50 × $0.0005 + 20 × $0.00099 = about $0.045 per day. maxJobs and Apify's "max cost per run" cap every run; the Actor stops cleanly at the cap and charges nothing above it.
How it compares
| Option | Good for | Trade-offs |
|---|---|---|
| ATS APIs directly | Free and official; ideal for a few known boards | Three schemas, slug discovery, history, repost detection and rate-limit handling are yours to build |
| Job aggregator APIs and scraped job-board datasets | Broad coverage across employers and ATSs, including ones without public APIs | Second-hand copies that can lag behind or outlive the original posting; terms vary by source |
| Fairquery Company Jobs Feed | Jobs read live from employers' own Greenhouse, Lever and Ashby boards; one schema; age, long-open and repost signals; history; MCP tools; pay per job | Only these three systems (no SmartRecruiters, Workday, Workable, Recruitee or LinkedIn); reposts need two or more runs; slug-probe matches are medium confidence |
FAQ
Is there a free API for live job listings?
Yes, per employer. Greenhouse (boards-api.greenhouse.io), Lever (api.lever.co/v0/postings) and Ashby (api.ashbyhq.com/posting-api/job-board) publish documented job-board APIs that need no key. You need each company's board slug, and you get only that company's published jobs.
How do I find a company's Greenhouse, Lever or Ashby board?
Look for links on its careers page such as boards.greenhouse.io/<slug>, jobs.lever.co/<slug> or jobs.ashbyhq.com/<slug>. The Fairquery jobs_find_board tool (free) and the Company Jobs Feed Actor do this for you: careers-page links first (robots.txt respected), then at most six slug guesses against the APIs.
Does this filter out ghost jobs?
No tool can prove hiring intent from public data, and Greenhouse reports that 18 to 22% of jobs on its own platform in a given quarter are ghost jobs. What the feed gives you is verified_open_at (published on the employer's ATS right now), posted_at and age_days, a long_open flag, and repost detection across runs, so you can filter out the listings most likely to be stale.
How fresh is the data?
Every run reads the employer's ATS live; nothing is served from a cache of job data. verified_open_at is the time of that read. With confirmOpen the Actor also re-checks each job on its per-job endpoint (Greenhouse, Lever) for $0.00029 per job.
Which ATS systems are supported?
Greenhouse, Lever and Ashby. SmartRecruiters is left out because the robots.txt on its API host allows only LinkedIn's crawler. Workday has no public job API, Recruitee's careers API needs an employer token, and Workable documents its public endpoints only for an account's own careers page, so they are not included. LinkedIn is not used.
How much does the Fairquery Company Jobs Feed cost?
$0.99 per 1,000 jobs delivered, $0.0005 per board scanned, $0.00029 per optional per-job re-check and $0.0019 per company found by discovery, pay per event on Apify. Error, closed and merged rows and empty-input runs are free. The four local MCP tools are free.
Can AI agents use it?
Yes. Add the Apify MCP server scoped to fairquery/company-jobs-feed, or run the stdio MCP server from the GitHub repo: jobs_get_board, jobs_find_board, jobs_check_open and jobs_list_seed_boards run free on your machine, and jobs_search_companies runs the Actor with your Apify token.
Is it allowed to use these APIs?
They are public job-board APIs that the ATS vendors document for publishing an employer's jobs. The Fairquery Actor calls only those documented hosts, without login, at most two concurrent requests per host, honoring Lever's crawl delay, and it stops calling a host on the first 403 or 429. Job content belongs to the employers; check that your use (for example republishing) fits their terms.
About the data. Jobs belong to the employers and are returned as published, with light normalization. Age, remote, country, long-open and repost fields are signals, labelled as such, not guarantees that a role is being hired for. Fairquery is not affiliated with Greenhouse, Lever or Ashby.
Source and docs: github.com/fairquery/company-jobs-feed (MIT) · Apify Store · all Fairquery tools