# Greenhouse, Lever, Ashby & Recruitee Jobs Scraper (`markdemian/ats-jobs-scraper`) Actor

Scrape all open jobs from company career pages on Greenhouse, Lever, Ashby and Recruitee using their official public job-board APIs. One clean schema, salary ranges where published, keyword, location and remote filters, plus new-jobs-only monitoring. No login, no proxies.

- **URL**: https://apify.com/markdemian/ats-jobs-scraper.md
- **Developed by:** [Mark Demian](https://apify.com/markdemian) (community)
- **Categories:** Jobs, Automation
- **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 results

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 & Recruitee Jobs Scraper

### What does this ATS jobs scraper do?

**Greenhouse, Lever, Ashby & Recruitee Jobs Scraper** collects **every open job** from company career pages hosted on the four most common applicant tracking systems (ATS). You give it a list of companies (career-page URLs or names), and you get back **one clean, normalized dataset**: title, department, team, location, remote/hybrid status, employment type, **salary range where the company publishes one**, posting date, description, and the public job and apply URLs.

It reads the ATS vendors' **official public job-board APIs**, the same JSON feeds that power companies' own careers pages. That makes it **fast** (about 4,900 jobs from 26 companies in about 20–40 seconds in our tests), **reliable** (no HTML parsing, so a careers-page redesign doesn't break it) and **cheap to run**. It needs no login, no cookies, no proxies and no browser.

**Try it now:** click **Start**. The example input pulls 20 jobs each from Figma (Greenhouse), Palantir (Lever) and Ramp (Ashby).

#### Main features

- 🏢 **4 ATS platforms in one schema:** Greenhouse, Lever, Ashby, Recruitee
- 🔎 **Auto-detection:** paste a careers URL, `ats:slug`, or just a company slug
- 💰 **Salary extraction:** structured pay ranges from the ATS (including Greenhouse's pay-transparency ranges), plus ranges found in the job text, and **yearly min/max** so hourly, monthly and annual pay are comparable
- 🎯 **Filters:** title keywords (include and exclude), location keywords, remote/hybrid only, posted in the last N days
- 🔔 **Monitor mode:** "Only new jobs since last run" turns a scheduled run into a new-job alert feed
- 🛡️ **Privacy by design:** recruiter emails and phone numbers are stripped from descriptions, and contact fields are never collected
- 🤝 **Polite:** respects `robots.txt`, waits at least 1 second between requests to the same host, and backs off on errors

Because it runs on Apify, you also get **scheduling**, a **REST API**, **webhooks** and **integrations** (Google Sheets, Slack, Zapier, Make, n8n, Airbyte and more), plus exports to JSON, CSV, Excel and XML.

### What can you use it for?

- **Niche job boards and newsletters:** build feeds like "remote Rust jobs" or "Toronto fintech jobs" from the companies you choose.
- **Hiring signals and market research:** see which teams, locations and roles competitors, customers or portfolio companies are hiring for.
- **Recruiting and job-search automation:** get daily alerts for new matching roles at your target companies.
- **Salary benchmarking:** collect published pay ranges (common in pay-transparency regions such as NY, CA, CO and WA).

### Supported systems

| ATS | Accepted input examples | Data source (public API) |
|---|---|---|
| **Greenhouse** | `https://boards.greenhouse.io/figma`, `https://job-boards.greenhouse.io/figma`, `greenhouse:figma` | `boards-api.greenhouse.io` Job Board API |
| **Lever** | `https://jobs.lever.co/palantir`, `lever:palantir` | `api.lever.co` Postings API |
| **Ashby** | `https://jobs.ashbyhq.com/ramp`, `ashby:ramp` | `api.ashbyhq.com` Job Postings API |
| **Recruitee** | `https://bunq.recruitee.com`, `recruitee:bunq` | `<company>.recruitee.com/api/offers` |

A **bare slug** such as `figma` is tried on Greenhouse, then Lever, Ashby and Recruitee, in that order, and the first non-empty board is used. Use an explicit `ats:slug` or a URL when you know the ATS. It is faster and avoids picking the wrong board.

A company's own careers domain (for example `stripe.com/jobs`) is **not** accepted directly. Use the ATS board URL or slug instead (see the FAQ).

### How to use it

1. Click **Try for free** or **Start** in Apify Console.
2. In **Companies / career boards**, add one company per line (a URL, `ats:slug` or slug).
3. Optionally add filters: title keywords, location keywords, remote only, or posted within N days.
4. Set **Max jobs per company** to `0` to get every job. The example input uses 20 to keep your first run cheap.
5. Click **Start**, then download the results as JSON, CSV or Excel, or use the API.
6. For alerts, enable **Only new jobs since last run** and add a **Schedule** (for example daily), with an integration to Slack, Sheets or a webhook.

### Input examples

**Quick start (the example input):**

```json
{
  "companies": ["greenhouse:figma", "https://jobs.lever.co/palantir", "ashby:ramp"],
  "maxJobsPerCompany": 20
}
```

**Remote engineering roles posted in the last 14 days, without descriptions:**

```json
{
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "https://boards.greenhouse.io/cloudflare",
    "https://jobs.ashbyhq.com/ramp",
    "lever:palantir"
  ],
  "titleKeywords": ["engineer", "developer"],
  "excludeTitleKeywords": ["intern", "manager"],
  "remoteOnly": true,
  "postedWithinDays": 14,
  "includeDescription": false
}
```

**Daily new-job monitor for a location** (schedule it, and only jobs not seen in earlier runs are output):

```json
{
  "companies": ["greenhouse:figma", "greenhouse:datadog", "ashby:ramp", "recruitee:bunq"],
  "locationKeywords": ["New York", "Remote"],
  "onlyNewSinceLastRun": true
}
```

#### Input fields

| Field | Type | Default | Description |
|---|---|---|---|
| `companies` | array of strings | (required) | Career-board URLs, `ats:slug`, or bare slugs |
| `titleKeywords` | array | `[]` | Keep jobs whose title contains **any** of these (case-insensitive) |
| `excludeTitleKeywords` | array | `[]` | Drop jobs whose title contains any of these |
| `locationKeywords` | array | `[]` | Keep jobs whose location, locations or workplace type contains any of these |
| `remoteOnly` | boolean | `false` | Keep only jobs whose location or workplace type mentions remote or hybrid |
| `postedWithinDays` | integer | `0` | Keep jobs updated or posted in the last N days (0 = no limit) |
| `includeDescription` | boolean | `true` | Include the plain-text description (with emails and phones removed) |
| `onlyNewSinceLastRun` | boolean | `false` | Monitor mode: output only jobs not seen in earlier runs |
| `maxJobsPerCompany` | integer | `0` | Cap on jobs per company after filters (0 = all) |
| `requestDelayMs` | integer | `1000` | Minimum delay between requests to the same host (minimum 1000) |

### Output

Each dataset item is one job posting. Example (description shortened):

```json
{
  "ats": "ashby",
  "companySlug": "ramp",
  "company": "ramp",
  "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"],
  "workplaceType": "Hybrid",
  "employmentType": "FullTime",
  "salary": { "min": 211400, "max": 290600, "currency": "USD", "interval": "1 YEAR" },
  "salaryText": "$211.4K - $290.6K",
  "salarySource": "ats",
  "salaryMinYearly": 211400,
  "salaryMaxYearly": 290600,
  "salaryCurrency": "USD",
  "payRanges": null,
  "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…",
  "key": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "scrapedAt": "2026-10-27T20:51:07.896Z"
}
```

#### Output fields

| Field | Description |
|---|---|
| `ats` | `greenhouse`, `lever`, `ashby` or `recruitee` |
| `companySlug` | Board identifier on the ATS (for example `figma`) |
| `company` | Company display name when the ATS provides it (Greenhouse, Recruitee); otherwise the slug |
| `jobId` | The job's ID on the ATS |
| `title` | Job title |
| `department`, `team` | As published (can be `null`) |
| `location` | Primary location text |
| `locations` | All listed locations |
| `workplaceType` | `Remote`, `Hybrid` or `OnSite` when stated. On Greenhouse it is inferred only from "remote" in the location, so it's often `null` |
| `employmentType` | For example `FullTime`, `Full-time` or `Contract`, as the ATS words it. Greenhouse's public API doesn't expose it |
| `salary` | `{min, max, currency, interval}` or `null` |
| `salaryText` | Human-readable range |
| `salarySource` | `ats` (structured field from the ATS), `description` (parsed from the job text; an interval of `year?` or `hour?` is a guess), or `null` |
| `salaryMinYearly`, `salaryMaxYearly` | Salary converted to a **yearly** amount in the original currency: hour × 2,080, day × 260, week × 52, month × 12. `null` when the period is unknown or one-time |
| `salaryCurrency` | Currency of the salary and the yearly fields (no currency conversion) |
| `payRanges` | Greenhouse only, when a job lists several ranges (pay zones or countries): every range with `min`, `max`, `currency`, `interval` and its label. `salary` then shows the widest span in the first range's currency |
| `postedAt`, `updatedAt` | ISO 8601 dates when the ATS exposes them |
| `url`, `applyUrl` | Public job page and application page |
| `descriptionText` | Plain-text description, with emails and phone numbers replaced by `[email removed]` / `[phone removed]` (omitted when `includeDescription` is `false`) |
| `key` | Stable unique ID `ats:companySlug:jobId`, for deduplication |
| `scrapedAt` | When this run collected the job |

The **Output** tab has two views: **Jobs** (all jobs) and **With salary**. A **RUN_STATS** record in the run's key-value store lists boards found and not found, and any errors.

**How complete is each field?** Measured on our test set of 26 company boards and 4,869 jobs on October 27, 2026 (version 0.1.3). Your companies will differ.

| ATS | Jobs in test | Salary present | Salary from the ATS's own field | Workplace type | Employment type |
|---|---|---|---|---|---|
| Greenhouse | 2,774 | 49% | 38% | 23% | 0% |
| Lever | 624 | 70% | 34% | 100% | 99% |
| Ashby | 1,451 | 64% | 57% | 78% | 100% |
| Recruitee | 20 | 0% | 0% | 100% | 100% |

Every job with a salary also got `salaryMinYearly`/`salaryMaxYearly` in this test. The rest of the salaries come from ranges parsed from the job text.

> **Was this useful?** If this Actor saved you time, a quick star rating on the [Reviews tab](https://apify.com/markdemian/ats-jobs-scraper/reviews) helps other people find it. If something didn't work, please [open an issue](https://apify.com/markdemian/ats-jobs-scraper/issues) instead, so it can be fixed.

### How much does it cost to scrape Greenhouse, Lever, Ashby and Recruitee jobs?

This Actor uses **pay-per-event** pricing, so you pay **per job result** and nothing for compute:

- **$0.0015 per job** in the dataset (**$1.50 per 1,000 jobs**)
- plus Apify's standard **Actor start** event of **$0.00005 per run** (charged once per GB of memory; this Actor defaults to 1 GB)

| Example | Jobs | Approximate cost |
|---|---|---|
| Example input (3 companies × 20 jobs) | 60 | $0.09 |
| One large company (around 500 open jobs) | 500 | $0.75 |
| 25 companies, all jobs | 5,000 | $7.50 |
| Daily monitor, about 30 new jobs a day, for a month | 900 | $1.35 |

Costs are simply the number of jobs × $0.0015, plus a fraction of a cent per run. Filters are applied **before** results are saved, so **you only pay for jobs that match**. Monitor mode only charges for **new** jobs.

**Control your spending:** set **Max cost per run** in the run options. The Actor stops right away when the limit is reached (it cancels any board downloads still in progress), and jobs it couldn't deliver are not marked as "seen", so the next monitor run picks them up.

**Free plan:** Apify's free plan includes monthly platform credit (currently $5), which covers about 3,300 jobs from this Actor.

### Use it from AI agents (Apify MCP)

AI agents and assistants (Claude, Cursor, VS Code and other MCP clients) can run this Actor through [Apify's hosted MCP server](https://docs.apify.com/platform/integrations/mcp). Add this to your MCP client's configuration. On first connection it opens your browser so you can sign in to Apify:

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

Then ask things like *"List Ramp's open engineering roles in New York"* or *"Which of these 10 companies posted a remote data job this week?"*

Tips for agents:

- Set `includeDescription: false` and a small `maxJobsPerCompany` to keep responses short and cheap.
- Use `salaryMinYearly` / `salaryMaxYearly` to compare pay across hourly and annual roles.
- Set a max cost (`maxTotalChargeUsd` in the call options) to cap what a single call can spend.

### What's new

- **0.1.3** (October 27, 2026)
  - Greenhouse salaries now come from Greenhouse's structured pay-transparency ranges. In our test, Greenhouse jobs with a salary went from 11% to 49%.
  - New `salaryMinYearly`, `salaryMaxYearly` and `salaryCurrency` fields for all four ATSes, and `payRanges` for Greenhouse jobs that list several pay zones.
  - The **With salary** output view shows the yearly columns.
- **0.1.2** (October 27, 2026): when the max cost per run is reached, the Actor now stops right away instead of finishing board downloads it can no longer output.
- **0.1.1** (October 27, 2026): first public release.

### FAQ

#### Is it legal to scrape job postings?

This Actor reads **only public job postings** that companies deliberately publish through their ATS's **public job-board APIs**, the same data shown on their careers pages without logging in. It does **not** collect candidate data, applicant information or anything behind a login. It removes email addresses and phone numbers from descriptions and never maps recruiter or contact fields. You are responsible for how you use the data. Respect each company's terms, and consult a lawyer if you're unsure about your use case.

#### Does it collect personal data?

No, by design. Job postings describe roles, not people. Contact details that sometimes appear in description text (recruiter emails and phone numbers) are **automatically redacted**, and fields like Recruitee's `mailbox_email` are never mapped. A job description can still mention a hiring manager by name in free text. If that matters for your use case, set `includeDescription` to `false`.

#### How do I find a company's ATS and slug?

Open the company's careers page and click a job. If the URL or the application form is on `boards.greenhouse.io`, `job-boards.greenhouse.io`, `jobs.lever.co`, `jobs.ashbyhq.com` or `<name>.recruitee.com`, paste that URL. If you only know the name, try the bare slug (for example `figma`) and the Actor will check all four systems.

#### Why was a company "not found"?

The company either uses a different ATS (Workday, SmartRecruiters, iCIMS, Taleo, its own system and so on), uses a different slug, or has no public board. Not-found companies are listed in the run log and in `RUN_STATS`. **They are not charged**, because no job results are produced.

#### How fresh is the data?

It is live: every run reads the ATS APIs directly, at the moment the run happens. For alerts, schedule it daily or hourly with **Only new jobs since last run**.

#### How does monitor mode work?

The Actor remembers the `key` of every job it has output in a named key-value store in your account (`ats-jobs-scraper-seen`). With monitor mode on, jobs already seen are skipped. To start fresh, delete that store in **Storage**.

#### Can I use it via API, Python, JavaScript, Zapier or Make?

Yes. Like every Apify Actor you can start it through the Apify API or client libraries, receive webhooks when it finishes, and connect it to Zapier, Make, n8n, Google Sheets, Slack and more. See the **API** tab.

#### Something's wrong or missing?

Open an issue on the **Issues** tab with the company input you used. Requests for more ATS platforms are welcome.

### Limitations

- Supports **Greenhouse, Lever, Ashby and Recruitee** only. Workday, SmartRecruiters, iCIMS, Taleo, BambooHR, Workable and others are not supported yet.
- It returns what the ATS publishes. Fields the vendor's public API doesn't expose (for example employment type on Greenhouse) stay `null`.
- Salaries parsed from description text (`salarySource: "description"`) are a best-effort heuristic. Check the `salaryText`, and treat an interval of `year?` or `hour?` as a guess.
- Greenhouse doesn't state the pay period as a field, so it's read from the range's label (for example "Annual Base Salary Range" or "Hourly Rate"). When the label doesn't say, the period is guessed from the amount and marked `year?` or `hour?`.
- The yearly fields assume full-time hours (2,080 a year) and don't convert currencies.
- `company` is the slug for Lever and Ashby, because their public APIs don't include a company display name.
- A bare slug is matched to the **first** ATS with a non-empty board. If two systems use the same slug for different companies, use an explicit `ats:slug`.
- Some companies use their own careers domain in front of an ATS. Use the underlying ATS board URL or slug.
- Requests to the same host are spaced at least 1 second apart, so very long lists on a single ATS take roughly 1 second per company.
- It does not apply to jobs, and it does not collect applicant or candidate data.

***

> **Was this useful?** If this Actor saved you time, a quick star rating on the [Reviews tab](https://apify.com/markdemian/ats-jobs-scraper/reviews) helps other people find it. If something didn't work, please [open an issue](https://apify.com/markdemian/ats-jobs-scraper/issues) instead, so it can be fixed.

# Changelog

This Actor's version history is a separate document: https://apify.com/markdemian/ats-jobs-scraper/changelog.md

# Actor input Schema

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

One per line. Accepts a career-board URL (boards.greenhouse.io/stripe, jobs.lever.co/palantir, jobs.ashbyhq.com/ramp, acme.recruitee.com), an explicit 'ats:slug' (greenhouse:stripe, lever:palantir, ashby:ramp, recruitee:bunq), or a bare slug (we try each ATS in turn).

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

Case-insensitive. Empty = all jobs.

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

Skip jobs whose title contains any of these (case-insensitive).

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

E.g. 'Toronto', 'Canada', 'Remote'.

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

Only output jobs whose location or workplace type mentions remote or hybrid.

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

0 = no limit.

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

Emails and phone numbers inside descriptions are redacted.

## `onlyNewSinceLastRun` (type: `boolean`):

Monitor mode: remembers job keys in a named key-value store and outputs only jobs not seen before.

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

Cap on output jobs per company after filters. 0 = no cap (all jobs). The example input uses 20 to keep the first run cheap; set 0 to get every job.

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

Politeness. Lever's robots.txt asks for crawl-delay 1s; minimum 1000.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:figma",
    "https://jobs.lever.co/palantir",
    "ashby:ramp"
  ],
  "titleKeywords": [],
  "excludeTitleKeywords": [],
  "locationKeywords": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "includeDescription": true,
  "onlyNewSinceLastRun": false,
  "maxJobsPerCompany": 20,
  "requestDelayMs": 1000
}
```

# Actor output Schema

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

All job postings collected in this run (default dataset).

## `runStats` (type: `string`):

Per-run counts: boards found / not found, jobs fetched and output, errors.

# 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:figma",
        "https://jobs.lever.co/palantir",
        "ashby:ramp"
    ],
    "maxJobsPerCompany": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("markdemian/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": [
        "greenhouse:figma",
        "https://jobs.lever.co/palantir",
        "ashby:ramp",
    ],
    "maxJobsPerCompany": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("markdemian/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": [
    "greenhouse:figma",
    "https://jobs.lever.co/palantir",
    "ashby:ramp"
  ],
  "maxJobsPerCompany": 20
}' |
apify call markdemian/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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