# Multi-ATS Jobs Scraper & New Jobs Monitor (`arcaedion/ats-jobs-scraper`) Actor

Scrape every open job from company career pages on Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee, Personio and Workday - from a URL, domain or just a company name. Normalized output with salary, remote flag and seniority, plus new-jobs-only monitoring.

- **URL**: https://apify.com/arcaedion/ats-jobs-scraper.md
- **Developed by:** [Mathews Motta](https://apify.com/arcaedion) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 job returneds

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

## Multi-ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee & Personio

Scrape every open job from company career pages on Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee, Personio and Workday. Pass a careers URL, an ATS board URL, a domain or a company name and get one normalized record per job.

### What does it do

Give the actor a list of companies. It finds the public job board behind each careers site, reads every open posting and writes them to a dataset in one consistent schema. It uses plain HTTP requests, so there is no browser and no proxy, and runs are fast and cheap.

- **Eight ATS platforms** in one actor, with the same output for all of them.
- **Flexible input**: ATS URL, careers page, domain or company name. Pages are scanned for embedded boards.
- **Normalized fields**: `remote`, `seniority` (inferred from the title), `department`, published salary and `postedAt`.
- **Only new jobs**: remembers seen postings per monitor name, so scheduled runs return just the fresh ones.
- **Filters**: keywords, exclusions, locations, remote only, departments, posted within N days, per-company cap.

### Who is it for

- **Job boards and niche job sites** that need a steady feed from specific employers.
- **Recruiters and agencies** tracking openings at target companies.
- **Job-alert bots** that post new matches to Slack, Telegram or email.
- **Market and salary research** across titles, levels and published pay.
- **Lead generation**: a company that is hiring has budget and a reason to buy.
- **AI agents and RAG**: structured job data with descriptions.

### How much does it cost

You pay per job returned: **$1.50 per 1,000 jobs**, plus a small actor-start fee set by Apify. Filtered-out jobs and already-seen jobs are free, and so are failed companies. The max-cost-per-run limit you set is respected: when it is reached, the run stops cleanly. The first run of a monitor charges for every matching job, since it records them as the baseline.

### Input

| Field | Type | Description |
|---|---|---|
| `companies` | string\[] | Required. ATS URLs, careers URLs, domains or company names (max 1,000). |
| `keywords` | string\[] | Keep jobs whose title or description contains any of these. |
| `excludeKeywords` | string\[] | Drop jobs whose title, department or team contains any of these. |
| `locations` | string\[] | Keep jobs whose location contains any of these (substring). |
| `remoteOnly` | boolean | Keep only jobs explicitly remote. Default `false`. |
| `postedWithinDays` | integer | Keep jobs published in the last N days. `0` disables it. |
| `departments` | string\[] | Keep jobs whose department or team matches. |
| `onlyNewJobs` | boolean | Output only jobs not seen under the same monitor name. |
| `monitorName` | string | Name of the seen-jobs memory. Default `default`. |
| `includeDescription` | boolean | Add plain-text `descriptionText`. Default `true`. |
| `includeDescriptionHtml` | boolean | Also add `descriptionHtml`. Default `false`. |
| `maxJobsPerCompany` | integer | Cap per company after filters. `0` means unlimited. |

Example input:

```json
{
    "companies": [
        "https://boards.greenhouse.io/stripe",
        "https://jobs.ashbyhq.com/notion",
        "https://jobs.lever.co/spotify"
    ],
    "maxJobsPerCompany": 10
}
```

### Output

One dataset item per job:

| Field | Type | Description |
|---|---|---|
| `id` | string | Stable: `${source}:${sourceJobId}`. |
| `source`, `sourceJobId` | string | ATS name (`greenhouse`, `lever`, ...) and the ATS job ID. |
| `title`, `company`, `companySlug` | string | As published; `companySlug` is the board token. |
| `url`, `applyUrl` | string | Public posting link and application link. |
| `locations` | string\[] | Locations as listed. |
| `remote` | boolean or null | `null` when unknown. |
| `department`, `team`, `employmentType` | string | When the ATS provides them. |
| `seniority` | string | Inferred: intern, junior, mid, senior, lead, manager, director, executive. |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod` | number, string | Pay range when published (Greenhouse, Lever, Ashby, Recruitee). |
| `tags` | string\[] | Labels from the ATS, when any. |
| `descriptionText`, `descriptionHtml` | string | Plain text (HTML stripped) and optional original HTML. |
| `postedAt`, `updatedAt`, `scrapedAt` | string | ISO 8601 timestamps. |

Sample item (one real record, description shortened):

```json
{
  "id": "ashby:1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
  "source": "ashby",
  "sourceJobId": "1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
  "title": "Software Engineer, AI Connections (Developer Platform)",
  "company": "Notion",
  "companySlug": "notion",
  "url": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
  "applyUrl": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a/application",
  "locations": ["San Francisco, California", "New York, New York"],
  "remote": false,
  "department": "Engineering",
  "team": "Engineering",
  "employmentType": "Full-time",
  "seniority": null,
  "salaryMin": null,
  "salaryMax": null,
  "salaryCurrency": null,
  "salaryPeriod": null,
  "tags": [],
  "descriptionText": "Who We Are\nNotion is the collaborative AI workspace where teams and agents think together. We're building one place where your knowledge, projects, me...",
  "descriptionHtml": null,
  "postedAt": "2026-08-24T14:44:49.699Z",
  "updatedAt": null,
  "scrapedAt": "2026-10-23T00:23:54.713Z"
}
```

### Monitoring new jobs

1. Enable **Only new jobs** and set a **Monitor name** such as `fintech-engineering`.
2. The first run outputs every matching job and stores their IDs as the baseline.
3. Add a schedule in Apify Schedules (for example, daily) with the same input. Later runs output only postings that were not seen before. Already-seen jobs are neither output nor charged.

Use one monitor name per company list or filter, so their state stays independent.

### Integrations

- **API**: start runs and read results through the Apify API, for example the `run-sync-get-dataset-items` endpoint.
- **Webhooks**: trigger a workflow when a run succeeds.
- **Zapier, Make, n8n**: connect the Apify app to send new jobs to your tools.
- **Google Sheets and exports**: push runs to a sheet with the Apify integrations, or download JSON, CSV or Excel from the Console.

### FAQ

**Is it legal to scrape these job boards?**
The actor reads only public postings through the public job-board endpoints each ATS provides for its career pages. It does not log in or collect candidate data. You remain responsible for how you use the data, so check the rules that apply to you.

**Which companies are supported, and how do I find the ATS URL?**
Any company that posts on one of the eight platforms. Open the careers page and check where the "Apply" links go, for example `boards.greenhouse.io` or `jobs.lever.co`. Paste that URL. If you are unsure, paste the careers page or domain and the actor will look for an embedded board.

**What are the limits?**
Up to 1,000 companies per run. Use `maxJobsPerCompany` for very large employers. For Workday, descriptions are fetched for the first 500 matching jobs per company.

**What if a company is not found?**
The run continues. The company is listed in the `SUMMARY` record with the reason, and nothing is charged for it. Name lookups are best effort, so a URL is always more reliable.

**How fresh is the data?**
Each run reads the live board, so freshness equals your schedule. Workday only shows relative dates in lists, so its `postedAt` is approximate.

### Related actors

- **[Company Hiring Signals](https://apify.com/arcaedion/company-hiring-signals)**: hiring activity by company, useful as a lead-generation signal.

# Actor input Schema

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

One entry per company. Each entry can be: <br>• an <b>ATS board URL</b> - fastest and most reliable, e.g. <code>https://boards.greenhouse.io/stripe</code>, <code>https://jobs.lever.co/spotify</code>, <code>https://jobs.ashbyhq.com/notion</code>, <code>https://apply.workable.com/huggingface</code>, <code>https://bunq.recruitee.com</code>, <code>https://knime.jobs.personio.de</code>, <code>https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite</code>;<br>• a <b>careers page or website</b> such as <code>https://www.example.com/careers</code> or <code>example.com</code> - the page is scanned for an embedded or linked job board;<br>• a <b>company name</b> such as <code>Stripe</code> - common board names are probed automatically (may miss or mismatch for ambiguous names; a URL is always safer).<br>Companies that cannot be resolved are reported in the SUMMARY record and never stop the run or cost anything.

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

Only keep jobs whose <b>title or description</b> contains at least one of these words or phrases (case-insensitive). Leave empty to keep everything. Example: <code>data engineer</code>, <code>python</code>.

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

Drop jobs whose <b>title</b> (or department) contains any of these words (case-insensitive). Example: <code>intern</code>, <code>sales</code>.

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

Only keep jobs located in at least one of these places. Matched as a case-insensitive substring of the job's location text, so use whatever the company uses: <code>Berlin</code>, <code>United States</code>, <code>London</code>, <code>Remote</code>.

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

Keep only jobs that are explicitly remote - flagged by the ATS or with 'remote' in the title / location. Jobs where remote status is unknown are dropped.

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

Only keep jobs first published in the last N days. 0 disables the filter. Jobs without a publish date are dropped when this is set. Note: Workday only exposes a relative date for list results ('Posted 30+ Days Ago'), which is approximated.

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

Only keep jobs whose department or team contains one of these words (case-insensitive substring), e.g. <code>Engineering</code>, <code>Product</code>. Workday does not expose departments, so its jobs never match this filter.

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

Output only jobs not seen in a previous run with the same <b>Monitor name</b>. Schedule the Actor daily to get just the fresh postings. <b>The first run of a monitor outputs all matching jobs</b> (and remembers them as the baseline); every later run outputs only newly appeared ones. Already-seen and filtered-out jobs are never output or charged.

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

Identifies the saved list of already-seen jobs for 'Only new jobs'. Use different names for different company lists or filters (e.g. <code>fintech-engineering</code>) so they track new jobs independently. State is stored per name in a named key-value store under your account.

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

Add the plain-text job description (<code>descriptionText</code>). SmartRecruiters and Workday need one extra request per job for this, so runs on those are faster when it is off (descriptions are still fetched internally if a keyword filter needs them).

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

Also add the original HTML of the description (<code>descriptionHtml</code>). Increases dataset size.

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

Cap on jobs output per company, applied after filters. 0 means unlimited. Useful for very large employers (thousands of openings) and for quick test runs.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "notion.so",
    "Spotify"
  ],
  "keywords": [
    "data engineer",
    "machine learning"
  ],
  "excludeKeywords": [
    "intern",
    "sales"
  ],
  "locations": [
    "Berlin",
    "London"
  ],
  "remoteOnly": false,
  "postedWithinDays": 7,
  "departments": [
    "Engineering"
  ],
  "onlyNewJobs": false,
  "monitorName": "default",
  "includeDescription": true,
  "includeDescriptionHtml": false,
  "maxJobsPerCompany": 50
}
```

# Actor output Schema

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

One normalized record per job posting.

## `summary` (type: `string`):

Per-company status, counts, failures and charged items for this run.

# 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/stripe",
        "https://jobs.ashbyhq.com/notion",
        "https://jobs.lever.co/spotify"
    ],
    "maxJobsPerCompany": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("arcaedion/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/stripe",
        "https://jobs.ashbyhq.com/notion",
        "https://jobs.lever.co/spotify",
    ],
    "maxJobsPerCompany": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("arcaedion/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/stripe",
    "https://jobs.ashbyhq.com/notion",
    "https://jobs.lever.co/spotify"
  ],
  "maxJobsPerCompany": 10
}' |
apify call arcaedion/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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