# ATS Jobs Scraper: Greenhouse, Lever & Ashby (`quicksloth/ats-jobs-scraper`) Actor

Scrape open jobs from Greenhouse, Lever and Ashby career boards into one clean schema. Paste board URLs, careers pages or company names and the Actor detects the ATS. Salary ranges where employers publish them, an only-new-jobs mode for scheduled runs, and no recruiter or contact fields.

- **URL**: https://apify.com/quicksloth/ats-jobs-scraper.md
- **Developed by:** [Quicksloth](https://apify.com/quicksloth) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 jobs

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

Get all the public open jobs from the **Greenhouse**, **Lever** and **Ashby** career boards of the companies you follow, in **one clean schema**. Paste board URLs, careers-page URLs or plain company names; the Actor finds the board, reads the public job postings and returns them as JSON, CSV or Excel. Switch on **only new jobs** for scheduled runs and, after the first run, you pay only for postings that are new or changed.

*ATS = applicant tracking system, the software companies use to publish and manage their job openings.*

- **One input for three job-board systems.** Mix Greenhouse, Lever and Ashby companies in one run (up to 1,000 companies) instead of integrating three different APIs.
- **Finds the board for you.** Accepts board URLs, `ats:board` names, careers pages that link or embed a board, or plain company names.
- **Same fields for every source.** Title, locations, remote flag, department, dates, apply link and salary range (where the employer publishes one), ready to merge in one table or database.
- **Only new or changed jobs.** For monitoring, get just what appeared or changed since the last run, and pay only for that.
- **Spending under control.** Set a maximum number of jobs or a maximum cost per run; the Actor stops cleanly and says why. Runs started from the Console form are prefilled with a 500-job limit (about $0.75); API and MCP runs have no limit unless you set `maxJobs` or a maximum cost.
- **No recruiter or contact fields.** The output has no recruiter, hiring-manager or contact fields, and job descriptions are off unless you ask for them (see [Legal and data protection](#legal-and-data-protection)).

Free Apify plan: up to 100 jobs per run, enough to test it on your own list of companies.

### What can you do with it?

- **Sales and business development:** find companies opening roles that signal a need for your product (for example several data-engineering jobs), then watch them on every scheduled run with *Only new or changed jobs*. Use `titleKeywords` to keep just those roles.
- **Recruiting and talent intelligence:** follow the openings, locations and remote policy of target employers and see the new roles on every scheduled run.
- **Pay and market research:** collect the salary ranges employers publish (`salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval`). Only part of the postings show pay, and Greenhouse ranges do not state the interval.
- **Labor-market and hiring-trend analysis:** keep the results of each run and compare open roles by department, location or workplace type over time (`scrapedAt` marks the run).
- **Internal job alerts:** send new postings to a spreadsheet, a database or a chat channel with Apify schedules, webhooks and integrations.
- **AI agents and data pipelines:** one predictable JSON schema, available through the Apify API and, for AI agents, through [Apify's MCP server](https://docs.apify.com/integrations/mcp).

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

1. Open the Actor and paste your companies in **Companies**, one per line: board URLs (`https://jobs.lever.co/spotify`), `ats:board` names (`ashby:ramp`), careers-page URLs or plain company names.
2. Optional: add title or location keywords, tick **Remote jobs only**, and set **Max jobs in total**.
3. Click **Start**. The **Overview** table of the dataset shows title, company, location, salary, posted date and job page. Download it as JSON, CSV or Excel, or read it through the Apify API.

### Pricing

You pay per job saved to the dataset: **$1.50 per 1,000 jobs** ($0.0015 per job), plus Apify's standard start charge of $0.00005 per run. Companies that are not found, and jobs removed by your filters, cost nothing.

| Jobs saved | Cost |
|---|---|
| 100 | $0.15 |
| 1,000 | $1.50 |
| 10,000 | $15.00 |
| 100,000 | $150.00 |

**Monitoring example.** You follow 200 companies and run the Actor once a day with *Only new or changed jobs* on. The first run saves all matching jobs. If, say, 100 jobs are new or changed on a given day (your number will differ), that run costs about $0.15, and 30 such days cost about $4.50.

**Cost control.** In Apify Console you can set a maximum cost for each run; the Actor stops cleanly when it is reached and says so in the status message. Runs started from the Console form are prefilled with a 500-job limit (about $0.75): change or clear it to get more. API and MCP runs have no limit unless you set `maxJobs` or a maximum cost.

**Free plan.** Users on the free Apify plan get up to 100 jobs per run. The run log and status message say when this limit is reached. Paid plans have no such limit.

### Supported ATS and accepted inputs

| ATS | Board URL examples | Prefixed name |
|---|---|---|
| Greenhouse | `https://job-boards.greenhouse.io/gitlab`, `https://boards.greenhouse.io/gitlab`, `https://boards.greenhouse.io/embed/job_board?for=gitlab` | `greenhouse:gitlab` |
| Lever | `https://jobs.lever.co/spotify`, `https://jobs.eu.lever.co/<company>` (EU instance) | `lever:spotify`, `lever-eu:<company>` |
| Ashby | `https://jobs.ashbyhq.com/ramp` | `ashby:ramp` |

You can also enter:

- **A careers page URL** (for example `https://www.example.com/careers`). The Actor looks for an embedded or linked Greenhouse, Lever or Ashby board on that page. Pages that load the board only with JavaScript from another domain may not be detected; use the board URL in that case.
- **A plain company name** (for example `Notion`). The Actor tries the most likely board names on Greenhouse, then Lever, then Ashby. Names are a convenience: two companies can share a name, so use board URLs when you need certainty.

### Input examples

**Basic: all jobs from three companies**

```json
{
    "companies": [
        "https://job-boards.greenhouse.io/gitlab",
        "https://jobs.lever.co/spotify",
        "ashby:ramp"
    ]
}
```

**Filtered: remote engineering jobs, with descriptions, at most 50 per company**

```json
{
    "companies": ["https://job-boards.greenhouse.io/gitlab", "Notion", "https://jobs.lever.co/spotify"],
    "titleKeywords": ["engineer", "developer"],
    "remoteOnly": true,
    "includeDescription": true,
    "maxJobsPerCompany": 50
}
```

**Scheduled monitoring: only new or changed jobs**

```json
{
    "companies": ["https://job-boards.greenhouse.io/gitlab", "ashby:ramp"],
    "locationKeywords": ["London", "Berlin", "Remote"],
    "onlyNewJobs": true,
    "stateKey": "weekly-europe"
}
```

#### Input fields

| Field | Type | Default | Description |
|---|---|---|---|
| `companies` | array of strings | (required) | Board URLs, `ats:board` names, careers-page URLs or company names. Up to 1,000 per run. |
| `titleKeywords` | array of strings | empty | Keep jobs whose title contains any of these words (case-insensitive). |
| `locationKeywords` | array of strings | empty | Keep jobs with a location containing any of these words (case-insensitive). |
| `remoteOnly` | boolean | `false` | Keep only jobs marked as remote. |
| `includeDescription` | boolean | `false` | Add the job description as plain text (emails, phone numbers and names after labels such as "Recruiter:" removed). |
| `onlyNewJobs` | boolean | `false` | Return only jobs that are new or changed since the previous run with the same filters. |
| `stateKey` | string | empty | Optional name for the "only new jobs" history, so different scheduled tasks keep separate histories. |
| `maxJobsPerCompany` | integer | no limit | Keep at most this many jobs per company (newest first). |
| `maxJobs` | integer | no limit | Stop after this many jobs in total. The Console form is prefilled with 500; clear it for no limit. |

### Output

Each job is one dataset item. Fields are always present; a field is `null` when the ATS does not provide it.

```json
{
    "id": "greenhouse:exampleco:4012345002",
    "ats": "greenhouse",
    "board": "exampleco",
    "companyName": "Example Co",
    "jobId": "4012345002",
    "title": "Senior Backend Engineer",
    "department": "Engineering",
    "team": null,
    "locations": ["Remote, Canada", "Remote, United States"],
    "location": "Remote, Canada",
    "isRemote": true,
    "workplaceType": "remote",
    "employmentType": null,
    "postedAt": "2026-10-01T15:20:33.000Z",
    "updatedAt": "2026-10-03T09:12:10.000Z",
    "url": "https://job-boards.greenhouse.io/exampleco/jobs/4012345002",
    "applyUrl": "https://job-boards.greenhouse.io/exampleco/jobs/4012345002",
    "salaryMin": 115200,
    "salaryMax": 129600,
    "salaryCurrency": "USD",
    "salaryInterval": null,
    "salaryText": "USD 115,200 - 129,600 (United States Salary Range)",
    "descriptionText": null,
    "scrapedAt": "2026-10-06T16:25:07.506Z"
}
```

#### What each ATS provides

| Field | Greenhouse | Lever | Ashby |
|---|---|---|---|
| `companyName` | yes | no (`null`) | no (`null`) |
| `department` / `team` | department only | both | both |
| `workplaceType` | from the employer's "Workplace Type" field if present, otherwise inferred from the location text ("Remote", "Hybrid") | yes | yes |
| `employmentType` | only if the employer adds an "Employment Type" field | yes (employer's own label, e.g. "Permanent") | yes (`full-time`, `part-time`, `contract`, `internship`, `temporary`) |
| `postedAt` | first published | created | published |
| `updatedAt` | yes | no (`null`) | no (`null`) |
| Salary | when the employer enables pay transparency (no interval given) | when the employer publishes a salary range | when the employer shows compensation |

Values are normalized: `workplaceType` is `remote`, `hybrid` or `onsite`; `isRemote` is `true` or `false` only when the ATS or the location says so, otherwise `null`; `salaryInterval` is `year`, `month`, `week`, `day` or `hour`; dates are ISO 8601 in UTC.

#### Run summary

Every run also saves a summary in the default key-value store under the key `OUTPUT`:

```json
{
    "companies": [
        { "input": "https://jobs.lever.co/spotify", "ats": "lever", "board": "spotify", "status": "ok", "jobs": 75, "error": null },
        { "input": "Unknown Company", "ats": null, "board": null, "status": "failed", "jobs": 0, "error": "No public Greenhouse, Lever or Ashby board found for \"Unknown Company\" (tried: unknowncompany, unknown-company)." }
    ],
    "totalJobs": 75,
    "newJobs": null,
    "stoppedBecause": null
}
```

A company that cannot be found or fails does **not** stop the run: it is reported here and the other companies continue. The run fails only if every company fails.

### Only new jobs (scheduled runs)

Turn on `onlyNewJobs` and schedule the Actor (for example daily). The first run returns all matching jobs and remembers them. Later runs return only jobs that are **new** or whose key fields **changed** (title, location, salary, department, workplace or employment type, URL, or the update date where the ATS provides one). Jobs that were not returned because of a limit stay "unseen" and come back in the next run.

The history is kept in a named key-value store in your account (`ats-jobs-seen-state`) and is separate for each combination of filters. If you run several scheduled tasks with the same filters, give each one its own `stateKey`.

### Legal and data protection

- The Actor reads the **public job-board APIs** that Greenhouse, Lever and Ashby document for publishing job postings ([Greenhouse Job Board API](https://docs.greenhouse.io/job-board.html): "Job Board data is publicly available, so authentication is not required for any GET endpoints"; [Lever Postings API](https://github.com/lever/postings-api); [Ashby job posting API](https://developers.ashbyhq.com/docs/public-job-posting-api)) and, only when you enter one, a single careers page. No login, no private data.
- If you enter a careers-page URL, the Actor loads only that page to look for a link or embed of a supported board. It does not crawl the website.
- It keeps to the robots.txt rules of the three APIs (last checked 2026-10-08), including Lever's one-second crawl delay.
- **No personal-data fields**: the output has no recruiter, hiring-manager or contact fields.
- **Descriptions are off by default.** When you turn them on, they are free text written by the employer: email addresses, phone numbers in common formats and names after labels such as "Recruiter:", "Hiring Manager:" or "Contact:" are removed, but other mentions of people may remain. Check that you have a legal basis before storing them, and that your use of the employer's text is allowed.
- Greenhouse, Lever and Ashby are trademarks of their respective owners, named here only to say which job boards the Actor can read. This Actor is not affiliated with or endorsed by Greenhouse, Lever or Ashby.
- You are responsible for how you use the data, including compliance with the laws that apply to you.

### FAQ

**How do I find a company's board?** Open the company's careers page and click a job. If the address contains `greenhouse.io`, `lever.co` or `ashbyhq.com`, copy that URL. Or just paste the careers page URL and let the Actor look for it.

**Do I need proxies or a browser?** No. The Actor calls the public job-board APIs with plain HTTP requests. The only web page it can load is a careers page you enter, to look for a board link.

**Can it find companies for me, or search jobs across all companies?** No. It reads the boards of the companies you list; it has no company directory and no search across all employers. Use the title and location keywords to filter the jobs of your companies.

**What counts as "changed" in only-new mode?** A change in title, locations, department, team, remote or workplace type, employment type, job URL, salary fields, or the update date where the ATS provides one. Changes in the description do not count. A job that was missing from the board during a run and is posted again later is returned as new.

**How current is the data?** Each run reads the live boards. `scrapedAt` is the time of the run; `postedAt` and `updatedAt` are the dates the ATS reports.

**Why is `companyName` empty for Lever and Ashby?** Their public APIs do not include it. Use `board`, or keep your own mapping from input to company.

**Why do some jobs have no salary?** Salary is included only when the employer publishes it on the job board.

**A company was not found. What can I do?** Use the exact board URL from the company's job pages instead of the name. Check the `OUTPUT` record for the reason. A company that fails does not stop the run.

**Are Greenhouse boards on the EU domain supported?** Not yet. Boards at `job-boards.eu.greenhouse.io` are recognised but not supported: that company is reported as failed in the run summary and the other companies continue.

**Do you support Workday, SmartRecruiters or other ATS?** Not in this Actor, which covers Greenhouse, Lever and Ashby. Tell us which one you need in the Issues tab.

**How many companies can I add?** Up to 1,000 per run.

### Support

Found a problem, or need another ATS? Open an issue in the **Issues** tab of this Actor and include the run ID.

### Changelog

- **0.2 (2026-10-09)**: descriptions remove more phone numbers (national numbers written in one or two blocks, such as UK and Australian numbers, and numbers after labels like "Phone:") and names after "Contact:" or "Name:" at the start of a line.
- **0.1 (2026-10-08)**: first public release.
  - Reads public job boards on Greenhouse, Lever (including the EU instance) and Ashby, from board URLs, `ats:board` names, careers-page URLs or company names (up to 1,000 per run).
  - One output schema for all three, with locations, remote and workplace type, dates, apply link and salary fields where the employer publishes them.
  - Filters: title keywords, location keywords, remote only, maximum jobs per company and in total.
  - Optional job descriptions as plain text, with email addresses, phone numbers and names after labels such as "Recruiter:" removed.
  - Only-new-or-changed mode for scheduled runs, with separate histories through `stateKey`.
  - Run summary per company in the `OUTPUT` record; one failing company does not stop the run.
  - Pay-per-event pricing: $0.0015 per job saved. Free plan: up to 100 jobs per run.
  - Known limits: Greenhouse boards on the EU domain are not supported yet; Lever and Ashby do not provide the company name.

# Actor input Schema

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

One company per line. Accepted: a job-board URL (https://job-boards.greenhouse.io/gitlab, https://jobs.lever.co/spotify, https://jobs.ashbyhq.com/ramp), a prefixed board name (greenhouse:gitlab, lever:spotify, lever-eu:<name>, ashby:ramp), a careers-page URL that embeds or links one of these boards, or a plain company name (the Actor tries Greenhouse, Lever and Ashby).

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

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

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

Keep only jobs with a location containing at least one of these words (case-insensitive), e.g. a city or country. Leave empty to keep all.

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

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

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

Add the full job description as plain text. Descriptions are the employer's own text: email addresses, phone numbers in common formats and names after labels such as "Recruiter:" are removed, but other mentions of people may remain. Makes the dataset much larger.

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

For scheduled runs: return only jobs that are new or have changed since the previous run with the same filters. The first run returns everything.

## `stateKey` (type: `string`):

Optional. Give each scheduled task its own name so their "only new jobs" histories stay separate. By default the history is shared by runs with the same filters.

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

Keep at most this many jobs per company (newest first). Empty or 0 = no limit.

## `maxJobs` (type: `integer`):

Stop after this many jobs in total. Empty or 0 = no limit (the run also stops at the maximum cost you set for the run).

## Actor input object example

```json
{
  "companies": [
    "https://job-boards.greenhouse.io/gitlab",
    "https://jobs.lever.co/spotify",
    "ashby:ramp"
  ],
  "titleKeywords": [
    "engineer",
    "data"
  ],
  "locationKeywords": [
    "London",
    "Remote"
  ],
  "remoteOnly": false,
  "includeDescription": false,
  "onlyNewJobs": false,
  "stateKey": "weekly-engineering",
  "maxJobsPerCompany": 50,
  "maxJobs": 500
}
```

# Actor output Schema

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

No description

## `runSummary` (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": [
        "https://job-boards.greenhouse.io/gitlab",
        "https://jobs.lever.co/spotify",
        "ashby:ramp"
    ],
    "maxJobs": 500
};

// Run the Actor and wait for it to finish
const run = await client.actor("quicksloth/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": [
        "https://job-boards.greenhouse.io/gitlab",
        "https://jobs.lever.co/spotify",
        "ashby:ramp",
    ],
    "maxJobs": 500,
}

# Run the Actor and wait for it to finish
run = client.actor("quicksloth/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": [
    "https://job-boards.greenhouse.io/gitlab",
    "https://jobs.lever.co/spotify",
    "ashby:ramp"
  ],
  "maxJobs": 500
}' |
apify call quicksloth/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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