# Multi-ATS Jobs Scraper: Greenhouse, Lever, Ashby & more (`jakob_jerich/multi-ats-jobs`) Actor

Get every open job from company career pages on Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee and Personio in one normalized schema: salaries, remote flag, filters and new-job monitoring. $1 per 1,000 jobs.

- **URL**: https://apify.com/jakob_jerich/multi-ats-jobs.md
- **Developed by:** [Jakob Jerič](https://apify.com/jakob_jerich) (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

$1.00 / 1,000 job saveds

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

Get **every open job from company career pages** in one clean, normalized dataset. Give it a list of companies (names or job board URLs) and it pulls all their current openings straight from the official public job board APIs of **Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee and Personio**: titles, departments, locations, remote/hybrid flag, employment type, **salary ranges**, publish dates, apply links and full descriptions.

- **7 applicant tracking systems, one output schema.** Same 29 fields for every job, whatever ATS the company uses.
- **Just type a company name.** `notion`, `vercel`, `bunq`... the Actor finds which ATS the company uses. Or paste the job board URL.
- **Salaries included.** Structured pay ranges from Ashby, Lever and Recruitee, plus pay-transparency ranges parsed from descriptions (e.g. "$156,800 – $235,200").
- **Monitoring mode.** Schedule it daily and get **only newly posted jobs**, so you pay only for new ones.
- **Filters before you pay**: title keywords, excluded words, location, department, remote only, employment type, posted in the last N days.
- **Live data, no login, no proxies.** Uses only official public job board endpoints, the same ones that power the companies' own career pages.
- **Cheap**: $1.00 per 1,000 jobs. No start fee. Companies that aren't found cost nothing.

### What can I use it for?

- **Job boards and aggregators**: keep a niche job board (remote, climate, AI, crypto, a city) filled with fresh roles directly from employers.
- **Recruiting and sales intelligence**: see which companies are hiring, for which teams and where. Hiring for a role often signals a need for your product.
- **Market and salary research**: compare pay ranges by role, location and company.
- **Job seekers and career coaches**: watch a list of dream companies and get alerted to new openings.
- **Investors and analysts**: track headcount growth plans across a portfolio or a competitor set.
- **AI agents and LLM pipelines**: clean, consistent JSON with plain-text descriptions, ready for RAG or matching.

### How to use it

1. Add companies to **Companies or job board URLs**, one per line. Any of these forms works:
   - Job board URL: `https://boards.greenhouse.io/stripe`, `https://jobs.lever.co/spotify`, `https://jobs.ashbyhq.com/openai`, `https://careers.smartrecruiters.com/BoschGroup`, `https://apply.workable.com/huggingface`, `https://bunq.recruitee.com`, `https://ottonova.jobs.personio.de`
   - Explicit `ats:slug`: `greenhouse:figma`, `lever:spotify`, `ashby:linear`, `smartrecruiters:BoschGroup`, `workable:huggingface`, `recruitee:bunq`, `personio:ottonova`
   - Plain company name: `notion`. The Actor checks all 7 ATSs and picks the board with the most open jobs.
2. Optionally set filters (title keywords, location, remote only, ...).
3. Click **Start**. Download results as JSON, CSV, Excel or via the API.

#### Example input

```json
{
  "companies": ["https://boards.greenhouse.io/stripe", "https://jobs.lever.co/spotify", "ashby:linear", "notion"],
  "titleKeywords": ["engineer", "developer"],
  "excludeTitleKeywords": ["intern"],
  "locationKeywords": ["remote", "London", "Berlin"],
  "postedWithinDays": 30,
  "descriptionFormat": "text",
  "onlyNewJobs": false
}
```

#### Example output (one job)

```json
{
  "ats": "ashby",
  "companySlug": "notion",
  "companyName": "Notion",
  "jobId": "06e1d3a8-2706-4ed9-9fe6-8b69607de18e",
  "title": "Security Engineer, Detection and Response",
  "department": "Security",
  "team": "Security",
  "location": "San Francisco, California",
  "locations": ["San Francisco, California", "New York, New York"],
  "country": "United States",
  "workplaceType": "HYBRID",
  "isRemote": false,
  "employmentType": "FULL_TIME",
  "employmentTypeRaw": "FullTime",
  "experienceLevel": null,
  "salaryMin": 185000,
  "salaryMax": 280000,
  "salaryCurrency": "USD",
  "salaryPeriod": "YEAR",
  "salaryText": "$185,000 - $280,000",
  "publishedAt": "2026-09-28T16:20:26.330Z",
  "updatedAt": null,
  "jobUrl": "https://jobs.ashbyhq.com/notion/06e1d3a8-2706-4ed9-9fe6-8b69607de18e",
  "applyUrl": "https://jobs.ashbyhq.com/notion/06e1d3a8-2706-4ed9-9fe6-8b69607de18e/application",
  "descriptionText": "WHO WE ARE\n\nNotion is the collaborative AI workspace where t ...",
  "descriptionHtml": null,
  "isNew": null,
  "input": "notion",
  "scrapedAt": "2026-10-31T23:36:36.827Z"
}
```

Real item from a run (description shortened here). Every job has all fields; unknown values are `null` (and `locations` is `[]`), so CSV columns stay stable across runs and ATSs.

#### Output fields

| Field | Description |
|---|---|
| `ats` | `greenhouse`, `lever`, `ashby`, `smartrecruiters`, `workable`, `recruitee` or `personio` |
| `companySlug`, `companyName` | Board identifier on the ATS and the company name (falls back to the capitalized slug when the ATS doesn't publish a name) |
| `jobId` | Job ID on the ATS. `ats` + `companySlug` + `jobId` is unique |
| `title`, `department`, `team`, `experienceLevel` | As published by the company |
| `location`, `locations`, `country` | Primary location, all locations, country (code or name as given) |
| `workplaceType`, `isRemote` | `REMOTE` / `HYBRID` / `ONSITE` / `null`; `isRemote` is `true` only for fully remote jobs |
| `employmentType`, `employmentTypeRaw` | Normalized `FULL_TIME`, `PART_TIME`, `CONTRACT`, `TEMPORARY`, `INTERNSHIP`, `OTHER`, plus the original value |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryText` | Pay range (`HOUR`/`DAY`/`WEEK`/`MONTH`/`YEAR`) when the company publishes one |
| `publishedAt`, `updatedAt` | ISO 8601 timestamps |
| `jobUrl`, `applyUrl` | Posting and application links |
| `descriptionText`, `descriptionHtml` | Full description, depending on the **Job description** setting |
| `isNew` | In monitoring mode `true`; otherwise `null` |
| `input`, `scrapedAt` | Which input line produced the job, and when |

A per-company report (ATS found, jobs on board, jobs matched, jobs saved, errors) is saved as the **OUTPUT** record in the run's key-value store.

### Monitoring new jobs

Turn on **Only new jobs (monitoring mode)** and run the Actor on a [schedule](https://docs.apify.com/platform/schedules) (e.g. daily). The first run saves all matching jobs; each later run saves only jobs that weren't on the board in earlier runs with the same **Monitoring key**. Use a different key for each separate watch list. Connect the run to email, Slack, Google Sheets or a webhook through Apify integrations to get alerts.

### Pricing

**$1.00 per 1,000 jobs saved** (pay-per-event, `job` event = $0.001). You pay only for jobs that end up in your dataset, after filters and monitoring. No start fee, nothing for companies that aren't found, and Apify platform usage is included.

Examples: all 745 open jobs at Stripe ≈ $0.75. A daily monitor of 50 companies that finds 30 new jobs a day ≈ $0.90 a month.

Set **Maximum cost per run** in the run options to cap spending; the Actor stops cleanly when it is reached.

### FAQ

**Which companies are supported?** Any company whose careers page runs on one of the 7 supported ATSs, which is tens of thousands of companies, from startups to enterprises (Stripe, Figma, Notion, OpenAI, Spotify, Bosch, ...). Workday, iCIMS, Taleo, Teamtailor, BambooHR and LinkedIn are **not** supported.

**How do I find a company's board?** Open the company's careers page and click a job. If the URL contains `greenhouse.io`, `lever.co`, `ashbyhq.com`, `smartrecruiters.com`, `workable.com`, `recruitee.com` or `personio.de/.com`, paste that URL. Some companies host their job pages on their own domain (e.g. `stripe.com/jobs`) but still use one of these ATSs; then just try the company name.

**The company name wasn't found. Why?** The board slug can differ from the brand name (e.g. `BoschGroup`). Paste the job board URL, or use `ats:slug`. The OUTPUT record lists the slugs that were tried.

**Is the data live?** Yes. Every run reads the company boards directly at that moment; there is no cached database.

**Does it include personal data?** No. Only job posting data is returned. Recruiter and hiring-manager names, emails and phone numbers that some ATSs expose are not included.

**How are salaries found?** From structured ATS fields where available (Ashby, Lever, Recruitee), otherwise parsed from clear pay-range sentences in the description (common in US pay-transparency text). Ambiguous numbers are skipped, so `null` means no clear range was published. For SmartRecruiters, salary parsing needs descriptions to be enabled.

**How fast is it?** Most boards take under a second. Large SmartRecruiters boards (thousands of jobs) need one request per 100 jobs, and with descriptions on, one request per job.

### Limits

- Up to 500 companies per run; up to 10,000 postings per SmartRecruiters board.
- Lever and Ashby don't publish a company name, so `companyName` is the capitalized board slug there.
- Data quality depends on what each company publishes: department, salary or employment type can be missing.
- Only public job boards are read. If an ATS rate-limits or refuses a request, the company is reported as `BLOCKED` in the OUTPUT record; the Actor never tries to get around blocks.

### Support

Found a board that doesn't work, or need another ATS? Open an issue on the Actor's **Issues** tab with the company or URL and it will be looked at.

# Actor input Schema

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

One entry per company. Use a job board URL (e.g. https://boards.greenhouse.io/stripe, https://jobs.lever.co/spotify, https://jobs.ashbyhq.com/openai, https://careers.smartrecruiters.com/BoschGroup, https://apply.workable.com/acme, https://acme.recruitee.com, https://acme.jobs.personio.de), an explicit "ats:slug" pair (e.g. "greenhouse:stripe"), or just a company name (e.g. "notion"), which is auto-detected across all supported ATSs.

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

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

## `excludeTitleKeywords` (type: `array`):

Drop jobs whose title contains any of these words (case-insensitive), e.g. "senior", "intern".

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

Keep only jobs whose location, country or workplace type contains one of these words (case-insensitive), e.g. "London", "Germany", "remote".

## `departmentKeywords` (type: `array`):

Keep only jobs whose department or team contains one of these words (case-insensitive).

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

Keep only jobs marked as remote by the ATS (or with "remote" in the location).

## `employmentTypes` (type: `array`):

Keep only these normalized employment types. Leave empty to keep all. Jobs whose ATS doesn't publish an employment type (common on Greenhouse) are kept.

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

Keep only jobs published (or, if the ATS gives no publish date, updated) in the last N days. 0 = no date filter. Jobs without any date are kept.

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

Include the full job description as plain text, HTML, both, or not at all. "none" makes the output much smaller. For SmartRecruiters, descriptions need one extra request per job.

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

Save at most this many jobs per company (after filters). 0 = no limit.

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

Save only jobs not seen in previous runs with the same monitoring key. The first run saves everything that matches; later runs (e.g. on a daily schedule) save only newly posted jobs, so you pay only for new ones.

## `monitoringKey` (type: `string`):

Name of the 'seen jobs' memory used by monitoring mode. Use a different key for each independent watch list.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/spotify",
    "ashby:linear",
    "bunq"
  ],
  "titleKeywords": [
    "engineer",
    "developer"
  ],
  "excludeTitleKeywords": [
    "intern"
  ],
  "locationKeywords": [
    "London",
    "remote"
  ],
  "departmentKeywords": [
    "engineering"
  ],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "descriptionFormat": "text",
  "maxJobsPerCompany": 0,
  "onlyNewJobs": false,
  "monitoringKey": "default"
}
```

# Actor output Schema

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

All saved job postings in the normalized schema.

## `report` (type: `string`):

ATS found, jobs on board, matched and saved, and errors for each input company.

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

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,jakob_jerich/multi-ats-jobs"
        }
    }
}
```

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/NIKyd3bDgCBEzwCtO/builds/wB7T0VKaMkrLja0v9/openapi.json
