# ATS Jobs Scraper: Greenhouse, Lever, Ashby & more (`tomstudio/ats-jobs-scraper`) Actor

Scrape public jobs from Greenhouse, Lever, Ashby, Workable, Personio, Recruitee & Teamtailor in one schema. Auto-detects the ATS. Only-new-jobs mode.

- **URL**: https://apify.com/tomstudio/ats-jobs-scraper.md
- **Developed by:** [Tom Studio](https://apify.com/tomstudio) (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

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workable, Personio, Recruitee & Teamtailor jobs API

Get **every open job posting straight from company career pages** - in one clean, unified format - from the 7 applicant tracking systems (ATS) most used by tech companies, startups and European scale-ups: **Greenhouse, Lever, Ashby, Workable, Personio, Recruitee and Teamtailor**.

Paste a careers page URL (e.g. `https://www.notion.com/careers`) and the Actor **auto-detects the ATS** behind it. Filter by keyword, location, department, remote and posting date, and on scheduled runs get **only the jobs that are new since the last run** - so you pay only for fresh data.

### What can this ATS jobs scraper do?

- 🧭 **Auto-detect the ATS** from any company careers page, or use board URLs / `ats:slug` shorthands
- 🧩 **One schema for 7 ATS** - title, department, team, location(s), country, remote/hybrid/onsite, employment type, posting date, apply URL, salary range (when published), description as text and/or HTML
- 🇪🇺 **European ATS included** - Personio, Recruitee, Teamtailor and Workable, plus EU-hosted Greenhouse and Lever boards
- 🔎 **Filters before you pay** - keywords (title or description), excluded words, locations, departments, remote only, posted within N days. Filtered-out jobs are never stored
- 🔔 **"Only new jobs" monitoring** - remembers job IDs between runs; perfect for daily schedules, Slack/email alerts, n8n / Make / Zapier flows
- 🛡️ **Polite and reliable** - uses each ATS's public job-board feed (the same data the company publishes for candidates), checks robots.txt, one request at a time per host, retries with backoff, one failing company never breaks the run
- 🤖 **AI-agent ready** - flat JSON, stable `jobId` and `contentHash` fields for de-duplication and change detection

### Who is it for?

- **Job boards & aggregators** that need fresh postings from hundreds of company career sites
- **Recruiters & talent-intelligence teams** tracking who is hiring, for which roles and where
- **Sales teams** using hiring as a buying signal ("companies hiring data engineers in Germany")
- **Investors & analysts** monitoring headcount growth of portfolio or competitor companies
- **Job seekers & communities** building niche boards (remote, climate, AI, DACH...)

### How to scrape jobs from Greenhouse, Lever, Ashby and more

1. Click **Try for free**.
2. In **Company career pages or ATS boards**, add one line per company, any of:
   - a board URL: `https://boards.greenhouse.io/airbnb`, `https://jobs.lever.co/palantir`, `https://jobs.ashbyhq.com/openai`, `https://apply.workable.com/huggingface`, `https://bunq.recruitee.com`, `https://personio.jobs.personio.de`, `https://career.teamtailor.com`
   - a shorthand: `greenhouse:airbnb`, `lever:palantir`, `ashby:openai`, `workable:huggingface`, `recruitee:bunq`, `personio:personio`, `teamtailor:career`, `greenhouse-eu:<slug>`, `lever-eu:<slug>`
   - any careers page: `https://www.notion.com/careers`, `https://linear.app/careers`
3. Optionally add filters (keywords, locations, remote only, posted within days).
4. Click **Start** and download the results as JSON, CSV, Excel or via API.

#### Example input

```json
{
  "companies": [
    "greenhouse:airbnb",
    "https://jobs.lever.co/palantir",
    "https://www.notion.com/careers",
    "https://bunq.recruitee.com",
    "https://personio.jobs.personio.de"
  ],
  "keywords": ["engineer", "developer"],
  "locations": ["London", "Amsterdam", "Munich", "Remote"],
  "postedWithinDays": 30,
  "onlyNewJobs": true,
  "stateKey": "eu-engineering-monitor",
  "descriptionFormat": "text"
}
```

#### Example output

```json
{
  "jobId": "ashby:linear:d3bc1ced-3ce4-4086-a050-555055dbb1ff",
  "ats": "ashby",
  "companySlug": "Linear",
  "companyName": "Linear",
  "title": "Senior / Staff Fullstack Engineer",
  "department": "Product",
  "team": "Engineering",
  "location": "Europe",
  "locations": ["Europe"],
  "country": "European Union",
  "workplaceType": "remote",
  "isRemote": true,
  "employmentType": "FullTime",
  "postedAt": "2021-04-27T20:13:45.158000Z",
  "url": "https://jobs.ashbyhq.com/Linear/d3bc1ced-3ce4-4086-a050-555055dbb1ff",
  "applyUrl": "https://jobs.ashbyhq.com/Linear/d3bc1ced-3ce4-4086-a050-555055dbb1ff/application",
  "salaryMin": null,
  "salaryMax": null,
  "salaryCurrency": null,
  "salaryText": null,
  "descriptionText": "At Linear, we're building the product development system for teams and agents...",
  "contentHash": "75b14f030f31a8f0",
  "scrapedAt": "2026-10-23T03:44:27Z"
}
```

Ashby boards that publish compensation also fill `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval` and `salaryText` (e.g. `"$250K - $445K"`). Lever and Recruitee salary ranges are included when the company publishes them.

A run summary (jobs found / saved per company, errors, request count) is saved as `OUTPUT` in the run's key-value store.

### Output fields

| Field | Description |
|---|---|
| `jobId` | Stable unique ID `ats:company:postingId` - use it to de-duplicate |
| `ats`, `companySlug`, `companyName` | Where the job comes from |
| `title`, `department`, `team`, `seniority` | Role details (`seniority` from Personio) |
| `location`, `locations`, `country` | Primary location, all locations, country (when provided) |
| `workplaceType`, `isRemote` | `remote` / `hybrid` / `onsite` and a boolean remote flag |
| `employmentType` | Full-time, part-time, contract... as given by the ATS |
| `postedAt`, `updatedAt` | ISO-8601 UTC |
| `url`, `applyUrl` | Public job page and application link |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval`, `salaryText` | Pay range when the company publishes it |
| `descriptionText`, `descriptionHtml` | Full job description |
| `contentHash` | Changes when title, location or description change |

### How much does it cost to scrape ATS jobs?

During the launch period this Actor is **free to use** - you only pay standard Apify platform usage, which is tiny because it calls public JSON/XML feeds instead of rendering pages. In our test on the Apify platform, a run that saved **659 jobs from 5 companies cost about $0.004** in platform usage at 256 MB memory (roughly **$0.006 per 1,000 jobs**); a single small board costs well under $0.001. Your actual cost depends on your plan and inputs.

Jobs removed by your filters or already returned by a previous "only new jobs" run are never written to the dataset, so they cost nothing.

### Tips

- **JavaScript-only careers pages**: if auto-detection fails, open the page, click any job and copy the board URL (e.g. `jobs.lever.co/...`) into the input.
- **EU-hosted boards**: use `greenhouse-eu:slug` / `lever-eu:slug` or paste the `.eu.` board URL.
- **Personio language**: set *Personio feed language* (e.g. `de`) to get German descriptions.
- **Several monitors**: give each schedule its own *Monitoring state key*.

### Integrations and API

Run it from the Apify API, the Python/JS clients, or connect it to **Make, Zapier, n8n, Google Sheets, Slack, Airbyte** and webhooks. AI agents can call it through the **Apify MCP server**.

### Is it legal to scrape job postings?

This Actor only reads **public job postings** that companies publish through their ATS's public job-board feeds for candidates and job aggregators. It does not log in, bypass paywalls or CAPTCHAs, and it does not collect candidate or employee data (recruiter contact fields such as reply-to mailboxes are dropped). It respects robots.txt and paces requests. You are responsible for how you use the data and for complying with each company's terms and with laws such as GDPR. When in doubt, consult a lawyer. Read more in Apify's blog post [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/)

### Not supported (yet)

Workday, SmartRecruiters, iCIMS, SuccessFactors, BambooHR and LinkedIn/Indeed are not supported in this version. Missing an ATS? Open an issue in the **Issues** tab - requests with the most votes get built first.

### Support

Found a bug or a board that doesn't parse? Open an issue with the input you used - we aim to fix broken boards quickly.

# Actor input Schema

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

One entry per company. Accepts: (1) an ATS board URL, e.g. https://boards.greenhouse.io/airbnb, https://jobs.lever.co/palantir, https://jobs.ashbyhq.com/openai, https://apply.workable.com/huggingface, https://bunq.recruitee.com, https://personio.jobs.personio.de, https://career.teamtailor.com; (2) the shorthand ats:slug, e.g. greenhouse:airbnb, lever:palantir, ashby:openai, greenhouse-eu:slug, lever-eu:slug; (3) any company careers page URL (e.g. https://openai.com/careers) - the Actor fetches it once and auto-detects the ATS.

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

Keep only jobs whose title contains at least one of these words (case-insensitive). Leave empty for all jobs.

## `searchInDescription` (type: `boolean`):

If enabled, keywords are matched against the title and the full job description.

## `excludeKeywords` (type: `array`):

Drop jobs whose title contains any of these words, e.g. intern, senior.

## `locations` (type: `array`):

Keep jobs whose location, office list or country contains any of these strings, e.g. Zurich, Germany, London, Remote.

## `departments` (type: `array`):

Keep jobs whose department or team contains any of these strings, e.g. Engineering, Sales.

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

Keep only jobs flagged remote by the ATS or with 'remote' in the location/title.

## `postedWithinDays` (type: `integer`):

Keep only jobs first published in the last N days. Leave empty for no date filter.

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

Remember job IDs between runs and return only jobs not returned before. Ideal for daily schedules and alerts - you only pay for new jobs.

## `stateKey` (type: `string`):

Use different keys for independent monitors (e.g. one per client). Only used with 'Only new jobs'.

## `descriptionFormat` (type: `string`):

Format of the job description field(s).

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

0 = no limit.

## `maxJobs` (type: `integer`):

Hard cap on saved jobs for the whole run (0 = no limit). You can also cap spend with the run's maximum cost setting.

## `personioLanguage` (type: `string`):

Optional language code for Personio feeds, e.g. en, de, fr. Leave empty for the company's default.

## `maxConcurrency` (type: `integer`):

How many company boards are fetched at the same time. Requests to the same host are always serialised.

## `requestDelaySecs` (type: `number`):

Politeness delay per host. Lever asks for 1 s (robots.txt Crawl-delay).

## Actor input object example

```json
{
  "companies": [
    "greenhouse:airbnb",
    "https://jobs.ashbyhq.com/openai",
    "https://apply.workable.com/huggingface"
  ],
  "searchInDescription": false,
  "remoteOnly": false,
  "onlyNewJobs": false,
  "stateKey": "default",
  "descriptionFormat": "text",
  "maxJobsPerCompany": 0,
  "maxJobs": 0,
  "maxConcurrency": 4,
  "requestDelaySecs": 1
}
```

# Actor output Schema

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

All saved job postings (overview table view). Fields are described in the dataset schema.

## `salaries` (type: `string`):

The same job postings shown with salary range fields (salaries view).

## `runSummary` (type: `string`):

JSON summary: counts of fetched, saved, filtered, already-seen jobs and per-board status/errors.

# 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 = {
    "companies": [
        "greenhouse:airbnb",
        "https://jobs.ashbyhq.com/openai",
        "https://apply.workable.com/huggingface"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("tomstudio/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 = { "companies": [
        "greenhouse:airbnb",
        "https://jobs.ashbyhq.com/openai",
        "https://apply.workable.com/huggingface",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("tomstudio/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 '{
  "companies": [
    "greenhouse:airbnb",
    "https://jobs.ashbyhq.com/openai",
    "https://apply.workable.com/huggingface"
  ]
}' |
apify call tomstudio/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,tomstudio/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/JfrPfL45gapCWNz55/builds/W77arLyfNr91RPvN1/openapi.json
