ATS Jobs Scraper: Greenhouse, Lever, Ashby & More avatar

ATS Jobs Scraper: Greenhouse, Lever, Ashby & More

Pricing

$1.00 / 1,000 results

Go to Apify Store
ATS Jobs Scraper: Greenhouse, Lever, Ashby & More

ATS Jobs Scraper: Greenhouse, Lever, Ashby & More

Fetch all open jobs from a company's career board on Greenhouse, Lever, Ashby, Workable, or SmartRecruiters using the official public job-board JSON APIs.

Pricing

$1.00 / 1,000 results

Rating

0.0

(0)

Developer

Zerah Data

Zerah Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

2 days ago

Last modified

Categories

Share

ATS Job Board Scraper - Greenhouse, Lever, Ashby, Workable, SmartRecruiters

Fetch every open job from a company's career page on Greenhouse, Lever, Ashby, Workable, or SmartRecruiters - using each platform's own public job-board JSON API, not HTML scraping and not a login.

Why this actor

  • Five live ATS platforms, not three - including Workable, which several comparable multi-ATS scrapers skip entirely.
  • Don't know which ATS a company uses? Give it a bare company slug and it auto-detects the platform by trying Greenhouse, Lever, Ashby, Workable, then SmartRecruiters in order, and uses whichever one responds.
  • Insight rows, not just raw rows. An opt-in per-company hiring summary (open roles, department/location mix, remote share, salary transparency, median pay) and opt-in new/closed-job change tracking between scheduled runs - without buying a second actor for either.
  • Deeper, deterministic normalization: rule-based seniority, a canonical department taxonomy, best-effort country/city, consolidated remote detection, and salary parsed into min/max/currency/period even when a platform only publishes free text. Same input always produces the same output - no probabilistic guessing.
  • A real self-check, every run. A RUN_REPORT record is written to the key-value store on every run: rows produced, errors by cause, warnings for companies that returned nothing, and a check that required fields are actually non-empty in the output - not just a success/failure flag.
  • No telemetry, no personal data. Nothing about your run or inputs is recorded anywhere outside your own Apify account, and no hiring-manager names/emails or other personal data are ever extracted.
  • Reads each platform's own public API directly - no login, no headless browser, so it can't break because a careers page redesign changed some HTML.
  • Priced at $1.00 per 1,000 results - cheaper than comparable multi-ATS job scrapers.

What it does

Give it one or more career-board URLs, company slugs, or bare company names and it will:

  • Auto-detect which of the five supported ATS platforms a URL belongs to, or (when you only give a name) probe all five and use the one that responds.
  • Call that platform's official public job-board API directly.
  • Paginate through all open jobs (SmartRecruiters) or fetch the full board in one call (Greenhouse, Lever, Ashby, Workable).
  • Normalize every job into one consistent row: title, department (raw and a canonical normalized category), team, location (plus best-effort parsed country/city), remote/workplace type, employment type, rule-based seniority, salary (structured where the board publishes it, parsed from free text otherwise) with min/max/currency/period, posted/updated dates, job URL, apply URL, and a plain-text and HTML job description.
  • Filter by keyword, location, remote-only, and recency before it counts against your result total.
  • Optionally track which jobs are new or have closed since your last run, and roll up a one-row-per-company hiring summary.
  • Report companies/URLs it couldn't resolve as clear error rows instead of failing the whole run.
  • Write a RUN_REPORT self-check to the key-value store on every run.

No recruiter names, emails, or other personal data are collected - only the job posting fields a company already publishes on its own public career page.

Use cases

  • Talent sourcing - pull open roles across many companies into one sheet.
  • Market research - track hiring trends, team growth, or new offices by company or industry.
  • Competitor intelligence - see what roles a competitor is actively hiring for, and get notified when roles open or close.
  • Job aggregation - feed a normalized job dataset into your own board, alert system, or newsletter.

How to use

  1. Add one or more career-board URLs to Career board URLs. Supported formats (the platform is detected automatically):
    • Greenhouse: https://boards.greenhouse.io/{company} or https://job-boards.greenhouse.io/{company}
    • Lever: https://jobs.lever.co/{company} (or https://jobs.eu.lever.co/{company})
    • Ashby: https://jobs.ashbyhq.com/{company}
    • Workable: https://apply.workable.com/{company}
    • SmartRecruiters: https://jobs.smartrecruiters.com/{company} (or https://careers.smartrecruiters.com/{company})
  2. Alternatively (or in addition):
    • Add entries to Companies (platform + slug), e.g. { "platform": "greenhouse", "slug": "airbnb" }.
    • Or, if you don't know which ATS a company uses, add its slug to Company names/slugs (auto-detect platform), e.g. "airbnb" - the actor tries Greenhouse, Lever, Ashby, Workable, then SmartRecruiters in order and uses the first one that responds.
  3. (Optional) Set Keyword filter, Location filter, and Remote only to narrow the results.
  4. (Optional) Set Max jobs per company (default 5) - set to 0 for unlimited. Note: change tracking and the per-company summary only see the jobs actually fetched, so set this to 0 for a complete picture.
  5. (Optional) Turn off Include job description for a lighter dataset.
  6. (Optional) Turn on Track new/closed jobs between runs and schedule the actor to get isNew/firstSeenAt on every job plus a change row for every job that disappeared since the last run.
  7. Run the actor.

Example input

{
"startUrls": [
{ "url": "https://boards.greenhouse.io/airbnb" },
{ "url": "https://jobs.lever.co/spotify" },
{ "url": "https://jobs.ashbyhq.com/ramp" },
{ "url": "https://apply.workable.com/huzzle" },
{ "url": "https://jobs.smartrecruiters.com/McDonaldsCorporation" }
],
"companyNames": ["stripe"],
"keyword": "",
"location": "",
"remoteOnly": false,
"maxJobsPerCompany": 5,
"includeDescription": true,
"postedWithinDays": 0,
"trackChanges": false,
"includeCompanySummary": true
}

The slugs above (airbnb on Greenhouse, spotify on Lever, ramp on Ashby, huzzle on Workable, McDonaldsCorporation on SmartRecruiters) were verified to return live job postings at the time this actor was built.

Output example

Job row

{
"type": "job",
"platform": "greenhouse",
"company": { "slug": "airbnb", "name": "Airbnb" },
"jobId": "7789554",
"title": "Associate Legal Counsel, Japan",
"department": "Legal",
"departmentNormalized": "Legal",
"team": null,
"location": "Tokyo, Japan",
"country": "Japan",
"city": "Tokyo",
"isRemote": false,
"seniority": "mid",
"employmentType": null,
"workplaceType": "Hybrid",
"salaryMin": null,
"salaryMax": null,
"salaryCurrency": null,
"salaryPeriod": null,
"salaryText": null,
"postedAt": "2026-04-13T08:49:05.000Z",
"updatedAt": "2026-09-30T00:00:54.000Z",
"jobUrl": "https://careers.airbnb.com/positions/7789554?gh_jid=7789554",
"applyUrl": "https://careers.airbnb.com/positions/7789554?gh_jid=7789554",
"descriptionText": "Airbnb was born in 2007 when two hosts welcomed three guests...",
"descriptionHtml": "<div class=\"content-intro\"><p>Airbnb was born in 2007...</p></div>",
"scrapedAt": "2026-10-26T13:46:18.281Z"
}

When Track new/closed jobs between runs is on, job rows also carry isNew (boolean) and firstSeenAt (ISO timestamp of the run that first saw this job ID).

Company hiring summary row

Included by default (disable with Include per-company hiring summary), charged as a single result row:

{
"type": "companySummary",
"platform": "greenhouse",
"company": { "slug": "airbnb", "name": "Airbnb" },
"generatedAt": "2026-10-26T13:46:18.281Z",
"openRoles": 184,
"byDepartment": { "Engineering": 61, "Sales": 24, "Legal": 3 },
"byLocation": { "United States": 102, "Japan": 6, "Remote": 18 },
"remoteShare": 0.38,
"newThisRun": 5,
"salaryTransparencyShare": 0.21,
"medianSalary": 142500
}

newThisRun and medianSalary are null when change tracking is off or no job in the run published a salary, respectively.

Change row (closed job)

Only produced when Track new/closed jobs between runs is on, for a job ID that was seen in an earlier run but is no longer on the board:

{
"type": "change",
"status": "closed",
"platform": "greenhouse",
"company": { "slug": "airbnb", "name": "Airbnb" },
"jobId": "7789554",
"firstSeenAt": "2026-10-20T09:00:00.000Z",
"closedDetectedAt": "2026-10-26T13:46:18.281Z"
}

Error row

If a URL or company can't be resolved, you get an error row instead of a silent gap in your data - and it is never charged:

{
"type": "error",
"input": "https://boards.greenhouse.io/this-company-does-not-exist",
"error": "COMPANY_NOT_FOUND",
"message": "Greenhouse board \"this-company-does-not-exist\" was not found",
"scrapedAt": "2026-10-26T13:46:19.677Z"
}

Monitoring: track new & closed jobs between runs

Turn on Track new/closed jobs between runs (trackChanges) and (optionally) run the actor on a schedule with the same Monitor name (monitorName). The actor persists which job IDs it has seen per company in a named key-value store:

  • Every job row gets isNew (true the first time that job ID is seen) and firstSeenAt.
  • Any job ID seen in an earlier run but missing from the current one gets a type: "change", status: "closed" row.
  • Set Output mode (outputMode) to newOnly to push only the jobs that are new since the last run (closed-job rows and the company summary are still produced either way) - useful for a lean "what changed" feed.
  • Use a different Monitor name to run multiple independent monitors (e.g. one per project) without their state colliding; leave it empty to use the shared default store.

The first run for a company only seeds the state (every job is isNew and no closed rows are produced, since there's nothing to compare against yet).

Normalization extras

Every job row is enriched the same way regardless of platform:

  • seniority - rule-based from the title: intern, junior, mid, senior, lead, manager, director, or exec. Deterministic: the same title always classifies the same way.
  • departmentNormalized - the platform's raw department mapped onto a small canonical set (Engineering, Product, Design, Data, Sales, Marketing, Customer Support, Operations, Finance, HR / People, Legal, Other).
  • country/city - best-effort split of the platform's free-text location into a normalized country name and city, when the text parses cleanly (e.g. "Tokyo, Japan"). Left null rather than guessed when it doesn't (e.g. "Remote", "Multiple locations").
  • isRemote - consolidates each platform's own remote flag with a keyword check across title/location/workplace type, so remote roles are flagged consistently even on platforms with no native remote field.
  • salaryMin/salaryMax/salaryCurrency/salaryPeriod - structured compensation where the platform publishes it (consistently available on Ashby; occasionally on Lever), otherwise parsed from the platform's free salary text when present. Never estimated: all four stay null when no platform data exists to parse.

Self-check: RUN_REPORT

Every run writes a RUN_REPORT record to the run's key-value store (see the Output tab / {{links.apiDefaultKeyValueStoreUrl}}/records/RUN_REPORT): how many inputs were processed, row counts by type, errors grouped by error code, warnings (e.g. a company whose board returned zero jobs), and a validation check confirming that required fields (title, jobUrl, platform, company.slug) are actually non-empty across the job rows produced. The run's status message mirrors a short version of the same summary.

Pricing

This actor uses pay-per-result pricing: $1.00 per 1,000 results, with no start fee. A result is one job row, one company hiring-summary row, or one closed-job change row. Error rows for unresolved URLs/companies are never charged. You can cap spending with the run's maximum cost setting, and the actor stops fetching as soon as that limit is reached.

FAQ

Is this legal? The actor only reads the public job-board JSON APIs that each platform exposes for its own "apply now" career pages (the same feeds that power the company's public job listings). It does not log in, does not scrape HTML, and does not access any private or authenticated data.

What happens if a URL isn't a recognized career board? You get an error row with error: "UNSUPPORTED_URL" for that entry instead of a failed run - every other URL in the same run is unaffected.

What other errors can show up? COMPANY_NOT_FOUND when the platform (or every platform tried during auto-detection) confirms the slug doesn't exist, and FETCH_FAILED for a network-level failure (timeout, DNS error, or an unexpected response shape).

How does auto-detection pick a platform? For each entry in Company names/slugs, the actor tries Greenhouse, then Lever, then Ashby, then Workable, then SmartRecruiters, and uses the first platform whose API responds without a "not found" error. If a company has boards on more than one platform, the one earliest in that order wins.

What are the limits? Only jobs a company has published to its public board are returned. Salary fields are only populated when a platform publishes structured compensation data or free salary text (consistently available on Ashby; occasionally on Lever; rarely exposed on Greenhouse, Workable, or SmartRecruiters). SmartRecruiters' public API returns an empty result for both "unknown company" and "company with zero open jobs right now" - the actor can't distinguish the two for that platform, so no error row is produced in that case. Closed-job detection and the per-company summary only see jobs actually fetched during the run, so set Max jobs per company to 0 for a complete picture.