# Multi-ATS Jobs Scraper (Greenhouse, Lever, Ashby) (`devilscrapes/multi-ats-jobs-scraper`) Actor

Scrape job postings from Greenhouse, Lever, and Ashby in one run. Pass a list of company slugs and get a normalised dataset — title, location, department, URL, and description — across all three ATS platforms.

- **URL**: https://apify.com/devilscrapes/multi-ats-jobs-scraper.md
- **Developed by:** [DevilScrapes](https://apify.com/devilscrapes) (community)
- **Categories:** Jobs
- **Stats:** 2 total users, 0 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/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

<div align="center">
  <img src=".actor/icon.svg" width="160" alt="Devil Scrapes mark" />

## Multi-ATS Jobs Scraper (Greenhouse, Lever, Ashby)

**$1.50 / 1 000 results**  ·  pay only for results  ·  no credit card to try

*We run the gauntlet across three ATS platforms so your jobs pipeline doesn't have to.*

One Actor, one company list — bare slugs, careers URLs, or explicit ATS types, mixed freely. We work out which of the three platforms each company runs on and hand back one normalised row per posting: title, department, team, location, remote flag, salary range where the ATS exposes it, and full description.

</div>

***

### 🎯 What this scrapes

Greenhouse, Lever, and Ashby together power the hiring pipelines of thousands of tech companies — but each platform speaks its own API dialect, and you don't always know upfront which one a given company runs on. This Actor figures that out for you: point it at a company, it auto-detects the platform, normalises the fields into one consistent schema, and writes the results to a typed Apify dataset.

Give it a bare board slug (`"stripe"`), a full careers URL (`"https://jobs.lever.co/palantir"`), or an explicit `{atsType, companySlug}` object if you already know the platform — mix all three forms freely in a single run. That auto-detection is the part the single-ATS wrappers on the Store don't do.

Supported platforms:

| ATS | API endpoint style |
|---|---|
| **Greenhouse** | REST — `boards-api.greenhouse.io/v1/boards/{slug}/jobs` |
| **Lever** | REST — `api.lever.co/v0/postings/{slug}?mode=json` |
| **Ashby** | REST — `api.ashbyhq.com/posting-api/job-board/{slug}` |

### 🔥 What we handle for you

- 🛡️ **Browser fingerprint rotation** — requests impersonate real browser TLS handshakes so the target sees a browser, not a bot.
- 🌐 **Residential proxy rotation** via Apify Proxy — fresh session and exit IP when upstream pushes back.
- 🔁 **Retries with exponential backoff** on `408 / 429 / 5xx` — up to 5 attempts, `Retry-After` honoured.
- 🧱 **Rate-limit-aware pacing** — we slow down rather than get banned; partial successes surface via a clear status message.
- 🧊 **Normalised, typed dataset rows** — Pydantic-validated, ISO-8601 timestamps, stable IDs across all three ATS platforms.
- 💰 **Pay-Per-Event pricing** — you pay only for job postings that land in your dataset.

### 💡 Use cases

- **Talent intelligence pipelines** — track hiring velocity at target companies; diff successive runs to spot new headcount or sudden freezes.
- **Competitive analysis** — monitor which roles a competitor is hiring for and in which locations.
- **Job aggregators** — power a niche job board (e.g. "remote fintech jobs") without scraping fragile HTML pages.
- **Recruitment automation** — feed a CRM or ATS of your own with normalised job data from multiple sources in one shot.
- **Market research** — identify which departments are growing fastest across an industry by counting roles by department field.
- **Targeted talent mapping** — set `departmentFilter`, `locationFilter`, or `remoteOnly` on the run itself so you only pay for the postings you actually want, instead of filtering the whole dataset afterward.
- **No-code workflows (n8n / Make / Zapier)** — connect via the Apify integration node; pipe new postings to a Slack channel or Google Sheet without writing code.

### ⚙️ How to use it

1. Click **Try for free** at the top of the page.
2. In the **Companies** field, enter a JSON array — mix bare slugs, careers URLs, and explicit `{atsType, companySlug}` objects freely:
   ```json
   [
     {"atsType": "greenhouse", "companySlug": "stripe"},
     "https://jobs.lever.co/palantir",
     "https://jobs.ashbyhq.com/ramp"
   ]
   ```
   Bare slugs and URLs get auto-detected; the explicit object skips detection entirely.
3. Optionally set **Department filter**, **Location filter**, or **Remote only** to narrow results before they're written (and billed).
4. Adjust **Max items per company** if you want a quick sample; leave at `0` for all postings.
5. Toggle **Include description** off if you only need metadata (faster, smaller dataset).
6. Click **Start**. Results stream into the run's dataset in real time.
7. Export from **Storage → Dataset** as JSON, CSV, or Excel — or fetch via the Apify API.

For no-code workflows: install the [Apify node for n8n](https://n8n.io/integrations/apify/) or the [Apify module for Make](https://www.make.com/en/integrations/apify), connect your Apify token, and point it at `DevilScrapes/multi-ats-jobs-scraper`.

### 📥 Input

| Field | Type | Required | Default | Notes |
|---|---|:--:|---|---|
| `companies` | `array` | **yes** | — | 1–1000 entries. Each is a bare slug (auto-detected), a full careers/ATS URL (auto-detected), or a `{atsType, companySlug}` object (skips detection). Mix freely. |
| `maxItemsPerCompany` | `integer` | no | `0` | Max postings per company, applied after filtering. `0` = unlimited. |
| `includeDescription` | `boolean` | no | `true` | Fetch and store full job description HTML in `description_html`. |
| `departmentFilter` | `string` | no | `null` | Case-insensitive substring match against `department` or `team`. |
| `locationFilter` | `string` | no | `null` | Case-insensitive substring match against `location`. |
| `remoteOnly` | `boolean` | no | `false` | Keep only postings where `remote == true`; unknown-remote rows are excluded. |
| `proxyConfiguration` | `object` | no | `{"useApifyProxy": true}` | Apify Proxy config. We rotate sessions to absorb rate limits. |

**Example input:**

```json
{
  "companies": [
    {"atsType": "greenhouse", "companySlug": "stripe"},
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "maxItemsPerCompany": 50,
  "includeDescription": true,
  "departmentFilter": null,
  "locationFilter": null,
  "remoteOnly": false,
  "proxyConfiguration": {"useApifyProxy": true}
}
```

### 📤 Output

One dataset row per job posting. All fields are present on every row; optional fields are `null` when the ATS doesn't expose them — salary in particular is Ashby-only, see **Limitations**.

| Field | Type | Notes |
|---|---|---|
| `company` | `string` | Company slug from your input, lowercased |
| `ats` | `string` | `"greenhouse"`, `"lever"`, or `"ashby"` — auto-detected or explicit |
| `job_id` | `string` | Posting ID from the ATS |
| `title` | `string` | Job title |
| `department` | `string \| null` | Department name |
| `team` | `string \| null` | Team name, when distinct from department (Lever/Ashby only — Greenhouse doesn't split the two) |
| `location` | `string \| null` | Location string as reported by the ATS |
| `remote` | `boolean \| null` | `true` = remote eligible; `false` = not; `null` = unknown |
| `salary_min` | `integer \| null` | Minimum salary. **Ashby only** — Greenhouse and Lever don't publish a structured salary field |
| `salary_max` | `integer \| null` | Maximum salary. **Ashby only** |
| `salary_currency` | `string \| null` | ISO 4217 currency code for the salary fields. **Ashby only** |
| `url` | `string` | Canonical link to the job posting |
| `posted_at` | `string \| null` | ISO-8601 creation timestamp |
| `updated_at` | `string \| null` | ISO-8601 last-updated timestamp. Only Greenhouse exposes this; Lever and Ashby leave it `null` |
| `description_html` | `string \| null` | Full description HTML (null if `includeDescription` is false) |

**Example output row:**

```json
{
  "company": "ramp",
  "ats": "ashby",
  "job_id": "3b1c2e9a-0000-0000-0000-000000000000",
  "title": "Senior Software Engineer, Payments",
  "department": "Engineering",
  "team": "Payments",
  "location": "New York City",
  "remote": false,
  "salary_min": 211400,
  "salary_max": 290600,
  "salary_currency": "USD",
  "url": "https://jobs.ashbyhq.com/ramp/3b1c2e9a-0000-0000-0000-000000000000",
  "posted_at": "2026-05-02T18:11:00.000Z",
  "updated_at": null,
  "description_html": "<p>About the role...</p>"
}
```

### 💰 Pricing

This Actor uses **Pay-Per-Event** pricing — you pay only for job postings written to your dataset.

| Event | Price |
|---|---|
| Actor start (one-off warm-up) | $0.005 |
| Per result emitted | $0.0015 |

**Effective rate: $1.50 / 1 000 results.**

A run that collects 500 job postings across three companies costs approximately $0.75 in result charges plus the $0.005 start fee. Empty runs (no matching postings) cost only the start fee.

### 🚧 Limitations

- **Salary is Ashby-only, and we won't fake the rest.** Only Ashby's public API exposes a structured salary field. Greenhouse and Lever don't publish comp data structurally — pulling a number out of their free-text descriptions would mean unreliable regex guesses, so `salary_min` / `salary_max` / `salary_currency` are honestly `null` for those two platforms instead.
- **Company slugs or URLs must exist** on the chosen ATS. Passing an incorrect one returns zero results for that company (logged as a warning, not a fatal error).
- **Auto-detect tie-breaks on ambiguous slugs.** If the same bare slug happens to resolve on more than one platform (rare — not observed in practice), we keep the one with more open postings and log a warning. Pass an explicit `{atsType, companySlug}` object if you need to force a specific platform.
- **Ashby's job-board schema** may evolve without notice — we track public API changes but cannot guarantee instant compatibility if Ashby renames or drops fields.
- **`description_html` field** can contain raw HTML. Strip tags on your end if plain text is required.
- **Pagination** is handled automatically, but very large boards (thousands of postings) may produce long run times at the default proxy tier.
- The Actor does **not** support authenticated ATS boards (private job listings require employer credentials).

### ❓ FAQ

**Can I scrape multiple companies in one run?**
Yes — that's the design. The `companies` array can hold up to 1 000 entries, mixing bare slugs, careers URLs, and explicit `{atsType, companySlug}` objects. They run sequentially; use `maxItemsPerCompany` to cap each one.

**Do I need to know which ATS a company uses before I start?**
No. Paste a bare company slug or its careers URL and we detect the platform for you. If you already know the ATS, the explicit `{atsType, companySlug}` form skips detection entirely and runs slightly faster.

**Does every job posting include a salary range?**
No — and we'd rather tell you that up front than have you find out from a column full of nulls. Only Ashby publishes a structured salary field through its public API, so `salary_min` / `salary_max` / `salary_currency` come back `null` for Greenhouse and Lever postings. If salary data is a hard requirement, point the Actor at Ashby-hosted employers.

**Does this work with private / internal job boards?**
No. The Actor hits the same public API endpoints visible in a browser without authentication. Internal-only boards require employer credentials that this Actor does not support.

**Can I schedule this to run daily?**
Yes. Click **Schedule** in the Apify Console and set a cron expression. Each run produces a fresh snapshot — diff two runs to find new or closed postings.

**Can I export to Google Sheets or Airtable?**
Yes, via the Apify Console integrations or the [n8n](https://n8n.io/integrations/apify/) / [Make](https://www.make.com/en/integrations/apify/) connectors. Point them at `DevilScrapes/multi-ats-jobs-scraper` and map the output fields.

### 🤝 Your feedback

Found a company slug that returns wrong results? Hit an ATS quirk we don't handle? Open a support ticket or leave a review on the Apify Store listing — we read every one.

***

### Changelog

#### 0.2 — 2026-07-20

- **Auto-detect** — `companies` now accepts a bare board slug or a full careers/ATS URL, not just an explicit `{atsType, companySlug}` object (which still works and skips detection). Mix all three forms in one run.
- **New fields**: `team`, `salary_min`, `salary_max`, `salary_currency` (Ashby only).
- **Renamed fields**: `source_ats` → `ats`, `description` → `description_html`.
- **Dropped field**: `employment_type` (not consistently available across all three platforms).
- **New filters**: `departmentFilter`, `locationFilter`, `remoteOnly` — narrow results before they're written and billed.
- Migrated the HTTP layer from `httpx` to `curl-cffi` with browser-fingerprint impersonation and rotation (org anti-blocking standard).
- Corrected a stale README claim — Ashby's job-board API is REST, not GraphQL.

#### 0.1 — 2026-06-07

- Initial release. Greenhouse, Lever, and Ashby scrapers via public JSON APIs. Normalised typed dataset rows with Pydantic validation.

# Actor input Schema

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

List of companies to scrape. Each entry may be: (1) a bare board slug, e.g. <code>"stripe"</code> — auto-detected via a concurrent probe of all three ATSes; (2) a full careers/ATS URL, e.g. <code>"https://jobs.lever.co/palantir"</code> — the ATS is regex-extracted, zero probes; or (3) a legacy object <code>{"atsType": "greenhouse", "companySlug": "stripe"}</code> that names the ATS explicitly and skips auto-detection entirely.

## `maxItemsPerCompany` (type: `integer`):

Maximum number of job postings to collect per company, applied after departmentFilter/locationFilter/remoteOnly. Set to <code>0</code> (default) to collect all available postings.

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

When enabled, the full job description (HTML/text) is fetched and stored in the <code>description\_html</code> field. Disable to reduce dataset size and run time.

## `departmentFilter` (type: `string`):

Case-insensitive substring match against the normalised <code>department</code> or <code>team</code> field. Leave empty to skip.

## `locationFilter` (type: `string`):

Case-insensitive substring match against the normalised <code>location</code> field. Leave empty to skip.

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

When enabled, keep only postings where <code>remote == true</code>. Postings with an unknown remote status are excluded.

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

Apify Proxy configuration. We rotate sessions and exit IPs to absorb rate limits across all three ATS platforms.

## Actor input object example

```json
{
  "companies": [
    {
      "atsType": "greenhouse",
      "companySlug": "stripe"
    },
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "maxItemsPerCompany": 0,
  "includeDescription": true,
  "remoteOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `datasetItems` (type: `string`):

No description

## `datasetItemsCsv` (type: `string`):

No description

## `datasetView` (type: `string`):

No description

# 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": [
        {
            "atsType": "greenhouse",
            "companySlug": "stripe"
        },
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp"
    ],
    "maxItemsPerCompany": 0,
    "includeDescription": true,
    "remoteOnly": false,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("devilscrapes/multi-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": [
        {
            "atsType": "greenhouse",
            "companySlug": "stripe",
        },
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp",
    ],
    "maxItemsPerCompany": 0,
    "includeDescription": True,
    "remoteOnly": False,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("devilscrapes/multi-ats-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    {
      "atsType": "greenhouse",
      "companySlug": "stripe"
    },
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "maxItemsPerCompany": 0,
  "includeDescription": true,
  "remoteOnly": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call devilscrapes/multi-ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=devilscrapes/multi-ats-jobs-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/rcJ2lTCjkJIHTUJUF/builds/ZDDqOjJhv9o0gVnbU/openapi.json
