Ats Job Scraper
Pricing
from $3.00 / 1,000 results
Ats Job Scraper
Pricing
from $3.00 / 1,000 results
Rating
0.0
(0)
Developer
Gorav Agarwal
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
20 hours ago
Last modified
Categories
Share
ATS Job Scraper — Greenhouse, Lever, Ashby & Workday
Scrape public job postings straight from company career pages — the applicant tracking system the employer actually posts into — and get one clean, normalised schema across every board. Give it a company domain and it works out which ATS that company uses on its own.
No API key. No login. No cookies. In testing, 2,580 jobs from 18 companies in 17 seconds, with country resolved on 99% of rows.
Why scrape the ATS instead of a job aggregator
Job boards and aggregators are a lossy, delayed copy. The ATS is the source: postings appear there first, the data is structured, and nothing has been de-duplicated, re-worded or dropped along the way. Scraping Greenhouse, Lever, Ashby and Workday directly gives you the employer's own record of the role — including requisition IDs and internal department names that aggregators strip out.
The catch has always been that every ATS models a job differently. This Actor does that normalisation for you.
Auto-detection: you don't need to know the ATS
Most ATS scrapers make you supply the board token yourself — you have to already know that Monzo is greenhouse:monzo and Ramp is ashby:ramp. This one resolves it, using three strategies in ascending order of cost:
- Parse — if you paste an ATS URL, the tenant is read straight out of it.
- Probe and verify — slug candidates derived from the domain or company name are tested against each board API.
- Crawl — if neither works, the company's own site and careers pages (including
careers.andjobs.subdomains) are searched for an embedded board link.
Nothing is ever reported on the strength of a pattern match alone. Every candidate is confirmed by calling the board API and requiring a non-empty job list, so a guess that 404s or returns an empty board is discarded. In a 12-company benchmark, auto-detection resolved 10 — in 5.9 seconds.
You can mix input styles freely in one run:
monzo.comDatadoghttps://jobs.ashbyhq.com/ramphttps://sanofi.wd3.myworkdayjobs.com/en-US/SanofiCareerslever:spotify
Supported ATS platforms
| ATS | Coverage | Salary | Req ID | Notes |
|---|---|---|---|---|
| Greenhouse | Full board, one request | From description text | Yes | No employment-type field on the public API |
| Lever | Full board, one request | From description text | No | Publishes workplace type and commitment |
| Ashby | Full board, one request | Structured | No | Richest source: real compensation components and postal address |
| Workday | Paginated, 20/request | Detail fetch only | Yes | See the honest note below |
An honest note on Workday
Workday works, and it is reliable when the tenant is known — 4/4 real careers URLs scraped correctly in testing. What is not reliable is guessing a Workday tenant from a company name. Unilever's site is Unilever_Experienced_Professionals, not Unilever_Careers; site names follow no dependable convention.
So: pass the Workday careers URL directly, or let the page crawl find it (it discovered Unilever's real tenant unaided). Blind tenant probing exists behind the enableWorkdayDiscovery flag and is off by default because it costs ~24 requests per company and usually fails.
Two further Workday quirks this Actor handles that naive scrapers do not: the CXS endpoint silently returns an empty array for any page size above 20 (it does not error — it just looks like an empty board), and its list response omits country and description. Enable workdayJobDetails to fill those in; in testing that took country resolution from 5/15 to 15/15.
Input
| Field | Type | Default | Description |
|---|---|---|---|
companies | array | 3 demo boards | Careers URLs, domains, company names, or ats:tenant tokens |
maxItems | integer | 500 | Total jobs across all boards |
maxJobsPerBoard | integer | 0 | Per-company cap so one big employer can't dominate |
includeDescription | boolean | false | Full description as plain text |
titleKeywords / excludeTitleKeywords | array | [] | Title filters |
countries | array | [] | ISO-3166 alpha-2 codes, e.g. ["GB","DE","IE"] |
remoteOnly | boolean | false | Remote roles only (hybrid excluded) |
withSalaryOnly | boolean | false | Only postings with an extractable salary |
postedWithinDays | integer | 0 | Recency window |
trackHistory | boolean | true | Persist first-seen / last-seen across runs |
newJobsOnly | boolean | false | Return only postings never seen before |
Example input
{"companies": ["monzo.com", "wise.com", "n26.com", "gocardless.com", "pleo.io"],"countries": ["GB", "DE", "IE", "NL", "PL"],"titleKeywords": ["engineer", "data"],"postedWithinDays": 14,"maxItems": 1000}
Output example
One dataset item per posting. Real output:
{"position": 1,"jobKey": "greenhouse:monzo:7115379","company": "Monzo","companyDomain": "monzo.com","ats": "greenhouse","atsTenant": "monzo","jobId": "7115379","requisitionId": "2311","title": "Android Engineer","department": "Engineering","team": null,"employmentType": null,"location": {"raw": "Cardiff, London or Remote (UK)","city": "Cardiff","region": null,"country": "United Kingdom","countryCode": "GB","isRemote": true,"workplaceType": "Remote"},"additionalLocations": [],"isRemote": true,"countryCode": "GB","salary": {"min": 85000.0,"max": 110000.0,"currency": "GBP","period": "YEAR","raw": "£85,000 - £110,000","source": "text"},"hasSalary": true,"url": "https://job-boards.greenhouse.io/monzo/jobs/7115379","applyUrl": "https://job-boards.greenhouse.io/monzo/jobs/7115379","postedAt": "2026-03-09T13:32:35Z","updatedAt": "2026-09-18T11:54:21Z","firstSeenAt": "2026-09-14T06:02:11Z","lastSeenAt": "2026-09-21T06:00:04Z","isNew": false,"scrapedAt": "2026-09-21T06:00:04Z"}
Parsed location and salary
location is parsed, not just passed through: "Cardiff, London or Remote (UK)" becomes a city, a country code and a remote flag. Country was resolved on 99% of 2,580 jobs in the benchmark sweep.
salary comes from structured compensation fields where the ATS publishes them (Ashby) and from careful text extraction otherwise. The text parser only fires when the surrounding words actually concern pay, so "this role runs 2024 - 2025" and "we hired 20 - 30 engineers" are correctly ignored. Across the sweep, 914 of 2,580 jobs carried a salary — 814 structured, 100 from text. Most employers simply do not publish pay, and this Actor reports that honestly rather than inventing numbers.
First-seen / last-seen tracking
With trackHistory on, the Actor remembers every job key in your own key-value store. Each posting then carries firstSeenAt, lastSeenAt and isNew — so isNew means absent from the previous run, not merely "the ATS says it was posted recently". Schedule the Actor daily and set newJobsOnly to get a clean feed of genuinely new roles. A run where nothing is new finishes successfully and says so.
Pricing
Pay per result — you are charged per job posting written to the dataset, at $10 per 1,000 jobs, and nothing else. Jobs are de-duplicated across boards before pushing, so a company found on two ATSs is never billed twice.
Use cases
Recruitment intelligence and market mapping. Track who your competitors are hiring, in which countries, and at what level. Department and req ID come straight from the employer's own system.
Job board and aggregator content. Build a vertical job board with first-party postings, and use newJobsOnly to ingest only deltas.
Compensation benchmarking. Ashby boards publish structured salary bands; combined with the text parser this yields a real, sourced pay dataset with currency and period normalised.
Hiring-signal research for sales and investors. A company that opens twelve sales roles in Germany is telling you something. Scheduled runs plus isNew turn that into a time series.
Talent sourcing ops. Feed normalised, deduplicated postings into an ATS, CRM or spreadsheet without writing a parser per board.
What this Actor does not do
No personal data. Job postings sometimes embed a recruiter's name, direct email or mobile number. All of it is stripped from every field before the record is written — emails, phone numbers, social profile links and labelled recruiter lines — whether or not descriptions are enabled. A scan over 3.3 MB of real descriptions from 400 postings found zero leaks. The output is job data, not contact data, and there is no option to turn that off.
No candidate data, no applications. This reads public postings only. It never authenticates, never submits anything, and never touches applicant information.
FAQ & Support
Do I need an API key for Greenhouse, Lever or Ashby? No. All three expose public, unauthenticated job board APIs, and this Actor uses only those. Workday's CXS endpoint is likewise public.
How do I scrape Greenhouse jobs for a specific company? Pass the domain (monzo.com), the token (greenhouse:monzo), or the board URL. All three work.
Why did my company return nothing? Most likely it does not use one of the four supported systems — plenty of large employers run SmartRecruiters, SuccessFactors, Taleo or a bespoke site. The run log names every seed that failed to resolve. For Workday specifically, pass the careers URL rather than the company name.
Why is employmentType empty for Greenhouse jobs? Greenhouse's public board API does not expose it. Rather than guess from the title, the field is left null.
Why do so few jobs have a salary? Because few employers publish one. Ashby boards are the exception. Use withSalaryOnly to keep just the ones that do, but expect it to remove most rows.
Can it scrape every company on an ATS? No — these APIs are per-tenant, so you supply the companies. There is no global index to enumerate, and this Actor does not pretend otherwise.
Does it need a proxy? No. These APIs are public and answer datacenter IPs. Apify Proxy is available in the input for very large runs and is off by default.
Is this legal? It reads public job postings from public endpoints — no login, no personal data, no candidate information. You are responsible for how you use the output.
Something looks wrong. Open an issue on the Issues tab with your input JSON and the run ID.