ATS Job Scraper — Greenhouse, Lever & Ashby Jobs API
Pricing
from $1.00 / 1,000 job results
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
Maintained by CommunityActor 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 watchlists —
onlyNewJobs+ 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
| Field | Type | Default | What it does |
|---|---|---|---|
atsCompanies | array 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. |
keyword | string | "" (off) | Only keep jobs whose title contains this text (case-insensitive), e.g. engineer. |
locationContains | string | "" (off) | Only keep jobs whose location contains this text (case-insensitive), e.g. remote, London. |
onlyRemote | boolean | false | Only keep jobs the board itself marks remote. No guessing: a job without remote evidence is dropped, not claimed. |
onlyNewJobs | boolean | false | Deliver (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. |
includeDescription | boolean | false | Add a description column with the complete HTML-stripped text. Off by default to keep rows light; the snippet is always included. |
maxItems | integer | 100 (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
| Column | Notes |
|---|---|
company | The name you supplied. |
title | Job title as posted. |
location | As the board publishes it (city, "Remote", multi-office strings…). |
remote | true when the board explicitly marks it remote; null when there is no evidence either way — never guessed. |
employment_type | Normalized: 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). |
department | Department/team when the board provides one. |
salary_min / salary_max | Published 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_currency | ISO code (USD, EUR, GBP…) when the board states one. |
salary_interval | yearly after annualization; the original interval when a conversion would not be safe. |
url | Canonical posting URL on the company's own board (deduplicated on this). |
source_ats | greenhouse, lever or ashby. |
posted_at | ISO date when the board provides one (Greenhouse exposes last-updated, Lever/Ashby creation/publish dates). |
description_snippet | First ~300 characters of the description, HTML stripped. Always present. |
description | The 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 ApifyClientclient = 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:
- Open the actor → Input → set your companies and filters, switch Only new jobs since the last run on. Save the input.
- 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. - 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
onlyNewJobson, no diff step is needed — every item IS news. - Make (Integromat): Apify → Run an Actor → Get 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).
remoteis evidence-based. Only an explicit remote flag or "remote" in the location sets ittrue; everything else isnull.onlyRemotetherefore undercounts rather than lies.posted_atsemantics 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
nullwhen the company publishes nothing; numbers are never invented. onlyNewJobsmemory 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.
maxItemsis 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.