# ATS Jobs Scraper – Greenhouse, Lever, Ashby & Workable (`phantom-x64/ats-jobs-scraper`) Actor

Extract every open job from company career pages on Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee and Personio: title, location, remote flag, salary, description and apply link. ATS auto-detected from a URL, domain or name. Monitor mode charges only new jobs. $1 per 1,000 jobs.

- **URL**: https://apify.com/phantom-x64/ats-jobs-scraper.md
- **Developed by:** [Phantom Data](https://apify.com/phantom-x64) (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 $1.00 / 1,000 job postings

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?

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, SmartRecruiters, Recruitee & Personio jobs API

**One clean job postings feed from 7 applicant tracking systems.** Paste a list of companies (careers page URLs, website domains or just names), and get every open job as normalized JSON, CSV or Excel: title, department, team, location, remote flag, employment type, **salary when published**, full description, apply link and publication date.

- **7 ATSs, one schema.** Greenhouse scraper, Lever jobs API, Ashby jobs, Workable, SmartRecruiters, Recruitee and Personio, with identical fields for all of them.
- **Auto-detection.** Give `stripe.com`, `Notion` or `https://jobs.lever.co/spotify`: the actor finds the ATS and the board ID for you.
- **Filters built in.** Keywords, excluded words, locations, remote / hybrid, departments, "posted in the last N days", max jobs.
- **Monitoring mode.** Schedule it daily with *Only new jobs* and you only receive (and pay for) jobs you haven't seen before.
- **Cheap and predictable.** **$1 per 1,000 jobs + $0.001 per company checked.** No start fee, no subscription.
- **Fast and light.** It reads the official public job-board endpoints that power each company's careers page: no browser, no proxies, no login. 10 companies take about 20-40 seconds.

### What can you use it for?

| Who | What they do with it |
|---|---|
| **Recruiters & sourcers** | Watch the career pages of target companies and get every new engineering / sales opening the day it is posted. |
| **Job boards & niche job sites** | Fill a remote-only, climate, AI or country-specific job board with fresh, structured jobs (use *Include description (HTML)* to re-publish). |
| **Sales intelligence (hiring signals)** | A company opening 5 data-engineering roles is buying data tools. Feed hiring signals into your CRM, Clay or n8n workflows and time your outreach. |
| **Investors & market researchers** | Track headcount plans, new offices, salary ranges and team growth across a portfolio or a competitor set. |
| **Academic researchers** | Build labour-market datasets (skills, salaries, remote share) from primary employer sources. |
| **Job seekers & career coaches** | Get a daily list of new roles that match your keywords at your 50 dream companies. |

### Supported ATSs

| ATS | Example careers URL | Salary | Notes |
|---|---|---|---|
| Greenhouse | `boards.greenhouse.io/airbnb`, `job-boards.greenhouse.io/discord` | Yes (pay-transparency block) | EU boards (`.eu.greenhouse.io`) supported |
| Lever | `jobs.lever.co/spotify` | Yes (when published) | EU boards (`jobs.eu.lever.co`) supported |
| Ashby | `jobs.ashbyhq.com/linear` | Yes (when published) | |
| Workable | `apply.workable.com/huggingface` | — | |
| SmartRecruiters | `jobs.smartrecruiters.com/Wise` | Yes (when published) | Large boards (5,000+ jobs) paginated automatically |
| Recruitee | `bunq.recruitee.com` | Yes (when published) | |
| Personio | `1komma5grad.jobs.personio.de` | — | `.de` and `.com` |

### Quick start (under 1 minute)

1. Click **Try for free** and paste the companies you want to follow (careers page URLs, domains like `stripe.com`, plain names, or `ats:slug` pairs such as `greenhouse:stripe`).
2. Optionally add keywords, locations, remote or date filters. The pre-filled input (4 companies, up to 100 jobs) is a safe first run.
3. Click **Start**. A first run finishes in about 30 seconds. Download the jobs as JSON, CSV or Excel, or read them through the Apify API.

### Input

You only need **Companies**. Mix any of these formats:

```json
{
  "companies": [
    "https://boards.greenhouse.io/discord",
    "https://jobs.lever.co/spotify",
    "linear.app",
    "stripe.com",
    "Notion",
    "smartrecruiters:Wise",
    "https://1komma5grad.jobs.personio.de/"
  ],
  "keywords": ["engineer", "data"],
  "excludeKeywords": ["intern"],
  "locations": ["London", "Germany", "Remote"],
  "remote": "any",
  "postedWithinDays": 14,
  "maxJobs": 1000
}
```

| Format | Example | How it is resolved |
|---|---|---|
| Careers / ATS URL | `https://jobs.ashbyhq.com/linear` | Read directly (most reliable). |
| Website or domain | `stripe.com` | The actor opens the website, follows the "Careers / Jobs" links and finds the ATS behind them. If that fails it tries the domain name on every ATS. |
| Company name | `Notion` | The name is tried on every ATS and the board with the most open jobs wins. Check `detected_via: "name-guess"` results. |
| Explicit | `greenhouse:stripe`, `lever:palantir`, `ashby:openai`, `workable:huggingface`, `smartrecruiters:Wise`, `recruitee:bunq`, `personio:1komma5grad` | No detection needed. |

Main options:

| Field | What it does |
|---|---|
| `keywords` / `excludeKeywords` | Whole-word, case-insensitive match on title, department and team (`searchInDescription` adds the description). |
| `locations` | Case-insensitive "contains" match on the location ("London", "Spain", "EMEA"…). Adding "Remote" also keeps every remote job. |
| `remote` | `any`, `remote`, `remoteOrHybrid` or `notRemote`. |
| `departments` | "Contains" match on department or team ("Engineering", "Sales"…). |
| `postedWithinDays` / `postedAfter` | Only jobs published recently (by the ATS publication date). |
| `maxJobsPerCompany` / `maxJobs` | Cost control. Newest jobs are kept first. |
| `includeDescription` / `includeDescriptionHtml` | Plain-text description (on by default) and original HTML (off by default). |
| `onlyNew` + `monitorName` | Monitoring mode (see below). |

### Output

Every job has exactly the same fields, whatever the ATS:

```json
{
  "company": "Wise",
  "ats": "smartrecruiters",
  "job_id": "744000159127419",
  "title": "Backend Engineer - Send Core",
  "department": null,
  "team": "Engineering",
  "location": "London, England, United Kingdom",
  "remote": false,
  "workplace_type": "hybrid",
  "employment_type": "full-time",
  "salary": { "min": 68000, "max": 87500, "currency": "GBP", "period": "year", "text": null },
  "description_text": "Wise is a global technology company, building the best way to move and manage the world's money...",
  "description_html": null,
  "apply_url": "https://jobs.smartrecruiters.com/Wise/744000159127419-backend-engineer-send-core?oga=true",
  "job_url": "https://jobs.smartrecruiters.com/Wise/744000159127419-backend-engineer-send-core",
  "posted_at": "2026-11-02T12:45:08Z",
  "updated_at": null,
  "scraped_at": "2026-11-02T13:15:24Z",
  "company_slug": "Wise",
  "company_input": "smartrecruiters:Wise",
  "detected_via": "explicit"
}
```

| Field | Description |
|---|---|
| `company`, `ats`, `job_id` | Company name, ATS name and the ATS's own job ID (`ats` + `company_slug` + `job_id` is unique). |
| `title`, `department`, `team` | As published. `null` when the ATS has no such field. |
| `location`, `remote`, `workplace_type` | All locations joined with `; `. `remote` is `true`/`false`; `workplace_type` is `remote`, `hybrid`, `onsite` or `null`. |
| `employment_type` | Normalized: `full-time`, `part-time`, `contract`, `internship`, `temporary`, `working-student`. |
| `salary` | `{min, max, currency, period, text}` when the company publishes it, otherwise `null`. |
| `description_text`, `description_html` | Clean plain text / original HTML (optional). |
| `apply_url`, `job_url` | Application form and job page. |
| `posted_at`, `updated_at`, `scraped_at` | ISO 8601, UTC. |
| `company_slug`, `company_input`, `detected_via` | Board ID, what you typed, and how the board was found (`url`, `website`, `name-guess`, `explicit`). |

A **run summary** (one row per company: ATS found, open jobs, matched, saved, error message) is stored as `SUMMARY` in the run's key-value store, so you can see at a glance which companies were not found.

Download the dataset as JSON, CSV, Excel, XML or RSS, or read it through the Apify API.

### Monitoring new jobs (pay only for what's new)

1. Turn on **Only new jobs** and give the monitor a name (e.g. `fintech-engineering`).
2. Optional: turn on **First run: save current jobs as seen** if you want alerts only for jobs posted from now on.
3. Create a **Schedule** (daily or hourly) in Apify and connect an integration (Slack, email, Google Sheets, Zapier, Make, n8n, webhook).

Each run outputs only jobs that this monitor has never output before. You pay $0.001 per company checked plus $0.001 per new job: watching 100 companies every day costs about $3 a month plus the new jobs.

### Pricing

Pay per event, no subscription:

| Event | Price |
|---|---|
| Job saved to the dataset | **$0.001** ($1 per 1,000 jobs) |
| Company board checked (found and read) | **$0.001** |

Examples: 1,000 jobs from 20 companies = **$1.02**. 50 jobs from 5 companies = **$0.055**. Companies that are not found are not charged, and filtered-out jobs are not charged. Set "Maximum cost per run" in the run options: the actor stops cleanly when it is reached.

### FAQ

**Is this legal? Do I need an API key?**
No key or login is needed. The actor only reads the public, unauthenticated job-board endpoints that each ATS provides so that companies can show their published jobs on their careers pages (for example, Greenhouse documents that its Job Board GET endpoints need no authentication, and Lever notes that published postings may be read by third parties). Only published job ads are collected: no candidate data, no personal profiles. Make sure your own use of the data (e.g. re-publishing) respects each company's terms.

**Why did a company return the wrong board or nothing?**
Name guessing can hit another company with the same name or an old board. Use the careers page URL or the explicit `ats:slug` form for 100 % accuracy. Check `detected_via` and the `SUMMARY` record.

**Does it support Workday, iCIMS, Taleo or BambooHR?**
Not yet. If you need one, open an issue on the actor page.

**How fresh is the data?**
Live. Every run reads the ATS at that moment; nothing comes from a cache or a stale database.

**How many companies can I put in one run?**
Up to 5,000. Three companies are processed in parallel; most boards take 1-3 seconds.

**Can I get jobs from all companies on Greenhouse without a list?**
No. The ATSs do not publish a list of their customers, so you give the companies. Tip: keep your target list in a Google Sheet and pass it through the API.

**Why is `salary` null?**
The company did not publish a range in the ATS. US states with pay-transparency laws make it common on Greenhouse, Lever and Ashby boards.

**Can I use it from Python, JavaScript, Make, Zapier or n8n?**
Yes: every Apify actor is an API. Call it with the Apify client, from Make/Zapier/n8n, or as an MCP tool from your AI agent.

### Limits

- The ATS must be one of the 7 supported systems (Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio). Workday, iCIMS, Taleo and BambooHR are not supported yet.
- A plain company **name** is guessed and can match the wrong board. Use the careers URL or `ats:slug` for exact results.
- Salary appears only when the company published it in the ATS. Otherwise `salary` is `null`.
- You provide the company list: the ATSs do not publish a directory of their customers.

### Use it from code (API)

Every Apify Actor is also an API. Replace `<YOUR_API_TOKEN>` with the token from your Apify account (Settings, API & Integrations).

**JavaScript**

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

const client = new ApifyClient({ token: '<YOUR_API_TOKEN>' });
const run = await client.actor('phantom-x64/ats-jobs-scraper').call({
    "companies": [
      "https://jobs.lever.co/spotify",
      "ashby:notion"
    ],
    "keywords": [
      "engineer"
    ],
    "maxJobs": 50
  });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

**Python**

```python
from apify_client import ApifyClient

client = ApifyClient('<YOUR_API_TOKEN>')
run = client.actor('phantom-x64/ats-jobs-scraper').call(run_input={
      "companies": [
        "https://jobs.lever.co/spotify",
        "ashby:notion"
      ],
      "keywords": [
        "engineer"
      ],
      "maxJobs": 50
    })
items = client.dataset(run['defaultDatasetId']).list_items().items
print(items)
```

You can also start it from Make, Zapier, n8n or Google Sheets with the Apify integrations, schedule it, or call it as a tool from an AI agent through the Apify MCP server.

### Support and feedback

Found a bug, need a new field or another source? Open an issue in the **Issues** tab of this Actor page. Reports with a run link are fixed fastest. If it saves you time, a short review on the Store page helps other people find it.

### Keywords

greenhouse scraper, greenhouse jobs api, lever jobs api, lever scraper, ashby jobs, ashby jobs api, ats scraper, ats jobs api, job postings api, careers page scraper, company jobs scraper, hiring signals, workable jobs scraper, smartrecruiters jobs api, recruitee scraper, personio jobs api, job monitoring.

# Actor input Schema

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

One company per line. Accepts a careers / job-board URL (e.g. https://jobs.lever.co/spotify, https://boards.greenhouse.io/airbnb, https://jobs.ashbyhq.com/linear), a company website or domain (e.g. stripe.com - the actor finds the careers page and the ATS behind it), a plain company name (e.g. Notion - the board is guessed, double-check the result) or an explicit 'ats:slug' pair (e.g. greenhouse:stripe, lever:palantir, ashby:openai, workable:huggingface, smartrecruiters:Wise, recruitee:bunq, personio:1komma5grad).

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

Keep only jobs whose title (or department/team) contains at least one of these words or phrases. Whole-word, case-insensitive: 'data' matches 'Data Engineer' but not 'Database'. Leave empty to keep all jobs.

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

Also match the keywords against the full job description text (more results, less precise).

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

Drop jobs whose title contains any of these words or phrases (e.g. 'Senior', 'Intern', 'Manager').

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

Keep only jobs whose location contains one of these texts (case-insensitive substring), e.g. 'London', 'Germany', 'Spain', 'EMEA'. Adding 'Remote' also keeps every job flagged as remote.

## `remote` (type: `string`):

Filter by workplace type: any, remote only, remote or hybrid, or exclude remote jobs.

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

Keep only jobs whose department or team contains one of these texts, e.g. 'Engineering', 'Sales', 'Marketing'.

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

Keep only jobs first published in the last N days (by the ATS publication date). Leave empty for no date limit.

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

Keep only jobs published on or after this date, format YYYY-MM-DD (e.g. 2026-10-01). If both date filters are set, the most recent one applies.

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

Maximum number of jobs saved per company (newest first). 0 = no limit.

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

Stop after saving this many jobs in the whole run (cost control). 0 = no limit.

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

Add the full job description as clean plain text (description_text). Turn off for smaller datasets.

## `includeDescriptionHtml` (type: `boolean`):

Also add the original HTML description (description_html), e.g. to re-publish jobs on your own job board.

## `onlyNew` (type: `boolean`):

For scheduled runs: output (and pay for) only jobs that were not output in a previous run of this monitor. Seen job IDs are kept in the named key-value store 'ats-jobs-scraper-state' of your account.

## `monitorName` (type: `string`):

Only used with 'Only new jobs'. Use a different name for each schedule that has different companies or filters, so each one keeps its own memory of seen jobs.

## `baselineOnFirstRun` (type: `boolean`):

Only used with 'Only new jobs'. On the first run for a company, remember all its current jobs without outputting them, so later runs return only jobs posted after you started monitoring. Only the company check is charged.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/discord",
    "https://jobs.lever.co/spotify",
    "linear.app",
    "ashby:notion"
  ],
  "searchInDescription": false,
  "remote": "any",
  "maxJobsPerCompany": 0,
  "maxJobs": 100,
  "includeDescription": true,
  "includeDescriptionHtml": false,
  "onlyNew": false,
  "monitorName": "default",
  "baselineOnFirstRun": false
}
```

# Actor output Schema

## `results` (type: `string`):

No description

# 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": [
        "https://boards.greenhouse.io/discord",
        "https://jobs.lever.co/spotify",
        "linear.app",
        "ashby:notion"
    ],
    "maxJobs": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("phantom-x64/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": [
        "https://boards.greenhouse.io/discord",
        "https://jobs.lever.co/spotify",
        "linear.app",
        "ashby:notion",
    ],
    "maxJobs": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("phantom-x64/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": [
    "https://boards.greenhouse.io/discord",
    "https://jobs.lever.co/spotify",
    "linear.app",
    "ashby:notion"
  ],
  "maxJobs": 100
}' |
apify call phantom-x64/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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