# Greenhouse, Lever & Ashby Jobs Scraper: Live Job Search (`morehavoc/ats-jobs-search`) Actor

Greenhouse, Lever, Ashby job search, live on every run with no stale index. Search 11,003 company job boards by keyword, location, remote, salary, department and date, or pull every job from named companies. Salary, location and apply link. $0.0015 per job.

- **URL**: https://apify.com/morehavoc/ats-jobs-search.md
- **Developed by:** [Christopher Moravec](https://apify.com/morehavoc) (community)
- **Categories:** Jobs, Lead generation
- **Features:** Limited permissions
- **Stats:** 5 total users, 3 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 job returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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: Live Job Search

Search jobs across **11,003 company job boards** on Greenhouse, Lever and Ashby, or pull every open job from the companies
you name. One clean row per job: title, company, department, location, remote, salary, posted date, apply link.

**Live on every run, no stale index.** The actor reads each company's own public job board at the moment you run it. There
is no database in the middle, so a job closed this morning is gone and a job posted this morning is there. No login, no
browser and no proxy: it uses the official public job-board APIs that power the companies' own careers pages.

### Who it's for

- **Job seekers and job boards:** every remote software role, or every AI role posted this week, in one spreadsheet.
- **Recruiters and sales teams:** see who is hiring for what, right now. A company hiring its first security engineer is a lead.
- **Analysts and investors:** track hiring at a list of named companies over time.

### Two ways to use it

**Search** every bundled board by leaving *Companies* empty and setting any filters. The 11,003 boards were found in the public
Common Crawl URL index and each one checked against its API on 2026-11-04 (5,881 Greenhouse, 1,652 Lever, 3,470 Ashby). A full
read covers about 329,000 open jobs.

```json
{
  "keywords": ["software engineer", "backend engineer"],
  "remoteOnly": true,
  "postedWithinDays": 7,
  "maxItems": 500
}
```

**Companies** reads only the boards you list. Enter a company name or slug (tried on all three ATSs) or a board URL. The filters
still apply; leave them empty to get every job.

```json
{
  "companies": ["stripe", "https://jobs.lever.co/spotify", "jobs.ashbyhq.com/ramp"],
  "maxItems": 5000
}
```

### Input

| Field | Meaning |
|---|---|
| `keywords` | Any keyword in the job title (and in the description, with `includeDescriptions`). Case-insensitive, from the start of a word. Keywords of 3 letters or fewer must be a whole word, and in capitals they are an acronym: `AI` matches *AI Engineer*, not *said*. |
| `locations` | Any of these in the location text: `New York`, `London`, `Remote`. `US` and `UK` also match their other spellings. A row reading only *Austin, TX* does not match a `US` filter unless the board also states the country. |
| `remoteOnly` | Jobs the board states as remote. A job that does not say is left out. |
| `postedWithinDays` | Posted in the last N days. A job with no date is left out. |
| `departments` | Any of these in the department or team: `Engineering`, `Sales`. |
| `minSalary` | Keeps jobs whose top of range reaches this number, in the posting's own currency, with no conversion. Jobs with no stated pay are left out, and so is hourly or monthly pay. |
| `companies` | Names, slugs or board URLs (`boards.greenhouse.io/x`, `job-boards.greenhouse.io/x`, `jobs.lever.co/x`, `jobs.ashbyhq.com/x`). Empty = search every bundled board. |
| `includeDescriptions` | Default `false`. Adds the full description as plain text and searches it with `keywords`. A full read takes about 4 times as long. |
| `maxItems` | Default 500. The run stops as soon as it has this many. |

### Output

One row per job, de-duplicated by `ats` + `id`. **A field the board did not publish is `null`, never guessed.**

```json
{
  "ats": "ashby",
  "company": "Ramp",
  "companySlug": "ramp",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "location": "New York, NY (HQ)",
  "locationsAll": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "remote": true,
  "workplaceType": "hybrid",
  "employmentType": "Full-time",
  "salaryMin": 211400,
  "salaryMax": 290600,
  "salaryCurrency": "USD",
  "salaryInterval": "year",
  "postedAt": "2026-04-07T17:12:35.753Z",
  "updatedAt": null,
  "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "id": "34413f8d-26bf-4bbc-8ade-eb309a0e2245"
}
```

| Field | Description |
|---|---|
| `ats` | `greenhouse`, `lever` or `ashby` |
| `company` | The name on the board (Greenhouse). Lever and Ashby publish no company name, so it is built from the slug: `tutorintelligence` becomes *Tutorintelligence*. |
| `companySlug` | The board's slug on that ATS |
| `title` | Job title |
| `department`, `team` | As published. Greenhouse has departments, not teams. |
| `location`, `locationsAll` | Main location, and every location listed |
| `remote` | `true`, `false`, or `null` when the board does not say |
| `workplaceType` | `remote`, `hybrid`, `onsite`, or `null`. A job can be `hybrid` and `remote: true` when the board offers both. |
| `employmentType` | As published. Ashby's values are tidied to *Full-time*, *Part-time*, *Internship*, *Contract*, *Temporary*. |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval` | Only from the board's structured pay fields. Interval is `year`, `month`, `week`, `day` or `hour`. |
| `postedAt`, `updatedAt` | ISO 8601 UTC |
| `jobUrl`, `applyUrl` | The job page and the application page |
| `description` | Plain text, only with `includeDescriptions` |
| `id` | The job's id on that ATS |

The run's key-value store has an `OUTPUT` record: boards tried, ok, dead and failed per ATS, jobs read and returned, and any
company names no board was found for. The log prints the same on one line.

### Pricing

Pay per result: **$0.0015 per job** ($1.50 per 1,000), plus **$0.025 once** for a run that read 5,000 or more boards, which covers
reading them whether or not anything matched. A run that reaches *Max jobs* before 5,000 boards, a normal list of named companies,
or a failed run does not pay that. Set *Max jobs* or a maximum charge on the run to cap the cost.

### Limits

- **Salary is `null` on most rows.** In a sample of 23,703 jobs, 28% of Greenhouse, 19% of Lever and 30% of Ashby rows stated
  pay, and only those are returned with a number. Pay written only in the description text is not parsed.
- **Salary interval is often `null` on Greenhouse.** Its pay fields carry no interval, so it is set only when the range's own
  label says so (*Annual Salary*, *Hourly*). Most Greenhouse ranges have a label like *Zone 1 Pay Range* and stay `null`.
- **Greenhouse ranges:** several ranges in one currency are widened to one. If a job lists more than one currency, the first
  is used and the others are not converted or merged.
- **`remote` on Greenhouse** is only known when the employer uses a workplace field or says *Remote* or *Hybrid* in the
  location: about 20% of rows. `remoteOnly` leaves the rest out.
- **Lever dates:** `postedAt` is the posting's `createdAt`, which can be earlier than the day it went public. Lever and Ashby
  publish no `updatedAt`.
- **The bundled list is a snapshot of which boards exist**, not of their jobs. Companies move ATS and new ones appear, so a
  company can be missing. Use `companies` to read any board on these three ATSs, listed or not.
- **`maxItems` samples.** Boards are read in random order, so a run that stops early returns a different mix each time. To
  get every match, set `maxItems` above what you expect.
- Boards are employer-run, so a few are staffing or aggregator boards with thousands of listings.

### FAQ

**How fresh is the data?** As fresh as the run. Every board is read when you run the actor.

**How long does it take?** Reading all 11,003 boards took about 5 minutes on 1 GB of memory, and about 19 minutes with
descriptions. A run with a low *Max jobs* stops much sooner, and a short company list takes seconds.

**Why did my company not show up?** Check `companiesNotFound` in the `OUTPUT` record. A name is tried as a slug on all three
ATSs, but a slug is not always the company name. Paste the board URL instead.

**What happens if a job board is down?** A board that no longer exists is skipped and counted as dead. Rate limits and server
errors are retried with backoff. If one ATS stays down, the run still returns the other two and reports it. If all of the
ATSs are down, the run fails rather than returning an empty success.

**Is this allowed?** It reads the public job-board APIs these ATSs provide so that employers can publish their jobs. It uses no
login and collects no data about individuals, only job postings.

# Actor input Schema

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

Optional. A job is kept when ANY keyword appears in its title (and its description, when Include descriptions is on), case-insensitive, from the start of a word. Keywords of 3 letters or fewer must be a whole word, and in capitals they match as an acronym ("AI" matches "AI Engineer", not "said").

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

Optional. A job is kept when ANY of these appears in its location text, e.g. "New York", "London", "Remote". Country spellings for US and UK are matched together, but a city listing without a country will not match a country filter.

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

Keep only jobs the board states as remote. A job that does not say is left out.

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

Optional. 0 or empty = any date. A job with no date is left out, never assumed recent. Lever's date is when the posting was created.

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

Optional. A job is kept when ANY of these appears in its department or team, e.g. "Engineering", "Sales".

## `minSalary` (type: `integer`):

Optional. Keeps jobs whose stated top of range reaches this number, in the posting's own currency (no conversion). Jobs with no stated pay are left out, and so is hourly or monthly pay.

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

Optional. Read only these boards instead of the bundled list. Each line is a company name or slug (tried on all three ATSs) or a board URL (boards.greenhouse.io/x, job-boards.greenhouse.io/x, jobs.lever.co/x, jobs.ashbyhq.com/x). Empty = search every bundled board. The filters above still apply; leave them empty for every job.

## `includeDescriptions` (type: `boolean`):

Add the full job description as plain text, and search it with Keywords. Slower and larger on Greenhouse boards.

## `maxItems` (type: `integer`):

Stop after this many jobs (each job is one charge). The run reads boards in random order and stops as soon as it has this many.

## Actor input object example

```json
{
  "keywords": [
    "software engineer"
  ],
  "locations": [],
  "remoteOnly": false,
  "departments": [],
  "companies": [],
  "includeDescriptions": false,
  "maxItems": 100
}
```

# Actor output Schema

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

One row per job: ATS, company, title, department, team, location, remote, employment type, salary, dates, job and apply links.

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

Boards tried, ok, dead and failed per ATS, jobs read and returned, and any company names no board was found for.

# 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 = {
    "keywords": [
        "software engineer"
    ],
    "locations": [],
    "departments": [],
    "companies": [],
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("morehavoc/ats-jobs-search").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 = {
    "keywords": ["software engineer"],
    "locations": [],
    "departments": [],
    "companies": [],
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("morehavoc/ats-jobs-search").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 '{
  "keywords": [
    "software engineer"
  ],
  "locations": [],
  "departments": [],
  "companies": [],
  "maxItems": 100
}' |
apify call morehavoc/ats-jobs-search --silent --output-dataset

```

## MCP server setup

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

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/pd317PIRi5IXVNyd8/builds/NAwTlyjg7xO5nae7w/openapi.json
