Ats Job Scraper avatar

Ats Job Scraper

Pricing

from $3.00 / 1,000 results

Go to Apify Store
Ats Job Scraper

Ats Job Scraper

Pricing

from $3.00 / 1,000 results

Rating

0.0

(0)

Developer

Gorav Agarwal

Gorav Agarwal

Maintained by Community

Actor 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:

  1. Parse — if you paste an ATS URL, the tenant is read straight out of it.
  2. Probe and verify — slug candidates derived from the domain or company name are tested against each board API.
  3. Crawl — if neither works, the company's own site and careers pages (including careers. and jobs. 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.com
Datadog
https://jobs.ashbyhq.com/ramp
https://sanofi.wd3.myworkdayjobs.com/en-US/SanofiCareers
lever:spotify

Supported ATS platforms

ATSCoverageSalaryReq IDNotes
GreenhouseFull board, one requestFrom description textYesNo employment-type field on the public API
LeverFull board, one requestFrom description textNoPublishes workplace type and commitment
AshbyFull board, one requestStructuredNoRichest source: real compensation components and postal address
WorkdayPaginated, 20/requestDetail fetch onlyYesSee 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

FieldTypeDefaultDescription
companiesarray3 demo boardsCareers URLs, domains, company names, or ats:tenant tokens
maxItemsinteger500Total jobs across all boards
maxJobsPerBoardinteger0Per-company cap so one big employer can't dominate
includeDescriptionbooleanfalseFull description as plain text
titleKeywords / excludeTitleKeywordsarray[]Title filters
countriesarray[]ISO-3166 alpha-2 codes, e.g. ["GB","DE","IE"]
remoteOnlybooleanfalseRemote roles only (hybrid excluded)
withSalaryOnlybooleanfalseOnly postings with an extractable salary
postedWithinDaysinteger0Recency window
trackHistorybooleantruePersist first-seen / last-seen across runs
newJobsOnlybooleanfalseReturn 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.