ATS Job Scraper — Greenhouse, Lever & Ashby Jobs API avatar

ATS Job Scraper — Greenhouse, Lever & Ashby Jobs API

Pricing

from $1.00 / 1,000 job results

Go to Apify Store
ATS Job Scraper — Greenhouse, Lever & Ashby Jobs API

ATS Job Scraper — Greenhouse, Lever & Ashby Jobs API

ATS job scraper for Greenhouse, Lever & Ashby: turn company names into live job openings straight from each company's own board via the public keyless APIs - title, location, remote flag, department, salary (annualized), posting date, canonical URL. No API key, no proxy, no browser.

Pricing

from $1.00 / 1,000 job results

Rating

0.0

(0)

Developer

Flash Scrape

Flash Scrape

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 hours ago

Last modified

Categories

Share

Turn company names into their live job openings. Give the actor a list of companies ("stripe", "openai", "figma"); for each one it finds the company's public job board on Greenhouse, Lever or Ashby — the three ATSes behind most tech-company careers pages — and returns every live posting as a clean, uniform row.

It reads the ATSes' own public job-board APIs directly. No API key, no proxy, no headless browser — just the same JSON the company's careers page loads. That makes it fast (a whole 500-job board is one HTTP call), reliable, and safe to run on a schedule.

What you get per job: company, title, location, remote flag, employment type, department/team, salary range (min/max/currency, annualized), canonical apply URL, posting date, a ~300-character description snippet — and the full description when you switch includeDescription on.

Built-in monitoring: turn on onlyNewJobs and schedule the actor — the first run is the baseline, every later run delivers (and bills) only postings it has never delivered before. A run where nothing changed delivers 0 rows and charges $0. No downstream diffing needed.

Good for:

  • Job alerts & company watchlistsonlyNewJobs + a daily schedule = only the new openings, ever (see the walkthrough below).
  • Recruiting & sourcing — see exactly who is hiring for what, straight from the source.
  • Sales signals — a company hiring 12 SDRs or opening a Berlin office is telling you something.
  • Job boards & aggregators — fresh first-party postings with canonical apply links.

Example output

A real row from a live run (bare default input, 2026-08-15):

{
"company": "datadog",
"title": "AI Research Scientist - Datadog AI Research (DAIR)",
"location": "New York, New York, USA",
"remote": null,
"employment_type": "full-time",
"department": "Dev Eng",
"salary_min": 320000,
"salary_max": 400000,
"salary_currency": "USD",
"salary_interval": "yearly",
"url": "https://careers.datadoghq.com/detail/6572669/?gh_jid=6572669",
"source_ats": "greenhouse",
"posted_at": "2026-08-12",
"description_snippet": "As a Research Scientist on our team, you will partner with Research Engineers, working on fundamental research problems and collaborating…"
}

Input

FieldTypeDefaultWhat it does
atsCompaniesarray of strings["stripe", "openai", "cloudflare", "datadog", "figma"]Company names or board slugs. Each is checked on Greenhouse, then Lever, then Ashby — the first board with live jobs wins. Your own list replaces the sample entirely.
keywordstring"" (off)Only keep jobs whose title contains this text (case-insensitive), e.g. engineer.
locationContainsstring"" (off)Only keep jobs whose location contains this text (case-insensitive), e.g. remote, London.
onlyRemotebooleanfalseOnly keep jobs the board itself marks remote. No guessing: a job without remote evidence is dropped, not claimed.
onlyNewJobsbooleanfalseDeliver (and bill) only postings not delivered by earlier runs of this same watch. First run = baseline. A no-news run delivers 0 rows and charges $0.
includeDescriptionbooleanfalseAdd a description column with the complete HTML-stripped text. Off by default to keep rows light; the snippet is always included.
maxItemsinteger100 (max 1000)Total cap across all companies, split round-robin so one giant board cannot crowd out the others.

Filters run before billing — a filtered job is never charged. Running with completely empty input works out of the box and returns live jobs from the sample companies.

Finding a slug: it is the last path segment of the careers page URL — job-boards.greenhouse.io/<slug>, jobs.lever.co/<slug>, or jobs.ashbyhq.com/<slug>. A plain lowercase company name usually just works ("Stripe Inc." is tried as stripeinc). The run's status message names every company that resolved (and on which ATS) and every company that was not found, so a bad slug is never silent.

Output columns

ColumnNotes
companyThe name you supplied.
titleJob title as posted.
locationAs the board publishes it (city, "Remote", multi-office strings…).
remotetrue when the board explicitly marks it remote; null when there is no evidence either way — never guessed.
employment_typeNormalized: full-time, part-time, contract, internship, temporary. From Ashby's employmentType, Lever's commitment, Greenhouse "Time Type" metadata. Measured fill: 53% of the 2,216 live jobs across the five sample companies (2026-08-15).
departmentDepartment/team when the board provides one.
salary_min / salary_maxPublished pay range. Hourly/weekly/monthly figures are annualized to yearly (40h×52wk arithmetic) with a magnitude sanity guard — a suspicious conversion ships the raw numbers instead. Measured fill: 47% of the 2,216 live jobs across the five sample companies (2026-08-15); varies hard by company — Ashby boards with published compensation hit 85%, companies that publish no pay (e.g. Stripe) are ~0%.
salary_currencyISO code (USD, EUR, GBP…) when the board states one.
salary_intervalyearly after annualization; the original interval when a conversion would not be safe.
urlCanonical posting URL on the company's own board (deduplicated on this).
source_atsgreenhouse, lever or ashby.
posted_atISO date when the board provides one (Greenhouse exposes last-updated, Lever/Ashby creation/publish dates).
description_snippetFirst ~300 characters of the description, HTML stripped. Always present.
descriptionThe complete HTML-stripped description — only when includeDescription: true. 100% filled on every board tested.

Where the salary numbers come from (structured sources first, text parsing only as fallback): Greenhouse pay-transparency metadata (currency_range) then a salary-context window of the description; Lever's structured salaryRange then salaryDescription; Ashby's compensationTierSummary. Equity percentages, funding amounts and 401(k) mentions are explicitly excluded by the parser.

Use it from your code (API)

JavaScript:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: 'YOUR_APIFY_TOKEN' });
const run = await client.actor('flash_scraper/ats-job-scraper').call({
atsCompanies: ['anthropic', 'vercel', 'ramp'],
keyword: 'engineer',
onlyRemote: true,
maxItems: 200,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);

Python:

from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("flash_scraper/ats-job-scraper").call(run_input={
"atsCompanies": ["anthropic", "vercel", "ramp"],
"keyword": "engineer",
"onlyRemote": True,
"maxItems": 200,
})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
print(job["company"], "-", job["title"], "-", job["url"])

curl:

curl -X POST "https://api.apify.com/v2/acts/flash_scraper~ats-job-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"atsCompanies": ["anthropic", "vercel"], "keyword": "engineer", "maxItems": 100}'

Turn it into a job alert (onlyNewJobs + a schedule)

onlyNewJobs: true gives the actor a memory. It remembers every posting it has delivered for a given watch (same companies + same filters) in a small key-value store named ats-job-monitor in your own Apify account, and on the next run drops those rows before billing — you pay only for postings you have never seen.

Setup, once:

  1. Open the actor → Input → set your companies and filters, switch Only new jobs since the last run on. Save the input.
  2. Go to Schedules (Apify Console → Schedules → Create new) → pick a cron like 0 7 * * * (every morning at 07:00) → add this actor as the scheduled task with that input.
  3. Run it once manually. The first run is the baseline: it delivers everything currently live and its status message says so.

From then on:

  • Each scheduled run delivers only postings that were not delivered before. New rows land in that run's dataset — pipe them anywhere (step 4 below).
  • A morning where nothing changed ends green with the status message "Nothing new since your last run (…): all N live job(s) matching this watch were already delivered… You were not charged." Zero rows, zero charge — an empty alert costs nothing.
  • Memory is per-watch: change the company list or filters and a new baseline starts (the old one is kept). Entries expire after 90 days, so a repost after a long gap counts as new.

4. Get pinged — any of these work on top of the schedule:

  • n8n: community Apify node (or HTTP Request) → "Run actor and get dataset items" with your input → an IF node "items > 0" → Slack/email node. With onlyNewJobs on, no diff step is needed — every item IS news.
  • Make (Integromat): Apify → Run an ActorGet Dataset Items → filter "bundle count > 0" → email/Sheets/Notion.
  • Zapier: Schedule trigger → Apify → Run Actor → second Apify step fetches dataset items → Slack/Gmail.
  • Apify only: Schedules + the actor's "Run succeeded" notification, and read the status message — it says how many new jobs arrived.

Honest limits

  • Coverage is exactly Greenhouse + Lever + Ashby. A company on Workday, SmartRecruiters, iCIMS, Recruitee, or a home-grown careers page will come back "not found" — that is a coverage boundary, not an error. The status message names every such company.
  • Slug guessing is simple. Names are lowercased and stripped to letters/digits. Most companies match; some use a slug that differs from their brand name (check their careers URL and pass the slug directly).
  • remote is evidence-based. Only an explicit remote flag or "remote" in the location sets it true; everything else is null. onlyRemote therefore undercounts rather than lies.
  • posted_at semantics differ per ATS. Greenhouse exposes last updated, Lever created at, Ashby published at. It can be missing.
  • Salary coverage depends on the employer, not the actor. Measured 2026-08-15 across the five sample companies (2,216 live jobs): 47% carry a parsed range overall — 85% on a compensation-publishing Ashby board, ~0% at companies that publish no pay anywhere. The columns are null when the company publishes nothing; numbers are never invented.
  • onlyNewJobs memory starts at your first run. The baseline run delivers (and bills) everything currently live; "new" is defined against what your runs have delivered, not against a global database. Memory entries expire after 90 days.
  • Big boards are capped fairly. maxItems is a global cap split round-robin; raise it (up to 1000) to pull an entire large board.

Need LinkedIn, Indeed and Glassdoor too - merged and deduplicated across 12 boards with only-new-jobs monitoring? -> Multi Job Board Scraper

Related: LinkedIn Jobs Scraper — search LinkedIn postings by keyword and location.