# ATS Jobs Scraper: All Company Job Boards (`alexmorain/ats-jobs-scraper`) Actor

Get live job openings from any company, whatever its ATS: Greenhouse, Ashby, Lever, Workday, SmartRecruiters, Workable, Personio, Rippling, Recruitee. One unified schema, cross-ATS dedup, new-jobs-only delta mode.

- **URL**: https://apify.com/alexmorain/ats-jobs-scraper.md
- **Developed by:** [Alex Morain](https://apify.com/alexmorain) (community)
- **Categories:** Jobs, Lead generation, AI
- **Stats:** 1 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $1.50 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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. Any company, any ATS, one schema.

Companies publish their jobs through a handful of applicant tracking systems. This Actor reads
all of them: **Greenhouse, Ashby, Lever, Workday, SmartRecruiters, Workable, Personio, Rippling
and Recruitee**. You give it companies, it gives you their live job openings in one unified
schema, whatever system each company happens to use.

Three things the single-ATS scrapers cannot do:

**A built-in directory of 17,000+ job boards.** You do not need to know which ATS a company
uses. Pass `stripe.com` and the embedded directory resolves it. Pass a board URL or an explicit
`greenhouse:stripe` pair when you want full control.

**Cross-ATS deduplication, because migrations leave ghosts.** When a company changes ATS, the
old board often keeps serving every job. Real example measured in August 2026: one well-known
database company served the exact same 94 openings on both its Greenhouse and its Ashby boards.
A naive aggregator returns them twice and charges you twice. This Actor deduplicates by company,
title and location before pushing, and a job you are not shown twice is a job you are not
charged for twice.

**A delta mode that turns scraping into monitoring.** In `new-only` mode the Actor keeps a
persistent registry of everything it has already seen and only outputs jobs that are new since
the last run. Schedule it daily on your target list: a run that finds nothing costs one cent.
Add a `webhookUrl` and it pings your endpoint whenever new jobs appear.

### How to scrape company job boards

1. Open the **Input** tab. Three companies are already filled in, one per input form, so you can press **Start** right away and see what comes out.
2. Replace them with your own list. A domain (`stripe.com`) is the simplest form and works for most companies. If one fails to resolve, the log names it: open its careers page, copy the job board URL it redirects to, and paste that instead.
3. Optionally filter with **title keywords**, **location keywords** or **remote only**. Filtering happens before output, so a job you filter out is a job you do not pay for.
4. Press **Start**, then download the dataset, or switch to `new-only` mode and schedule it for daily monitoring.

#### Which fields should I fill?

| If you want to... | Fill in | Leave alone |
|---|---|---|
| Export every opening of a company list | `companies` | everything else |
| Monitor new openings daily | `companies`, `mode: new-only`, a schedule | everything else |
| Get notified when a role opens | add `webhookUrl` to the above | everything else |
| Build a targeted list of roles | `titleKeywords`, `locationKeywords` | `includeDescription` |
| Classify jobs downstream with an LLM | `includeDescription: true` | filters |

`companies` is the only required field. Everything else is off by default.

### What you get

One row per unique live job:

| Field | Notes |
|---|---|
| `title`, `company`, `location`, `apply_url` | filled on 100% of jobs in our benchmark run |
| `remote` | ATS flag when available, location text signal otherwise |
| `department`, `employment_type` | when the ATS provides them |
| `posted_at` | ISO 8601, only when the source serves a real date, never a guess |
| `salary_min`, `salary_max`, `salary_currency`, `salary_period` | Ashby, Lever and Recruitee publish ranges |
| `description` | full text, opt-in via `includeDescription` |
| `raw` | the untouched original payload, for anything ATS-specific |
| `ats`, `board_slug`, `job_id` | provenance, stable identifiers |

### Input in ten seconds

```json
{
    "companies": ["stripe.com", "https://jobs.lever.co/acme", "greenhouse:duolingo"],
    "mode": "new-only",
    "titleKeywords": ["engineer", "developer"],
    "remoteOnly": true,
    "webhookUrl": "https://your-endpoint.example/new-jobs"
}
```

Filters (`titleKeywords`, `locationKeywords`, `remoteOnly`) are applied before output: a
filtered-out job is never charged.

### Honest limits, so you are not surprised

- **Workday list mode has no reliable posting date.** Workday serves relative labels like
  "Posted 30+ Days Ago". We never convert those into fake timestamps: `posted_at` stays null
  there. Use `new-only` mode for reliable first-seen dating.
- **Descriptions cost extra requests on SmartRecruiters and Workday** (one per job), and
  multiply the payload by 12 on Greenhouse. That is why `includeDescription` is off by default.
- **Cross-ATS dedup is best-effort.** Boards write locations differently ("Remote" vs
  "Remote, US"), so a small share of migration duplicates can survive. Measured on the worst
  real case we know: 98 of 174 duplicates caught by the exact key, the rest differ in location
  wording.
- **The directory is a snapshot.** It is rebuilt monthly from public web indexes. A company
  missing from it still works: pass its board URL or `ats:slug`, or the Actor probes the ATS
  APIs live from the domain.
- **A dead board is reported, not hidden.** Unknown slugs and 404 boards are logged per
  company; the run only fails if every company fails.

### Pricing logic

Pay per event: one start fee per run, one `job` event per unique job pushed. Duplicates,
filtered-out jobs and already-seen jobs in delta mode are never charged. Scheduled monitoring
of a company list costs one cent per quiet day.

### For pipelines and agents

Output is a clean dataset, one JSON object per job, stable keys, ISO dates. Runs are
idempotent per registry. The Actor exits FAILED when nothing could be scraped, so your
automation can trust the run status. Typical run: 50 companies, 3,000 jobs, about two minutes.

# Actor input Schema

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

One company per line. Three accepted forms, freely mixed in the same list:<br>• <b>a domain</b>, the simplest: <code>stripe.com</code>. A built-in directory of 17,000+ job boards resolves it, and if the company is not in it the Actor probes the ATS APIs live.<br>• <b>a job board URL</b>, the most reliable: <code>https://jobs.lever.co/acme</code>, <code>https://acme.wd5.myworkdayjobs.com/External</code>. Open a company careers page, copy the URL it redirects to, paste it here.<br>• <b>an explicit ats:slug pair</b>, for full control: <code>greenhouse:stripe</code>, <code>ashby:linear</code>. Workday needs its three parts: <code>workday:acme|wd5|External</code>.<br><br>You never need to know which ATS a company uses. Start with domains; switch to a board URL only for a company that fails to resolve, which the log names explicitly.<br><br>A company that fails does not stop the others: the run only fails if every single one fails.

## `mode` (type: `string`):

<b>All live jobs</b> returns every opening currently published by these companies, on every run. Use it for a one-off export, or to build your first snapshot.<br><br><b>New jobs only</b> keeps a persistent registry of everything previous runs have already seen and returns only what appeared since. This turns the Actor into monitoring: schedule it daily and a quiet day costs you the start fee alone, since a job you are not shown is a job you are not charged for. The first run in this mode still returns everything, because nothing has been seen yet.

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

Adds the complete job description to every row.<br><br><b>Off by default on purpose.</b> Descriptions multiply the payload up to 12x on some boards, and on SmartRecruiters and Workday they cost one extra HTTP request per job, which makes the run several times longer. It does not change the price.<br><br>Turn it on when you need to read or classify the content of the jobs, for example to filter with an LLM downstream. Leave it off when you only need titles, locations and apply links.

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

Safety cap per company, useful when your list contains a giant that publishes thousands of openings and would dominate both the dataset and the bill.<br><br><code>0</code> means no cap, which is the default: you get everything each company publishes.

## `titleKeywords` (type: `array`):

Keep only jobs whose title contains at least one of these words, case-insensitive. One word or phrase per line: <code>engineer</code>, <code>data</code>, <code>product manager</code>.<br><br><b>Filtering is free.</b> It happens inside the Actor before anything is returned, so a job that does not match is never charged. Filtering a 3,000-job board down to the 40 roles you care about costs you 40 jobs, not 3,000.<br><br>Leave empty to keep every title.

## `locationKeywords` (type: `array`):

Keep only jobs whose location contains at least one of these words, case-insensitive: <code>Paris</code>, <code>New York</code>, <code>Germany</code>.<br><br>Boards write locations in their own way, so match on the shortest distinctive part: <code>Berlin</code> catches "Berlin, Germany" and "Berlin (hybrid)" alike. Free like the title filter: non-matching jobs are never charged.<br><br>Leave empty to keep every location.

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

Keep only jobs the ATS itself flags as remote, plus those whose location text says so. Applied before output, so filtered-out jobs are never charged.<br><br>Note that boards declare remote inconsistently; combining this with a location keyword such as <code>remote</code> catches the ones that only say it in words.

## `registryKey` (type: `string`):

Only used by <b>New jobs only</b> mode. Names the persistent registry of already-seen jobs.<br><br>Leave empty and it is derived from your company list, which is what you want: the same list always finds its own registry again, and two different lists never pollute each other. Set it by hand only to make several schedules share one memory, for example a daily run and a weekly catch-up over the same companies.

## `webhookUrl` (type: `string`):

If set, the Actor POSTs a small JSON summary to this URL at the end of the run, <b>only when at least one job was returned</b>. Combined with delta mode and a daily schedule, that is a push notification: silence when nothing moved, a call when a company opens a role.<br><br>Leave empty to skip it. An unreachable webhook is logged as a warning and never fails the run.

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

<b>Leave this off.</b> Every ATS endpoint the Actor reads is a public JSON API with no anti-bot layer, so a proxy adds latency and buys nothing. Kept for the rare corporate network edge case.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "greenhouse:duolingo",
    "https://jobs.lever.co/acme"
  ],
  "mode": "all",
  "includeDescription": false,
  "maxJobsPerCompany": 0,
  "titleKeywords": [
    "engineer",
    "developer"
  ],
  "locationKeywords": [
    "Paris",
    "Remote"
  ],
  "remoteOnly": false
}
```

# Actor output Schema

## `dataset` (type: `string`):

Dataset containing all scraped job offers in the unified schema

# 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:duolingo",
        "ashby:linear",
        "https://jobs.lever.co/AIFund"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("alexmorain/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:duolingo",
        "ashby:linear",
        "https://jobs.lever.co/AIFund",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("alexmorain/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:duolingo",
    "ashby:linear",
    "https://jobs.lever.co/AIFund"
  ]
}' |
apify call alexmorain/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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