# Multi-ATS Jobs API: Greenhouse, Lever, Ashby, Workable & more (`rod_analytics/multi-ats-jobs-api`) Actor

Get job postings from 7 ATS job boards in one normalized feed: Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee, Personio. Paste board URLs or company domains, the ATS is auto-detected. Salary, remote, filters, only-new mode.

- **URL**: https://apify.com/rod\_analytics/multi-ats-jobs-api.md
- **Developed by:** [Rod Services](https://apify.com/rod_analytics) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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 API** collects job postings straight from the official public job board APIs of **7 applicant tracking systems**: **Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee and Personio**. You get one clean, normalized feed with title, department, locations, remote status, employment type, **salary ranges when published**, dates, apply links and the full description.

Paste ATS board URLs, careers page URLs or just **company domains**. The Actor finds the ATS behind the careers page for you. It is built for **job aggregators, recruiting and sales teams, labor market research and AI agents** that need fresh job data without maintaining seven scrapers.

**$1.20 per 1,000 jobs.** No browser, no proxy, no API keys.

### What does Multi-ATS Jobs API do?

- Reads the **public job board APIs** that Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee and Personio provide for career sites.
- **Auto-detects the ATS** from a careers page or a domain like `figma.com` or `notion.so`, including jobs embedded as JSON or iframes.
- **Normalizes every job** into the same 22 fields, so a Lever job and a Personio job look the same in your database.
- **Filters** by keyword, location, remote, department and posting date.
- **Only new jobs mode** remembers what it returned before, so a daily schedule gives you just the new postings.
- Runs on the Apify platform: **API access, scheduling, webhooks, integrations** (Make, Zapier, n8n, Google Sheets, Slack), monitoring and the **Apify MCP server for AI agents**.

### Why use this job board scraper?

- **Job aggregators and job boards**: fill a niche board (remote, AI, climate, Berlin startups) from hundreds of company career pages.
- **Recruiting and staffing**: track which companies open roles in your field, and reach them first.
- **Sales intelligence**: hiring is a buying signal. A company hiring 20 data engineers needs data tools.
- **Labor market data**: salary ranges, remote share, skills in demand, hiring velocity per company.
- **AI agents and LLM apps**: give an agent a list of companies and get structured, fresh job data through one tool call.
- **One Actor instead of seven**: most alternatives cover one ATS per Actor or three at most.

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

1. Open the **Input** tab.
2. Add companies, one per line. Any of these work:
   - Board URL: `https://boards.greenhouse.io/gitlab`, `https://jobs.lever.co/spotify`, `https://jobs.ashbyhq.com/linear`
   - Shorthand: `greenhouse:airbnb`, `lever:palantir`, `ashby:openai`, `workable:huggingface`, `smartrecruiters:BoschGroup`, `recruitee:bunq`, `personio:holidu`
   - Careers page: `https://linear.app/careers`
   - Domain: `figma.com`
3. Optional: add keywords, locations or turn on **Remote jobs only**.
4. Click **Start**. The prefilled example (3 companies, 20 jobs each) finishes in a few seconds.
5. Download the jobs as **JSON, CSV, Excel or HTML**, or read them through the API.

For monitoring, turn on **Only new jobs since last run** and create a schedule (daily or hourly).

### Input

| Field                    | What it does                                                                                                      |
| ------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `companies`              | Board URLs, `ats:slug`, careers page URLs or domains. Required.                                                   |
| `keywords`               | Keep jobs whose title, department or team contains any word. Short words like AI or QA match whole words only.    |
| `excludeKeywords`        | Drop jobs whose title contains any word (intern, director).                                                       |
| `locations`              | Keep jobs whose location contains any text (Berlin, Germany, United States). Add `remote` to include remote jobs. |
| `departments`            | Keep jobs in matching departments or teams.                                                                       |
| `remoteOnly`             | Only remote jobs.                                                                                                 |
| `postedWithinDays`       | Only jobs first published in the last N days.                                                                     |
| `onlyNew`                | Return only jobs not returned in earlier runs. Seen IDs are kept per company in a named key-value store.          |
| `stateStoreName`         | Name of that store. Use one per schedule.                                                                         |
| `includeDescription`     | Plain text description (default on).                                                                              |
| `includeDescriptionHtml` | Original HTML description (default off).                                                                          |
| `maxJobsPerCompany`      | Cap per company after filters, newest first. 0 = all.                                                             |
| `probeSlugs`             | If a careers page shows no ATS link, try the domain name on each ATS (stripe.com -> stripe).                      |
| `maxConcurrency`         | Companies processed in parallel (default 5).                                                                      |

Example input:

```json
{
    "companies": ["https://boards.greenhouse.io/gitlab", "lever:spotify", "notion.so"],
    "keywords": ["engineer", "data"],
    "locations": ["remote", "Germany"],
    "postedWithinDays": 14,
    "onlyNew": true
}
```

### Output

One dataset item per job. You can download the dataset in various formats such as JSON, HTML, CSV, or Excel.

```json
{
    "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "ats": "ashby",
    "company": "Ramp",
    "companySlug": "ramp",
    "title": "Security Engineer, Cloud",
    "department": "Engineering",
    "team": "Backend",
    "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
    "isRemote": true,
    "workplaceType": "hybrid",
    "employmentType": "full-time",
    "salaryMin": 211400,
    "salaryMax": 290600,
    "salaryCurrency": "USD",
    "salaryPeriod": "year",
    "postedAt": "2026-04-07T17:12:35.753Z",
    "updatedAt": null,
    "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
    "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "descriptionText": "ABOUT RAMP\n\nRamp is building the smart infrastructure for finance teams...",
    "fetchedAt": "2026-09-27T12:11:30.391Z"
}
```

A **SUMMARY** record in the key-value store lists every input with the detected ATS, board slug, detection method (`input`, `page`, `careers-link`, `slug-probe`, `cached`), job counts and errors. Two inputs that point to the same board (`greenhouse:gitlab` and `https://boards.greenhouse.io/gitlab`) are fetched and charged once. The second one shows status `duplicate`.

If the input has no usable company, the run still finishes and the dataset holds one item with `error` and `help` fields. That item is not charged.

### Data fields

| Field                                                      | Description                                                                         |
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `jobId`, `ats`, `companySlug`                              | Stable ID per ATS board. `ats` + `companySlug` + `jobId` is unique.                 |
| `company`                                                  | Company name from the ATS.                                                          |
| `title`, `department`, `team`                              | Job title and org unit.                                                             |
| `locations[]`                                              | All locations.                                                                      |
| `isRemote`, `workplaceType`                                | Remote flag and remote / hybrid / onsite.                                           |
| `employmentType`                                           | full-time, part-time, contract, internship, or the ATS label.                       |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod` | Pay range when the employer publishes it (Greenhouse, Ashby, Lever, Recruitee).     |
| `postedAt`, `updatedAt`                                    | ISO 8601 dates. `updatedAt` only where the ATS provides it (Greenhouse, Recruitee). |
| `applyUrl`, `jobUrl`                                       | Apply form and public job page.                                                     |
| `descriptionText`, `descriptionHtml`                       | Full description as text and optional HTML.                                         |
| `fetchedAt`                                                | Time of the fetch.                                                                  |

### Supported ATS and what each provides

| ATS             | Board URL example                      | Salary              | Remote flag      | Notes                                    |
| --------------- | -------------------------------------- | ------------------- | ---------------- | ---------------------------------------- |
| Greenhouse      | boards.greenhouse.io/gitlab            | yes, when published | from location    | EU boards supported                      |
| Lever           | jobs.lever.co/spotify                  | yes, when published | yes              | EU boards supported, paged by 100        |
| Ashby           | jobs.ashbyhq.com/linear                | yes, when published | yes              | compensation tiers mapped                |
| Workable        | apply.workable.com/huggingface         | no                  | yes              | one request per board                    |
| SmartRecruiters | careers.smartrecruiters.com/BoschGroup | no                  | yes              | description needs one request per job    |
| Recruitee       | bunq.recruitee.com                     | yes, when published | yes              |                                          |
| Personio        | holidu.jobs.personio.de                | no                  | from office name | XML feed must be enabled by the employer |

Teamtailor and Workday are **not supported**.

### How much does it cost to scrape job postings?

Pay per event, no subscription:

- **$1.20 per 1,000 jobs** saved ($0.0012 per job).
- **$0.002 per company** when the ATS is auto-detected from a careers page or domain. Board URLs and `ats:slug` are free to resolve. With `onlyNew`, a detected board is remembered and not charged again.
- **$0.001** per run start.

Examples: the prefill run costs about $0.07. 30 companies with 5,605 jobs cost about $6.73. A daily only-new check of 100 companies with 150 new jobs costs about $0.18 per day. Jobs removed by filters or already seen are not charged. Set **Maximum cost per run** to cap spending.

### Tips

- **Board URLs are fastest and cheapest.** Use detection once, then copy `ats:companySlug` from the output into your input.
- Turn off `includeDescription` for quick counts. SmartRecruiters then needs one request per page of 100 jobs instead of one per job.
- Use `maxJobsPerCompany` to sample large employers.
- Politeness is built in: 2 requests per second per ATS host and automatic backoff on HTTP 429.
- For AI agents: call the Actor through the Apify API or the Apify MCP server with a short company list and `includeDescription: true`.

### FAQ

**Is it legal to scrape these job boards?** The Actor reads only the **public job board APIs** that each ATS offers so employers can publish jobs on their own sites. Greenhouse states job board data is publicly available. Lever states published postings "may be scraped by third parties". No login, key or protection is bypassed. You are responsible for how you use the data.

**Personal data?** Recruiter names and job inbox addresses are dropped. Personal-looking email addresses inside descriptions are replaced with `[email removed]`. Names that employers write into description text are not removed.

**Why was my company not found?** The careers page may use an unsupported ATS (Workday, Teamtailor, SAP, own system) or load jobs only after login. Check `SUMMARY`. If you know the board URL, pass it directly.

**Can slug guessing pick the wrong company?** Rarely, two companies can share a slug. Boards whose company name clearly differs from the domain are skipped. Boards that publish no company name (some Personio, Lever and Ashby boards) can still match another company with the same name. Check `companySlug` and `company`, or pass the board URL.

**Feedback or a custom ATS?** Open an issue in the **Issues** tab. Custom integrations are available on request.

# Actor input Schema

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

One company per line. Accepted: an ATS board URL (https://boards.greenhouse.io/gitlab, https://jobs.lever.co/spotify, https://jobs.ashbyhq.com/linear, https://apply.workable.com/acme, https://careers.smartrecruiters.com/Acme, https://acme.recruitee.com, https://acme.jobs.personio.de), a shorthand like greenhouse:gitlab or lever:spotify, a careers page URL (https://linear.app/careers) or a company domain (figma.com). For careers pages and domains the ATS is detected automatically, which is charged as one "company detected" event.

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

Keep jobs whose title, department or team contains any of these words. Case-insensitive. Words of 3 letters or less (AI, QA, Go) must match a whole word. Empty = all jobs.

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

Drop jobs whose title contains any of these words, for example intern, senior, director.

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

Keep jobs whose location contains any of these texts, for example Berlin, Germany, London, United States. Add "remote" to also keep remote jobs.

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

Keep jobs whose department or team contains any of these texts, for example Engineering, Sales, Marketing.

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

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

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

Keep jobs first published in the last N days. Jobs without a publish date are dropped when this is set. 0 = no limit.

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

Remember job IDs per company in a named key-value store and return only jobs not returned in earlier runs. Use it with a daily or hourly schedule. You pay only for new jobs.

## `stateStoreName` (type: `string`):

Named key-value store that keeps seen job IDs for "Only new jobs". Use a different name per schedule or task to keep their histories apart. Letters a-z, digits and dashes.

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

Add the full job description as clean plain text (descriptionText). Turn off for smaller, faster runs.

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

Also add the original description HTML (descriptionHtml).

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

Cap the number of jobs returned per company after filters. The newest jobs are kept. 0 = no limit.

## `probeSlugs` (type: `boolean`):

When a careers page shows no ATS link, try the company name from the domain (stripe.com -> stripe) on each supported ATS API. Finds most JavaScript-only careers pages. Can match a different company with the same name, check companySlug in the output.

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

How many companies are processed at the same time. Requests to each ATS host are always limited to 2 per second.

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/gitlab",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/linear"
  ],
  "keywords": [],
  "excludeKeywords": [],
  "locations": [],
  "departments": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "onlyNew": false,
  "stateStoreName": "ats-jobs-seen",
  "includeDescription": true,
  "includeDescriptionHtml": false,
  "maxJobsPerCompany": 20,
  "probeSlugs": true,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

All jobs, one row per job.

## `salary` (type: `string`):

No description

## `details` (type: `string`):

No description

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

Detected ATS, board slug and job counts for every input, including the ones that could not be resolved.

# 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://job-boards.greenhouse.io/gitlab",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/linear"
    ],
    "maxJobsPerCompany": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("rod_analytics/multi-ats-jobs-api").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://job-boards.greenhouse.io/gitlab",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/linear",
    ],
    "maxJobsPerCompany": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("rod_analytics/multi-ats-jobs-api").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://job-boards.greenhouse.io/gitlab",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/linear"
  ],
  "maxJobsPerCompany": 20
}' |
apify call rod_analytics/multi-ats-jobs-api --silent --output-dataset

```

## MCP server setup

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

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/D7zdynLWE59QfHkhd/builds/EYh2kQq9Mid6m2CTK/openapi.json
