# ATS Jobs Scraper — Greenhouse, Lever, Ashby, Workday & more (`dataloft/ats-jobs-scraper`) Actor

Scrape open roles from company career sites across 11 ATS platforms via official public APIs. Extract titles, locations, salaries, full descriptions and apply links. Auto-detects the ATS from a plain company website.

- **URL**: https://apify.com/dataloft/ats-jobs-scraper.md
- **Developed by:** [Dataloft Studio](https://apify.com/dataloft) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 job scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

Pull open roles straight from company career sites — Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee, Personio, Teamtailor, BambooHR and Rippling — through each platform's own public API. Give it a board URL, or just a company website and it finds the board for you.

### What this scraper does

Most job data on the internet is second-hand: aggregated, re-listed, days stale. This one goes to the source. Every posting comes from the applicant tracking system the company actually publishes to, which means you get the role the moment it is live, with the real requisition ID and the real apply link.

It reads **11 ATS platforms** through their official public endpoints. No login, no cookies, no browser automation, no personal data.

### What data you get

Every posting is normalised into one flat schema, so a Greenhouse role and a Workday role look identical to your code:

| Field | Notes |
|---|---|
| `title`, `company`, `companySlug` | |
| `atsPlatform` | which system it came from |
| `department`, `team`, `employmentType`, `seniority` | when the platform exposes them |
| `location`, `locations[]`, `country`, `isRemote` | `isRemote` is inferred when the ATS has no explicit flag |
| `compensation` | `{min, max, currency, interval}` — structured where published, parsed from text otherwise |
| `descriptionHtml`, `descriptionText` | full posting, both formats |
| `applyUrl`, `jobUrl`, `companyCareerUrl` | |
| `postedAt`, `updatedAt`, `scrapedAt` | ISO 8601 |
| `source`, `sourceUrl` | exact endpoint the row came from |

### Point it at a company, not a board

The part people usually have to do by hand is finding the board. You do not.

Put `stripe.com` in **Company websites** and the scraper reads the careers page, spots the ATS behind it, verifies the board actually serves jobs, and pulls everything. If no link is visible it falls back to probing each platform's public API with the obvious slug. In testing, plain domains resolved for 8 of 10 well-known companies — including one that had been acquired and moved onto its parent's board.

That makes a target list of 200 companies a single run instead of 200 manual lookups.

### How to use it

1. Paste board URLs into **Job board URLs**, company websites into **Company websites**, or both.
2. Add filters if you want a slice rather than everything — title, location, department, keyword, posted-after, remote-only.
3. Set **Max jobs in total** on your first run so you can see the shape of the output before scraping at scale.
4. Run it, then export as JSON, CSV, Excel or pull it from the API.

#### Input example

```json
{
  "boards": [
    "https://job-boards.greenhouse.io/airbnb",
    "https://jobs.ashbyhq.com/openai",
    "https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"
  ],
  "companyDomains": ["stripe.com", "figma.com"],
  "titleIncludes": ["engineer", "developer"],
  "locationIncludes": ["remote", "berlin"],
  "postedAfter": "2026-08-01",
  "maxTotalJobs": 500
}
```

Board URLs work in whatever form you copied them — `job-boards.greenhouse.io/acme`, `boards.greenhouse.io/embed/job_board?for=acme`, `jobs.lever.co/acme`, `acme.recruitee.com`, `acme.jobs.personio.de`, a Workday URL with or without the site ID. You can also write `greenhouse:acme` if you already know the slug.

#### Output example

```json
{
  "title": "Senior / Staff Fullstack Engineer",
  "company": "Linear",
  "atsPlatform": "ashby",
  "department": "Product",
  "team": "Engineering",
  "employmentType": "FullTime",
  "location": "Europe",
  "isRemote": true,
  "compensation": null,
  "descriptionText": "At Linear, we're building the product development system for teams…",
  "applyUrl": "https://jobs.ashbyhq.com/linear/d3bc1ced-…/application",
  "postedAt": "2026-04-27T20:13:45.158Z",
  "scrapedAt": "2026-08-25T19:14:15.176Z"
}
```

### Which platforms are supported

| Platform | Board looks like | Notes |
|---|---|---|
| Greenhouse | `job-boards.greenhouse.io/acme` | full description included |
| Lever | `jobs.lever.co/acme` | structured salary when published |
| Ashby | `jobs.ashbyhq.com/acme` | structured compensation tiers |
| Workday | `acme.wd5.myworkdayjobs.com/SiteName` | site ID optional — it is probed for you |
| SmartRecruiters | `careers.smartrecruiters.com/Acme` | company ID is case-sensitive |
| Workable | `apply.workable.com/acme` | |
| Recruitee | `acme.recruitee.com` | |
| Personio | `acme.jobs.personio.de` | `.de` and `.com` both handled |
| Teamtailor | `acme.teamtailor.com` | RSS feed, capped at 100 by the platform |
| BambooHR | `acme.bamboohr.com/careers` | |
| Rippling | `ats.rippling.com/acme/jobs` | |

A valid board with no open roles returns zero rows rather than an error — that is the platform's answer, not a failure.

### How much does it cost to scrape ATS job boards?

You are charged per job actually saved to your dataset, so filtered-out postings cost nothing. A run that scans 5,000 listings and matches 40 charges for 40.

Two ways to keep a first run cheap: set **Max jobs in total**, and turn **Include full job descriptions** off — descriptions are the slow part on Workday, which needs one extra request per posting.

### Why not just use the official APIs yourself?

You can — they are public, and that is exactly what this actor calls. What you would be rebuilding is the boring part: eleven different response shapes, three different pagination schemes, Workday reporting a total of `0` on every page after the first, Personio nesting a `<name>` tag inside another `<name>` tag, retries, rate limits, and one platform whose RSS feed was retired but still answers `200` with an HTML error page.

That last one is why **JazzHR is not in the supported list**. Its feed endpoint returns a success status with a "page not found" body, which a naive scraper happily parses into rows of nothing. It was tested across 180 tenants and removed rather than shipped as a silent data leak.

### Is it legal to scrape ATS job boards?

This actor only calls endpoints the ATS platforms publish for public consumption — the same JSON your browser fetches when you open a company's careers page, and in several cases documented integration APIs. It collects **job postings, which are company information, not personal data**: no candidate records, no recruiter contact details, no email harvesting.

You are still responsible for how you use the output, including under GDPR and each platform's terms. If you plan to redistribute the data commercially, read those terms first.

### Frequently asked questions

**Can I scrape a company if I do not know its ATS?** Yes — put the plain website in **Company websites**. If detection fails the run tells you which inputs it could not resolve instead of quietly skipping them.

**Why did a board return zero jobs?** Almost always because the company genuinely has no openings. Every supported platform answers `200` with an empty list for a valid-but-dormant board.

**Does it work with Workday if I only have the company name?** Yes. Workday site IDs like `NVIDIAExternalCareerSite` are probed from a list of naming patterns, so `nvidia.wd5.myworkdayjobs.com` alone is enough.

**How fresh is the data?** As fresh as the company's own careers page — you are reading the same source it renders from.

**Can I run this on a schedule?** Yes, use Apify Schedules. Deduplication is on by default, so a daily run over the same boards gives you a clean current snapshot.

**What happens when a filter matches nothing?** The log reports `scanned N → matched 0` for that board, so you can tell the difference between "no matches" and "something broke".

### Other job scrapers

- **Workday Jobs Scraper** — the same Workday support, but deeper: search terms passed to Workday's own engine, site-ID discovery from a plain company website.
- **Remote Jobs Aggregator** — 7 remote-only job boards merged into one deduplicated feed.

### Feedback

Issues and feature requests go in the Issues tab. Bug reports that include the input JSON get fixed fastest.

# Actor input Schema

## `boards` (type: `array`):

Career-site or ATS board URLs. Paste them exactly as you see them in a browser — the scraper works out which platform each one is. Examples: https://job-boards.greenhouse.io/airbnb, https://jobs.lever.co/spotify, https://jobs.ashbyhq.com/openai, https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite. You can also write platform:slug, e.g. greenhouse:airbnb.

## `companyDomains` (type: `array`):

Give a plain company website and the scraper finds which ATS runs its careers page, then pulls every open role. Use this when you have a list of target companies but not their board URLs.

## `platforms` (type: `array`):

Leave empty to allow every supported platform. Useful when a company runs more than one board and you only want one of them.

## `includeDescription` (type: `boolean`):

Fetches the complete description as both HTML and plain text. Turn this off for a much faster run when you only need titles, locations and links.

## `remoteOnly` (type: `boolean`):

Keeps only roles the ATS flags as remote, or whose location reads as remote.

## `titleIncludes` (type: `array`):

Case-insensitive. A role is kept if its title contains at least one of these, e.g. engineer, designer, product manager.

## `titleExcludes` (type: `array`):

Case-insensitive. Handy for dropping intern, contractor or director-level postings.

## `locationIncludes` (type: `array`):

Case-insensitive match against every location on the posting, e.g. berlin, united states, remote.

## `departmentIncludes` (type: `array`):

Case-insensitive match against the department or team the platform publishes.

## `keywords` (type: `array`):

Full-text filter, e.g. kubernetes, rust, series b.

## `postedAfter` (type: `string`):

ISO date such as 2026-08-01. Postings without a date are always kept.

## `maxJobsPerCompany` (type: `integer`):

0 means no limit.

## `maxTotalJobs` (type: `integer`):

Hard stop for the whole run. 0 means no limit. Use this to cap what you spend on a first test.

## `concurrency` (type: `integer`):

How many job boards to work through at once.

## `deduplicate` (type: `boolean`):

Drops the same role when it shows up on more than one board.

## `proxyConfiguration` (type: `object`):

Optional. These are public JSON APIs, so a proxy is rarely needed — turn it on only if you are running very large jobs from a single IP.

## `maxScanPerBoard` (type: `integer`):

Safety limit that applies only when filters are set. A board with 5,000 postings will not be crawled end-to-end just because you asked for 20 engineers — the scan stops here and the run log tells you it did. Raise it if you need to look deeper.

## Actor input object example

```json
{
  "boards": [
    "https://job-boards.greenhouse.io/airbnb",
    "https://jobs.ashbyhq.com/openai"
  ],
  "companyDomains": [],
  "platforms": [],
  "includeDescription": true,
  "remoteOnly": false,
  "titleIncludes": [],
  "titleExcludes": [],
  "locationIncludes": [],
  "departmentIncludes": [],
  "keywords": [],
  "maxJobsPerCompany": 0,
  "maxTotalJobs": 300,
  "concurrency": 5,
  "deduplicate": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "maxScanPerBoard": 1000
}
```

# Actor output Schema

## `jobs` (type: `string`):

All scraped job postings in the normalized schema (title, company, location, isRemote, compensation, description, applyUrl, postedAt and more).

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "boards": [
        "https://job-boards.greenhouse.io/airbnb",
        "https://jobs.ashbyhq.com/openai"
    ],
    "companyDomains": [],
    "maxTotalJobs": 300
};

// Run the Actor and wait for it to finish
const run = await client.actor("dataloft/ats-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "boards": [
        "https://job-boards.greenhouse.io/airbnb",
        "https://jobs.ashbyhq.com/openai",
    ],
    "companyDomains": [],
    "maxTotalJobs": 300,
}

# Run the Actor and wait for it to finish
run = client.actor("dataloft/ats-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "boards": [
    "https://job-boards.greenhouse.io/airbnb",
    "https://jobs.ashbyhq.com/openai"
  ],
  "companyDomains": [],
  "maxTotalJobs": 300
}' |
apify call dataloft/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dataloft/ats-jobs-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/rvFpDBdCc0Gg2HYYF/builds/DQF7eIvv5Ncqf6dKs/openapi.json
