ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workday & More
Pricing
from $1.20 / 1,000 jobs
ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workday & More
Scrape jobs from Greenhouse, Lever, Workday, Ashby, Workable, SmartRecruiters & Recruitee in one schema with salary & remote data. Monitor mode: only new jobs.
Pricing
from $1.20 / 1,000 jobs
Rating
0.0
(0)
Developer
tessellware
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 hours ago
Last modified
Categories
Features
Share
Collect job postings from company career pages that run on seven major applicant tracking systems (ATS) and get them in one clean, unified format. Paste career page URLs, ATS board URLs, or company names. The Actor detects the ATS, pulls the open jobs, normalizes the fields (including salary where the employer publishes it), and can run in monitoring mode to give you only the jobs that are new since your last run.
You are billed only for jobs written to the dataset. Jobs removed by your filters, jobs already seen in monitoring mode, duplicates and jobs skipped by your limits are never written and never billed.
Maintained by Tessellware.
Quick start
- Open the Actor and paste one or more companies into Companies: a board URL such as
https://boards.greenhouse.io/lattice, a career page URL, or a company name. The prefilled example is small (about 40 jobs) and capped at 100 jobs in total. - Optionally add filters (keywords, locations, remote only, posted within N days) and pick a description format.
- Click Start, then open the Jobs overview table, or download the dataset as JSON, CSV or Excel. For a recurring feed of new jobs, turn on Only new jobs and add a schedule.
How much does it cost to scrape job postings?
You pay per job written to the dataset (see Pricing). At $2.00 per 1,000 jobs, scanning 50 companies with about 40 open jobs each is about 2,000 jobs, or about $4.00 for the first run. With monitoring mode on and a daily schedule, the following runs write only newly posted jobs, typically a small fraction of that. Set Max jobs in total (or the maximum cost per run in the Apify Console) to cap any run.
What you can do with it
- Recruiters and sourcers: build a feed of openings across many employers, filtered by keyword, location, department or remote.
- Job-board operators and aggregators: one schema for every source, with stable
jobIds and canonicaljobUrl/applyUrllinks. - Sales and RevOps teams: use hiring as a buying signal. The per-company
COMPANY_SUMMARYshows how many roles a company has open, in which departments and locations, and when it last posted. - Market and labor researchers: track demand for roles, skills and locations over time, with salary ranges where employers publish them.
How it works
The Actor reads the public job-board APIs and the public JSON endpoints that power companies' careers pages. No login, no cookies, no proxies and no CAPTCHA bypass. Greenhouse, Lever, Ashby, SmartRecruiters and Recruitee publish job-board APIs; Workday's /wday/cxs endpoint and Workable's v3 listing are the JSON endpoints behind their public careers pages. Requests are paced (at most 4 at a time per host), retried with back-off on rate limits and server errors, and identify themselves honestly.
Supported systems
| ATS | Example input | Notes |
|---|---|---|
| Greenhouse | https://boards.greenhouse.io/stripe, https://job-boards.greenhouse.io/stripe, embed URLs with ?for=stripe | Department/team hierarchy, pay-transparency ranges (every pay zone) |
| Lever | https://jobs.lever.co/palantir, https://jobs.eu.lever.co/... (EU) | Workplace type, salary range when published |
| Ashby | https://jobs.ashbyhq.com/openai | Structured compensation (per tier), remote/hybrid flags |
| Workable | https://apply.workable.com/huggingface, https://huggingface.workable.com | Full descriptions, remote/hybrid |
| SmartRecruiters | https://careers.smartrecruiters.com/Experian | Description via one extra request per job |
| Recruitee | https://bunq.recruitee.com | Descriptions, requirements, salary when published |
| Workday | https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite | Needs the full Workday board URL (tenant + site). Posting date and full description via one extra request per job |
Greenhouse boards hosted under job-boards.eu.greenhouse.io are parsed and queried through the standard Greenhouse endpoint. That path has not been verified against a live EU-hosted board, so paste the board URL and check the result.
Company names, career pages and bare names
- Career page URL (for example
https://example.com/careers): the Actor fetches that one page (public hosts only, redirects re-checked, at most 3 MB read) and looks for links to supported boards. At most 3 board links are taken from a page; the report notes if there were more. - Bare name or domain (
stripe,Hugging Face,airbnb.com): the Actor tries sensible slug variants against the supported systems. A name can match boards at several vendors (for examplemercury). By default only the single best match is scraped, chosen by: exact company name where the API gives one, then the most jobs, then the order Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee. The other matches are listed underalternativesinRESOLUTION_REPORT. Set Use every matching board for a bare name (scrapeAllMatches) to scrape all matches that have jobs. Pasting the exact board URL always avoids ambiguity. - Workday cannot be found from a bare name, because its URL contains a company-specific site name. Use the full Workday URL.
- Duplicates: the same board given twice (for example
boards.greenhouse.io/figma,job-boards.greenhouse.io/figmaandfigma) is scraped and billed once. Later copies appear inRESOLUTION_REPORTasduplicate of <input>.
Input
Only companies is required.
{"companies": ["https://boards.greenhouse.io/lattice","https://jobs.ashbyhq.com/linear","https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"],"keywords": ["data engineer", "machine learning"],"locations": ["London", "Remote"],"descriptionFormat": "markdown","maxJobsPerCompany": 200,"maxTotalJobs": 1000}
| Field | What it does |
|---|---|
companies | Career page URLs, ATS board URLs, slugs, names or domains. |
scrapeAllMatches | For bare names that match several systems, scrape every match with jobs instead of the single best one. |
keywords / excludeKeywords | Keep jobs whose title contains any keyword / drop jobs whose title contains any exclude word. Turn on matchDescription to also search the description. |
locations | Keep jobs where any location contains one of the terms. |
departments | Keep jobs whose department or team contains one of the terms. |
remoteOnly | Keep remote jobs only. If the ATS gives no flag, a job counts as remote when its title or location says "remote". |
postedWithinDays | Keep jobs posted in the last N days. Jobs for which the ATS gives no date are kept. |
descriptionFormat | none, text (default), markdown or html. |
extractSalaryFromDescription | Experimental, off by default. Reads an explicit pay range from the description text when the ATS has no structured salary. Labelled salary.source: "description". |
fetchJobDetails | SmartRecruiters and Workday only: read one extra page per job. Only jobs that pass your filters are fetched. SmartRecruiters skips this request automatically when descriptionFormat is none. Workday needs it for dates and locations. |
onlyNewJobs, monitorName | Monitoring mode, see below. |
maxJobsPerCompany, maxTotalJobs | Cost control. maxTotalJobs: 0 means no limit. When the total is reached, the remaining inputs are marked skipped (limit reached) in RESOLUTION_REPORT and are not resolved or listed. |
proxyConfiguration | Optional. Not needed for normal use. |
Filter matching is whole-word and case-insensitive. AI matches "AI Engineer" but not "Retail"; US matches "Remote - US" but not "Austin"; a multi-word term such as data engineer matches as a phrase. Filters run before anything is written to the dataset.
Output
One dataset item per job, in the same shape for every ATS:
{"jobId": "8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3","source": "ashby","company": "Openai","companySlug": "openai","title": "Technical Program Manager, Compute Infrastructure","department": "Technical Program Management","team": "Technical Program Management","locations": ["San Francisco"],"location": "San Francisco","isRemote": false,"workplaceType": "hybrid","employmentType": "full-time","postedAt": "2026-11-05T02:20:02.702Z","updatedAt": null,"jobUrl": "https://jobs.ashbyhq.com/openai/8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3","applyUrl": "https://jobs.ashbyhq.com/openai/8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3/application","salary": { "min": 207000, "max": 285000, "currency": "USD", "interval": "year", "raw": "$207K - $285K", "source": "provider" },"salaryRanges": [{ "min": 207000, "max": 285000, "currency": "USD", "interval": "year", "raw": "$207K – $285K", "source": "provider" }],"description": "ABOUT THE TEAM\n\nThe Compute Infrastructure team runs the GPU fleet ...","scrapedAt": "2026-11-15T12:41:47.918Z"}
- Nothing is invented.
postedAt,updatedAt,workplaceType,employmentType,departmentandsalaryarenullwhen the ATS does not provide them. - Salary:
salaryis the first pay range the employer published;salaryRangeslists every published range exactly as given (for example one per Greenhouse pay zone or Ashby compensation tier). Ranges are never merged. Many postings have none, which is normal. - HTML descriptions are sanitised to a safe tag allowlist (no scripts, styles or event handlers; only
http,httpsandmailtolinks). Markdown output is built from the sanitised HTML. - Company names: Greenhouse, SmartRecruiters, Workable and Recruitee return the official name. Lever, Ashby and Workday do not, so for those
companyis derived from the board slug, such as "Openai" foropenai.companySlugis always exact.
Where dates come from
| ATS | postedAt | updatedAt |
|---|---|---|
| Greenhouse | first_published | updated_at |
| Lever | createdAt (when the posting was created in Lever) | not provided |
| Ashby | publishedAt | not provided |
| Workable | published_on, falling back to created_at | not provided |
| SmartRecruiters | releasedDate | not provided |
| Recruitee | published_at, falling back to created_at | updated_at |
| Workday | startDate from the job page (a calendar date, so the time part is 00:00:00Z); null if details are off | not provided |
Company summary (hiring signals)
Each run also saves COMPANY_SUMMARY to the default key-value store, one entry per company:
[{"company": "Stripe", "source": "greenhouse", "totalJobs": 785, "listingComplete": true,"matchingJobs": 785, "outputJobs": 25, "remoteJobs": 0,"departments": { "GEO Sales (EMEA) (Planning Group)": 58, "Solutions Architecture (Planning Group)": 43 },"locations": { "Dublin": 59, "Singapore": 56 },"newestPostedAt": "2026-11-13T21:14:55.000Z", "oldestPostedAt": "2023-11-01T17:50:50.000Z"},{"company": "Experian", "source": "smartrecruiters", "totalJobs": 389, "listingComplete": false,"matchingJobs": 100, "outputJobs": 25, "remoteJobs": 7,"departments": { "Technology": 25, "Product Management": 17 },"locations": { "Hyderabad, , India": 18 },"newestPostedAt": "2026-11-13T22:12:32.217Z", "oldestPostedAt": "2026-11-06T03:22:25.469Z"}]
Example from a run capped at 25 jobs per company. outputJobs is the number written to the dataset; totalJobs is what the board lists. listingComplete: false means the Actor stopped listing early, so matchingJobs, departments and locations describe only the part it read. This applies to SmartRecruiters and Workday, where the Actor reads the list page by page and stops at your per-company cap (when no filters are set), and to a Workday board that hit its roughly 2,000-result window or was searched by keyword (one search per keyword). Greenhouse, Lever, Ashby, Workable and Recruitee return the whole board in one response, so their summary is always complete.
Resolution report
RESOLUTION_REPORT lists every input with its status (resolved, partial, duplicate, skipped or failed), how it was detected (url, page or probe), the matched ATS and slug, job counts, alternatives for ambiguous names, and a clear reason for failures. The run's status message also names the inputs that could not be resolved. One bad input never fails the run.
Monitoring mode: only new jobs
Set onlyNewJobs: true and schedule the Actor (for example daily). The first run outputs all matching jobs and remembers them. Every later run outputs only jobs that were not seen before.
- Seen jobs are stored in a named key-value store
ats-monitor-<name>-<hash>in your account, so they survive between runs. Use a differentmonitorNameper watch list; names that differ only by case or punctuation get separate stores. - State is saved after every batch, so a run that stops or restarts does not lose what it already delivered.
- A job that disappears from a board is forgotten, so it counts as new if it is posted again. A board that comes back empty (or with less than half of the remembered jobs) never wipes the memory.
- Jobs skipped because of
maxJobsPerCompanyor a spending limit are not marked as seen and show up in the next run. - A run with 0 new jobs is a successful run, and its status message says so.
- Independently of monitoring mode, each run remembers what it already delivered, so if the platform restarts it mid-run (for example a server migration), jobs already delivered are not written or billed again, except, at most, the one batch being written at the moment of a hard crash. A write-ahead marker and a read of the dataset tail are used to recover even that batch where possible; if the dataset cannot be read, nothing is assumed delivered.
Pricing
$2.00 per 1,000 jobs ($0.002 per job) on Apify's Free and Bronze discount tiers; Silver $1.60 / 1,000; Gold $1.20 / 1,000.
You pay per job written to the dataset (pay-per-event). Apify's standard start charge also applies: $0.00005 per run for up to 1 GB of memory, plus $0.00005 for each additional GB (the default 512 MB run costs $0.00005). Apify Store discount tiers are based on your total monthly spend on Apify. Set a maximum cost per run in the Apify Console. The Actor stops cleanly when that limit is reached and keeps everything it has already delivered.
Integrations
Everything on Apify works with this Actor: run it from the API or the client libraries, put it on a schedule, trigger webhooks when a run finishes, and connect the dataset to Make, Zapier, n8n, Google Sheets and other integrations. In monitoring mode, a schedule plus a webhook gives you a "new jobs" notification pipeline.
Use it from AI agents (MCP)
AI agents and assistants can find and run this Actor through the Apify MCP server (search-actors, then call-actor), for example to answer "which companies opened new ML roles this week?". The input is plain JSON (companies, keywords, locations, onlyNewJobs), and the output is one flat record per job, so it is easy for an agent to use. The Actor runs with Apify's limited permissions.
Other Actors from Tessellware
- Bulk Domain Checker: availability, expiry date and registrar (RDAP/WHOIS), DNS records and an SPF/DMARC email-authentication grade for a list of domains. Pairs well with this Actor when you turn hiring companies into a clean lead list.
FAQ
Is it legal to scrape job postings? This Actor reads only job postings that employers publish openly: the public job-board APIs and careers-page JSON endpoints behind their careers pages. It does not log in, bypass access controls or read anything behind authentication. You are responsible for complying with the terms of the sites you use it on and with applicable law, including rules on the use of personal data and on how you reuse job postings. If in doubt, ask a lawyer about your specific use.
How do I find out which ATS a company uses? Often you do not have to: paste the careers page URL, or the company name or domain, and the Actor looks for the ATS itself. If you know the address of the job board (it usually contains greenhouse.io, lever.co, ashbyhq.com, workable.com, smartrecruiters.com, recruitee.com or myworkdayjobs.com), paste that for the most reliable result. Click "Apply" on a job and look at the address you land on.
A company was not found. What now? Check RESOLUTION_REPORT for the reason, then paste the exact job-board URL. Companies on other systems, or with a fully custom careers site, are not supported yet.
Are there limits? Workday returns at most about 2,000 results per search, so for very large employers add keywords; the Actor then runs one search per keyword. Unknown fields stay null.
Why do SmartRecruiters and Workday runs take longer? Their list pages carry no description or posting date, so the Actor reads one extra page per job, at most 4 at a time, and only for jobs that pass your filters. Use descriptionFormat: none (SmartRecruiters) or fetchJobDetails: false for a faster, lighter run.
What happens when some companies fail? The run still succeeds with the jobs it got and tells you which inputs failed. If no input can be resolved, the run fails with a clear message instead of finishing silently with an empty dataset.
Do I need proxies? No.