# Greenhouse, Lever & Ashby Jobs Scraper (8 ATS) (`cleanfeed-data/multi-ats-jobs`) Actor

Scrape every open job from company career pages on Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Teamtailor, Recruitee & Personio. Auto-detects the ATS, uses official public APIs only. Salaries, remote flag, filters and new-job alerts as clean JSON.

- **URL**: https://apify.com/cleanfeed-data/multi-ats-jobs.md
- **Developed by:** [CleanFeed Data](https://apify.com/cleanfeed-data) (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 $3.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

## Greenhouse, Lever & Ashby Jobs Scraper (8 ATS)

**Scrape every open job from any company's careers page, from Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Teamtailor, Recruitee and Personio, as clean, normalized JSON.** Give it a list of companies (names, domains, careers-page URLs or job-board links). It works out which applicant tracking system (ATS) each company uses, pulls the postings from that ATS's **official public job-board API or feed**, and returns one consistent record per job with title, department, location, remote flag, salary, posting date and apply link. Turn on **Only new jobs** and schedule it, and you get a feed of new postings since the last run.

👉 **Try it in one click:** the input is prefilled with Stripe, Spotify, OpenAI and Hugging Face and capped at 20 jobs, so a first run takes a few seconds and costs about $0.07.

### What does this jobs scraper do?

- ✅ **8 ATSs in one dataset**, one schema, no matter where the job is hosted
- ✅ **Auto-detects the ATS** from a company name, domain or careers page
- ✅ **Filters before you pay:** keywords, locations, remote only, departments, employment types, posted within N days
- ✅ **Only-new-jobs mode** for daily hiring alerts and job-board refreshes
- ✅ **Salary ranges** when the ATS or the job text publishes them
- ✅ Runs on the Apify platform: API, scheduling, webhooks, and Slack, Google Sheets, Zapier, Make or n8n integrations

| Supported ATS | Public source used | Description | Salary |
|---|---|---|---|
| **Greenhouse** | Job Board API (`boards-api.greenhouse.io`) | ✅ | ✅ when the board publishes pay ranges or the description states one |
| **Lever** | Postings API (`api.lever.co/v0/postings`) | ✅ | ✅ `salaryRange` or from the description |
| **Ashby** | Public Job Posting API (`api.ashbyhq.com/posting-api`) | ✅ | ✅ structured compensation |
| **Workable** | Public jobs widget API (`apply.workable.com/api/v1/widget`) | ✅ | from the description |
| **SmartRecruiters** | Posting API (`api.smartrecruiters.com/v1/companies/{id}/postings`) | ✅ | ✅ salary custom fields or from the description |
| **Teamtailor** | Public career-site RSS feed (`/jobs.rss`) | ✅ | from the description |
| **Recruitee** | Careers Site API (`{company}.recruitee.com/api/offers`) | ✅ | ✅ when published |
| **Personio** | Public job XML feed (`{company}.jobs.personio.de/xml`) | ✅ | from the description |

No login, no API keys, no proxies, no browser. Runs are fast and cheap: thousands of jobs in seconds.

### Use cases

- **Job boards and aggregators:** fill a niche board (e.g. "remote AI jobs at Series A–C startups") from hundreds of company boards and refresh it daily.
- **Recruiting and sales intelligence:** see who is hiring for what. New engineering or sales roles are strong buying and growth signals for outreach.
- **Talent-market research:** track hiring by department, location and remote share across a peer group or a portfolio.
- **Job seekers and communities:** get alerts when target companies post roles that match your keywords (pipe the output into Slack or email with Apify integrations).
- **VC and portfolio tracking:** watch headcount plans across portfolio companies.

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

1. Click **Try for free** (or **Start**) and open the **Input** tab.
2. In **Companies**, paste one company per line: a job-board link like `https://jobs.lever.co/spotify`, a hint like `greenhouse:stripe`, a careers page, or just a name.
3. Optionally add **Keywords** (e.g. `engineer`), **Locations** (e.g. `Remote`) or **Posted within (days)**.
4. Set **Max results** to 0 for every job, or keep a cap while you test.
5. Click **Start**. Download the jobs as JSON, CSV or Excel, or read them through the API.
6. To get alerts, turn on **Only new jobs since last run** and add a schedule (for example daily at 8:00).

### How it works

1. **Resolve each company.** For each entry, in this order:
   - an explicit hint like `greenhouse:stripe` is used as is;
   - a known ATS URL (e.g. `https://jobs.lever.co/palantir`) is parsed;
   - a company website or careers URL is fetched **once** to find the embedded ATS board (e.g. a Greenhouse embed on `figma.com/careers`);
   - otherwise the public ATS APIs are probed with slugs derived from the name or domain (`Hugging Face` → `huggingface`).
2. **Fetch postings** from that ATS's public endpoint, with polite per-host rate limiting and automatic retries (exponential backoff, honours `Retry-After`).
3. **Normalize, filter, diff.** Every job is mapped to one schema, filtered, and compared with the previous run's state to set `isNew`.
4. **Errors stay per company.** If one company can't be resolved, the others still finish. Each error is listed in the `RUN_SUMMARY` record.

### Input

| Field | Type | Description |
|---|---|---|
| `companies` | string\[] | **Required.** One per line: `Stripe`, `notion.so`, `https://www.figma.com/careers/`, `https://jobs.lever.co/palantir`, or `ats:slug` (e.g. `ashby:openai`, `smartrecruiters:Experian`, `teamtailor:polestar`, `personio:tado`). |
| `companiesAdvanced` | object\[] | Optional: `{ "name", "url", "ats", "slug" }` objects. |
| `keywords` / `excludeKeywords` | string\[] | Title must contain any keyword / none of the excluded ones (case-insensitive). |
| `searchInDescription` | boolean | Also match keywords in the description. |
| `locations` | string\[] | Location contains any of these (e.g. `London`, `Germany`, `Remote`). |
| `remoteOnly` | boolean | Only jobs flagged as remote. |
| `departments` | string\[] | Department or team contains any of these. |
| `employmentTypes` | string\[] | e.g. `full-time`, `contract`, `intern`. |
| `postedWithinDays` | integer | Only jobs published in the last N days. |
| `onlyNew` | boolean | Output only jobs not returned by earlier runs (see Monitoring). |
| `stateStoreName` | string | Named key-value store for the seen-jobs state (default `multi-ats-jobs-state`). |
| `maxResults` / `maxResultsPerCompany` | integer | Caps (0 = unlimited). `maxResults` is **split fairly across companies**: each company gets an equal share, and any share a small board doesn't need goes to the bigger ones. Example: 1,000 over boards with 31, 299, 465, 746 and 813 matching jobs gives 31, 243, 242, 242, 242. |
| `includeDescription` | boolean | Include the description as text and HTML (default `true`). |
| `scanCareerPage` | boolean | Allow one fetch of a company's careers page to detect its ATS (default `true`). |
| `maxConcurrency`, `requestDelayMs` | integer | How many companies run in parallel, and the minimum gap between requests to one host. |

#### Example input

```json
{
  "companies": [
    "greenhouse:stripe",
    "https://jobs.lever.co/spotify",
    "ashby:openai",
    "Hugging Face",
    "https://www.figma.com/careers/",
    "smartrecruiters:Experian",
    "teamtailor:polestar",
    "recruitee:bunq",
    "personio:1komma5grad"
  ],
  "keywords": ["engineer", "developer"],
  "excludeKeywords": ["intern"],
  "locations": ["United States", "Remote"],
  "onlyNew": true,
  "maxResults": 2000
}
```

### Output

Each job is one dataset item:

```json
{
  "company": "Ramp",
  "companySlug": "ramp",
  "ats": "ashby",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "remote": true,
  "workplaceType": "hybrid",
  "employmentType": "Full-Time",
  "postedAt": "2026-04-07T17:12:35.753Z",
  "updatedAt": null,
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "descriptionText": "ABOUT RAMP\n\nRamp is building the smart infrastructure for finance teams…",
  "descriptionHtml": "<h1><strong>About Ramp</strong></h1>…",
  "salary": { "min": 211400, "max": 290600, "currency": "USD", "interval": "year", "text": "$211.4K - $290.6K", "source": "ats" },
  "isNew": true,
  "firstSeenAt": "2026-10-31T02:27:30.668Z",
  "scrapedAt": "2026-10-31T02:27:30.668Z"
}
```

Field notes:

- `remote`: `true` when the ATS says remote work is possible (Ashby/Lever/Workable/SmartRecruiters/Recruitee/Teamtailor flags). For Greenhouse and Personio, which have no flag, it is inferred from "remote" in the location or title. `workplaceType` (`remote` / `hybrid` / `onsite`) is filled when the ATS provides it.
- `salary.source`: `ats` means the ATS published it as structured data. `description` means it was parsed from the job text and is best effort.
- `postedAt` / `updatedAt` are ISO-8601 UTC. Not every ATS exposes both. Empty fields are `null`, never left out.
- Personio items also include `seniority` and `legalEntity`.

The Output tab has three views: **Jobs overview**, **New jobs only** and **Full detail**. Each run also writes a `RUN_SUMMARY` record to the default key-value store, with per-company status, detected ATS, counts and errors.

### Monitoring new jobs (`onlyNew`)

Run the Actor on a **schedule** (e.g. daily) with `onlyNew: true`:

- The **first** run for a company returns all matching jobs (the baseline) and stores their IDs in the named key-value store `stateStoreName`.
- Later runs return only jobs whose IDs haven't been returned before. When a job closes, its ID is dropped from the state.
- Every item has `isNew` and `firstSeenAt`, even with `onlyNew` off.
- For several independent monitors (different filters or company lists), give each saved task its own `stateStoreName`.

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

You pay only for what you get:

| Event | Price |
|---|---|
| Actor start (once per run) | **$0.005** |
| Job posting returned | **$0.003** per job ($3 per 1,000 jobs) |

Examples: monitoring 50 companies daily with ~30 new matching jobs a day costs about **$0.095/day**. A one-off pull of 5,000 jobs costs about **$15.01**. Set `maxResults` (split fairly across your companies), or a maximum cost per run in the run options, to cap spend. The Actor stops cleanly at the limit and never charges for jobs it didn't return.

### FAQ

**Is this legal? Does it scrape websites?**
It only uses public endpoints that the ATS vendors provide so job postings can be shown on career sites and job boards (Greenhouse Job Board API, Lever Postings API, Ashby Public Job Posting API, SmartRecruiters Posting API, the Workable jobs widget, Teamtailor RSS, Recruitee Careers Site API, the Personio XML feed). It never logs in, never uses private or authenticated APIs, never bypasses bot protection, and collects no personal data about candidates. If you pass a company website, it fetches that one public page only to find the job-board link. You are responsible for how you use the data. Respect each company's terms and applicable law, such as rules on republishing and data protection.

**My company wasn't detected.** Pass the job-board URL (e.g. `https://jobs.ashbyhq.com/acme`) or an explicit hint (`ashby:acme`). Name-based probing can't guess every slug.

**Can I get Workday, iCIMS, Taleo, BambooHR, SuccessFactors or Jobvite?** Not right now. Those systems don't offer a documented public, unauthenticated postings API, so this Actor doesn't touch them. You'll get a clear "unsupported" error for those hints.

**Why do SmartRecruiters runs take longer?** Its listing API doesn't include descriptions, so the Actor makes one extra request per job you actually receive. Set `includeDescription: false` for a fast summary feed.

**How fresh is the data?** It's live: every run reads the ATS at that moment.

**Can I search all jobs on Greenhouse or Lever by keyword?** No. These ATS APIs are per company, so you give the companies you care about and the Actor filters their jobs. Many users keep a list of 50–500 target companies and run it daily with `onlyNew`.

**How do I find a company's ATS slug?** Open its careers page and click a job. The link usually shows it, e.g. `jobs.ashbyhq.com/ramp` means `ashby:ramp` and `boards.greenhouse.io/stripe` means `greenhouse:stripe`. You can also paste that link directly.

**How many jobs can I get on the free plan?** Apify's free plan includes $5 of monthly usage, which covers roughly 1,600 job postings with this Actor.

**Can I integrate it?** Yes. Use the Apify API, webhooks, or integrations (Slack, Google Sheets, Zapier, Make, n8n) and call it from code with `apify-client` in JS or Python.

### More scrapers from cleanfeed-data

All of our Actors use only official, documented public APIs and feeds, charge only for results, and have a new-items-only monitoring mode.

| Actor | What it does |
|---|---|
| [App Store Reviews Scraper](https://apify.com/cleanfeed-data/app-store-reviews) | Apple App Store reviews from many countries, with sentiment and topic tags |
| [Bluesky Scraper](https://apify.com/cleanfeed-data/bluesky-data) | Bluesky posts, profiles, reply threads and followers via the official public API |

### Legal note

This Actor reads only publicly available job postings through public ATS APIs and feeds that the ATS providers document. It doesn't scrape content behind logins, doesn't circumvent technical measures, and doesn't process candidate personal data. Trademarks belong to their owners. This Actor is not affiliated with or endorsed by any ATS vendor.

# Actor input Schema

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

One company per line. Accepts a company name (<code>Stripe</code>), a domain (<code>notion.so</code>), a careers-page URL (<code>https://www.figma.com/careers/</code>), a job-board URL (<code>https://jobs.lever.co/spotify</code>, <code>https://boards.greenhouse.io/stripe</code>, <code>https://jobs.ashbyhq.com/openai</code>), or a hint <code>ats:slug</code> (e.g. <code>greenhouse:stripe</code>, <code>ashby:openai</code>, <code>lever:spotify</code>, <code>smartrecruiters:Experian</code>). Job-board URLs and hints are the fastest and most reliable; plain names are guessed and may not be found.

## `companiesAdvanced` (type: `array`):

Optional. Objects with any of: <code>name</code>, <code>url</code> (careers page or domain), <code>ats</code> (greenhouse | lever | ashby | workable | smartrecruiters | teamtailor | recruitee | personio), <code>slug</code> (board token). Example: <code>\[{"name": "Ramp", "ats": "ashby", "slug": "ramp"}]</code>

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

Keep jobs whose title contains ANY of these words (case-insensitive), e.g. <code>engineer</code>, <code>data</code>, <code>sales</code>. Leave empty to get all jobs.

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

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

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

Apply keyword filters to the description too, not just the title. For SmartRecruiters this requires one extra request per job.

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

Keep jobs whose location contains ANY of these substrings (e.g. "London", "Germany", "Remote").

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

Keep only jobs flagged as remote by the ATS (or with "remote" in the location/title when the ATS has no flag).

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

Keep jobs whose department or team contains ANY of these (e.g. "Engineering", "Sales").

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

Keep jobs whose employment type contains ANY of these (e.g. "full-time", "contract", "intern"). Jobs with no employment type are excluded when this is set.

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

Keep only jobs first published within the last N days, e.g. 7 for a weekly digest (0 = no limit). Jobs without a publish date are kept.

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

Diff mode for scheduled runs: output only jobs not returned by a previous run with the same state store. The first run for a company returns all matching jobs (baseline). Every item has an isNew flag regardless of this setting.

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

Named key-value store that remembers seen jobs between runs. Use a different name for each saved task/filter set you monitor separately.

## `maxResults` (type: `integer`):

Stop after this many jobs across all companies (0 = unlimited). Prefilled to 20 so your first test run is quick and cheap; set 0 to get every job. The total is split fairly: each company gets an equal share, and any share a small board doesn't need goes to the others. Also caps your cost ($0.003 per job).

## `maxResultsPerCompany` (type: `integer`):

Cap per company (0 = unlimited). Useful when one huge board would otherwise dominate.

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

Include full description (plain text + HTML). Turn off for smaller, faster output.

## `scanCareerPage` (type: `boolean`):

When you pass a company website/careers URL, fetch that one page to find the embedded ATS board. If off, only public ATS APIs are probed.

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

How many companies to process at the same time. Requests to the same ATS host are still rate-limited.

## `requestDelayMs` (type: `integer`):

Polite rate limiting per API host.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:stripe",
    "https://jobs.lever.co/spotify",
    "ashby:openai",
    "Hugging Face",
    "notion.so"
  ],
  "companiesAdvanced": [],
  "searchInDescription": false,
  "remoteOnly": false,
  "postedWithinDays": 0,
  "onlyNew": false,
  "stateStoreName": "multi-ats-jobs-state",
  "maxResults": 20,
  "maxResultsPerCompany": 0,
  "includeDescription": true,
  "scanCareerPage": true,
  "maxConcurrency": 4,
  "requestDelayMs": 300
}
```

# Actor output Schema

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

All job postings returned by this run, one item per job. Opens in the 'Jobs overview' view; other views are available in the selector.

## `jobsMonitoring` (type: `string`):

The same items with the monitoring columns (isNew, firstSeenAt, applyUrl). Use with onlyNew for new-job alerts.

## `jobsFull` (type: `string`):

The same items with every normalized field, including descriptions and salary.

## `runSummary` (type: `string`):

JSON record with per-company results: input, detected ATS and slug, detection method, jobs found/matched/new, fair-share quota, jobs output, error message, timing, and HTTP stats.

# 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:stripe",
        "https://jobs.lever.co/spotify",
        "ashby:openai",
        "Hugging Face"
    ],
    "maxResults": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("cleanfeed-data/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": [
        "greenhouse:stripe",
        "https://jobs.lever.co/spotify",
        "ashby:openai",
        "Hugging Face",
    ],
    "maxResults": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("cleanfeed-data/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": [
    "greenhouse:stripe",
    "https://jobs.lever.co/spotify",
    "ashby:openai",
    "Hugging Face"
  ],
  "maxResults": 20
}' |
apify call cleanfeed-data/multi-ats-jobs --silent --output-dataset

```

## MCP server setup

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