FairqueryBlogToolsAboutSecurityllms.txt

How do I get live job listings straight from company ATS boards (Greenhouse, Lever, Ashby) via API?

Fairquery ·

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

ATSEndpoint (no key)What you getDocs
Greenhousehttps://boards-api.greenhouse.io/v1/boards/{token}/jobs?content=trueAll published jobs with title, location, departments, offices, first_published, updated_at, HTML description. Per-job: /jobs/{id} (404 when closed)Job Board API
Leverhttps://api.lever.co/v0/postings/{company}?mode=jsonPostings with team, department, location, workplaceType, createdAt, optional salary range. Per-job: /{id}Postings API
Ashbyhttps://api.ashbyhq.com/posting-api/job-board/{name}?includeCompensation=trueJobs with department, location, remote flag, publishedAt, compensation summary. No per-job endpointJob 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:

Ghost jobs and dead listings: the signals you can measure

SignalHow it is computedWhat it does not prove
verified_open_atTime 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_daysThe ATS publish date (null when the ATS doesn't give one, never guessed); age falls back to first-seen and says so in age_basisSome employers refresh dates when they edit a posting
long_openage_days above a threshold you set (default 60)Evergreen and hard-to-fill roles are legitimately open for months
reposted, repost_ofSame company + normalized title + location as a job that closed in the last 90 days, under a new idNeeds at least two tracked runs
change, first_seen_at, last_seen_atPer-board history: new, updated, unchanged, closedClosures 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

Pricing

OptionPrice
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

OptionGood forTrade-offs
ATS APIs directlyFree and official; ideal for a few known boardsThree schemas, slug discovery, history, repost detection and rate-limit handling are yours to build
Job aggregator APIs and scraped job-board datasetsBroad coverage across employers and ATSs, including ones without public APIsSecond-hand copies that can lag behind or outlive the original posting; terms vary by source
Fairquery Company Jobs FeedJobs read live from employers' own Greenhouse, Lever and Ashby boards; one schema; age, long-open and repost signals; history; MCP tools; pay per jobOnly 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