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

\[💵 $2.00 / 1K] Scrape open jobs across 11 ATS platforms — Greenhouse, Lever, Ashby, Workday & more — in one unified schema. Auto-detects the ATS from a plain company domain. Monitoring mode returns only new jobs since your last run. For n8n, Make, Zapier & MCP. Official public APIs only.

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

## Pricing

from $1.30 / 1,000 jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

ATS jobs scraper for Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee, Personio, Teamtailor, BambooHR and Rippling — every posting pulled through the platform's own public API, normalised into one schema. Give it a board URL, or just a company website and it finds the board for you. Works with n8n, Make, Zapier, the Apify API and as an MCP tool for AI agents.

### What is the ATS Jobs Scraper?

Most job data on the internet is second-hand: aggregated, re-listed, days stale. This scraper goes to the source. Every posting comes from the applicant tracking system the company actually publishes to, which means you get the role when the source API exposes it, 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 proxies needed, no personal data — job postings only.

### What data can you extract from Greenhouse, Lever, Ashby and Workday?

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

#### Output fields

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

#### Example output JSON

```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-%E2%80%A6/application",
  "postedAt": "2026-04-27T20:13:45.158Z",
  "scrapedAt": "2026-08-25T19:14:15.176Z"
}
```

### Which ATS 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.

#### Why JazzHR is not supported

JazzHR's feed endpoint returns HTTP 200 with a "page not found" HTML body, and its sibling feed paths answer 410 Gone. A scraper that trusts the status code turns that into rows of nothing. It was tested across 180 tenants and removed rather than shipped as a silent data leak. If you need JazzHR, it has to be HTML parsing with real maintenance — open an issue and say so.

### How to scrape jobs from a company domain (no board URL needed)

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. A target list of 200 companies is a single run instead of 200 manual lookups.

#### Step-by-step

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 or Excel, or pull it from the API.

#### API target requirement

API callers must supply `boards` or `companyDomains` (or their supported aliases). The Console prefilled form is an example, not an implicit target list for an empty API request. A raw POST with no target is rejected with a clear input error and no job-result event; the small Actor-start fee may still apply. In the 2026-09-05 cloud check, the runtime did not substitute the saved `exampleRunInput` for a missing request body. Copy the complete JSON example when integrating.

#### Example input JSON

```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.

#### Input aliases from other ATS scrapers

Input copied from another ATS actor runs here unchanged: `companies`, `companyUrls`, `startUrls` and `queries` are accepted as aliases and merged into the board and domain lists. Switching costs you nothing.

### Monitoring mode: get only new jobs since your last run

The first successful run delivers a baseline of the matching postings within your scan limits. Later runs with **Only new jobs since last run** skip previously delivered identities. This is an unseen-record feed, not proof that a job was first published yesterday, changed, or closed.

Use a distinct **Seen-jobs store name** per watchlist and run only one task at a time against that store. Named key-value storage has no atomic cross-run lock; do not overlap schedules. Scan/output caps can limit coverage. If history cannot be read or persisted, or a previous delivery is unresolved, monitoring fails visibly instead of silently treating everything as new. Inspect `RUN_SUMMARY` and the saved pending-delivery record before retrying; do not delete history to bypass a reconciliation error.

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

**$2.00 per 1,000 jobs.** You are charged per job actually saved to your dataset, so filtered-out postings, duplicates and previously-seen postings in monitoring mode cost nothing. A run that scans 5,000 listings and matches 40 charges for 40. No matching result incurs a job event, but the configured Actor-start fee is $0.00005 per GB of memory, including on an empty run (one event at the 1 GB default).

#### Cost examples

Illustrative result-event charges at the base tier; not measured demand or recurring revenue. Add the small Actor-start fee described above. Actual available matches vary.

| What you run | Rows charged | Cost |
|---|---|---|
| 20 companies, everything open, ~40 roles each | 800 | $1.60 |
| Same 20 companies daily in monitoring mode, ~2% churn | ~16 per day | ~$0.03 per day, ~$1.00 per month |
| Engineering roles only across 200 companies, ~10 matches each | 2,000 | $4.00 |
| Full inventory of one large Workday board with descriptions | 1,500 | $3.00 |

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.

### Integrations: n8n, Make, Zapier, MCP and the API

#### Use with AI agents (MCP server)

Add the scraper as a tool in Claude Desktop, Cursor, or any MCP client through Apify's hosted MCP server:

```bash
npx -y @apify/actors-mcp-server --actors dataloft/ats-jobs-scraper
```

Or point your client at `https://mcp.apify.com` with your Apify token and enable this actor. An agent can then answer "which of these 30 companies opened a senior data role this week?" directly.

#### n8n, Make and Zapier

Use the official Apify node (n8n), the Apify app (Make) or the Apify integration (Zapier): pick **Run Actor**, choose `dataloft/ats-jobs-scraper`, paste the input JSON above, and read the dataset items in the next step. Combined with monitoring mode this gives you a "new roles at my watchlist → Slack / Sheets / CRM" workflow in three nodes.

#### JavaScript, Python and REST

```js
const { ApifyClient } = require('apify-client');
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('dataloft/ats-jobs-scraper').call({
  companyDomains: ['stripe.com', 'figma.com'],
  titleIncludes: ['engineer'],
  maxTotalJobs: 200,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

```python
from apify_client import ApifyClient
client = ApifyClient(token=os.environ["APIFY_TOKEN"])
run = client.actor("dataloft/ats-jobs-scraper").call(run_input={
    "companyDomains": ["stripe.com", "figma.com"],
    "maxTotalJobs": 200,
})
items = client.dataset(run["defaultDatasetId"]).list_items().items
```

REST: `POST https://api.apify.com/v2/acts/dataloft~ats-jobs-scraper/run-sync-get-dataset-items` with an `Authorization: Bearer <APIFY_TOKEN>` header with the input JSON as the body.

### What this Actor does and does not do

| Does | Does not |
|---|---|
| Pull live postings from 11 ATS platforms via their public APIs | Scrape LinkedIn, Indeed or any site that forbids it |
| Find a company's board from a plain domain | Log in, solve CAPTCHAs, or bypass anti-bot systems |
| Normalise 11 response shapes into one schema | Collect candidate, recruiter or any personal data |
| Parse structured salary where published | Guess salaries that are not published |
| Return only new postings in monitoring mode | Apply to jobs on your behalf |

### Limits and known caveats

- **Teamtailor** publishes an RSS feed capped at 100 postings per board by the platform.
- **SmartRecruiters** company IDs are case-sensitive; a wrong case returns an empty board, not an error.
- **Workday** reports the board total only on the first page; this scraper holds that total, but a Workday tenant with a fully custom site ID may need the full URL pasted in.
- **Domain detection** resolves most well-known companies (8 of 10 in testing). When it cannot, the run names the input it could not resolve instead of skipping it silently.
- **Descriptions** on Workday cost one extra request per posting — expect longer runs when **Include full job descriptions** is on for large boards.
- **Rate limits** are the platforms' own; the scraper retries with backoff and does not need a proxy for normal volumes.

### Example tasks

Ready-made configurations — competitor hiring watchlists, daily new-role alerts, remote engineering roles from Greenhouse and Lever — are on the **Example tasks** tab of this actor: https://apify.com/dataloft/ats-jobs-scraper/examples

### 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. With **Only new jobs since last run** on, each run returns only what appeared since the previous one; with it off, deduplication still gives you a clean current snapshot.

**Does monitoring mode cost extra?** No. Previously-seen postings are skipped before anything is charged, so a stable watchlist costs a few cents a day.

**Can I use it from n8n or as an MCP tool?** Yes — see the Integrations section above. No code is needed for n8n, Make or Zapier.

**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 by Dataloft Studio

- [Workday Jobs Scraper](https://apify.com/dataloft/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](https://apify.com/dataloft/remote-jobs-aggregator) — 7 remote-only job boards merged into one deduplicated feed.

### Changelog

- **0.3 — 2026-09-05**: monitoring fails visibly on history/delivery errors; budget-aware result charging and delivery reconciliation; published examples corrected and bounded.

- **0.2 — 2026-09-02**: monitoring mode (only new jobs since last run, named seen-jobs store); input aliases `companies` / `companyUrls` / `startUrls` / `queries`; memory capped at 1 GB (lower per-run platform cost); dataset schema and output schema aligned; changelog added.

- **0.1.4 — 2026-08-25**: fixed a concurrency race where **Max jobs in total** could be exceeded across parallel boards.

- **0.1.2 — 2026-08-25**: output schema added; first-run prefill capped so daily automated tests finish under 5 minutes.

- **0.1 — 2026-08-25**: initial release, 11 ATS platforms, domain-to-board detection.

### Support and 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.

## `onlyNewJobs` (type: `boolean`):

Turns this into a job monitor. Every posting delivered is remembered in a named key-value store, and later runs skip anything already on that list — skipped postings are not saved to the dataset and are not charged for. The first run builds the baseline and returns everything it finds; from then on you get only what appeared since. Note the limits above count postings scanned, not new ones found, so pair this with a generous (or unset) "Max jobs in total".

## `seenStoreName` (type: `string`):

Optional. Name of the key-value store that holds the seen-jobs list. Defaults to dataloft-seen-ats-jobs-scraper. Give each watchlist its own name when you run several monitors that should not share history.

## `companies` (type: `array`):

ALIAS — merged into "Job board URLs" / "Company websites" above. Accepts board URLs, platform:slug forms and bare company domains, so a task written for another job scraper runs here unchanged. Values that are neither a URL nor a domain are reported in the log and ignored.

## `companyUrls` (type: `array`):

ALIAS — same handling as "Companies": URLs become boards, bare domains go through ATS auto-detection.

## `startUrls` (type: `array`):

ALIAS — accepts the crawler-style list of {"url": "…"} objects as well as plain strings, e.g. \[{"url": "https://job-boards.greenhouse.io/airbnb"}]. Each URL is treated as a job board; a bare domain goes through ATS auto-detection. Remote URL lists (requestsFromUrl) are not supported.

## `queries` (type: `array`):

ALIAS — accepted for compatibility and handled exactly like "Companies". These are board or company identifiers, not search terms: to filter by keyword use "Keyword must appear in title or description" instead.

## 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,
  "onlyNewJobs": false,
  "seenStoreName": "",
  "companies": [],
  "companyUrls": [],
  "startUrls": [],
  "queries": []
}
```

# 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/nvwBn4hbHstTHgOyG/openapi.json
