# Greenhouse, Lever & Ashby Jobs | $1/1K | Auto-Detect ATS (`extractkit/greenhouse-lever-ashby-jobs-scraper`) Actor

Scrape jobs from any Greenhouse, Lever or Ashby board. Auto-detects which ATS a company uses, returns one normalized schema across all three, and parses salary out of the posting text. $1 per 1,000 jobs.

- **URL**: https://apify.com/extractkit/greenhouse-lever-ashby-jobs-scraper.md
- **Developed by:** [Extract Kit](https://apify.com/extractkit) (community)
- **Categories:** Jobs, Lead generation, MCP servers
- **Stats:** 2 total users, 1 monthly users, 75.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 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.

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

## Greenhouse, Lever & Ashby Jobs Scraper

Scrape job postings from any company running **Greenhouse**, **Lever** or **Ashby** — three of the most widely deployed applicant tracking systems — and get them back in **one schema**, whichever ATS they came from.

Built on the providers' own **public JSON APIs**. No proxies, no login, no browser, no anti-bot workarounds.

### What makes this different

**You don't need to know which ATS a company uses.** That is the whole point. Give it `stripe` and it works out that Stripe is on Greenhouse; give it `ramp` and it finds Ashby. Single-ATS Actors push that lookup back onto you, which is unhelpful when you have 200 companies and no idea how they're split.

**One schema across all three.** Greenhouse calls a title `title`, Lever calls it `text`. Lever timestamps in epoch milliseconds, Ashby in ISO 8601, Greenhouse in ISO with an offset. Ashby says `FullTime`, Lever says `Full-time`. All of it is normalized before you see it.

**Salary, including where the provider doesn't publish it.** Ashby returns structured compensation. Greenhouse and Lever return none at all — any pay range lives in the posting prose, because US pay-transparency laws require it in the text rather than in a field. This Actor reads both, and tells you which is which:

```json
"salary": {
  "min": 211400, "max": 290600, "currency": "USD",
  "interval": "year", "source": "structured", "raw": "$211.4K – $290.6K"
}
```

`source` is `structured` (from the provider, reliable) or `parsed` (read from prose, best-effort). `raw` is the exact text the numbers came from, so you can audit any parse you don't trust. When no range can be established **confidently**, `salary` is `null` — the parser is deliberately biased toward returning nothing rather than guessing, because a wrong salary silently corrupts your dataset in a way an empty field never does.

**A bad slug doesn't cost you the run.** Unrecognized companies are recorded in the `COMPANIES` key-value record with a reason, and the other boards continue.

### Input

Give it board slugs, full board URLs, or a mix:

```json
{
  "companies": ["stripe", "https://jobs.lever.co/palantir", "https://jobs.ashbyhq.com/ramp"],
  "titleKeywords": ["engineer"],
  "locationKeywords": ["remote", "berlin"],
  "remoteOnly": false,
  "includeSalary": true
}
```

Pasting a board URL is worth it — the ATS is read straight off the hostname, which skips detection entirely.

| Field | Default | Notes |
|---|---|---|
| `companies` | — | Required. Slugs (`stripe`) or board URLs. |
| `provider` | `auto` | Pin to one ATS only if every company on the list uses it. |
| `titleKeywords` | `[]` | Case-insensitive substring; any match keeps the job. |
| `locationKeywords` | `[]` | Matches secondary locations too. |
| `departmentKeywords` | `[]` | Matches department or team. |
| `remoteOnly` | `false` | Inferred from location text on Greenhouse. |
| `employmentTypes` | `[]` | ⚠️ Greenhouse publishes none — this filter excludes all Greenhouse postings. |
| `postedAfter` | `null` | ISO date. Excludes undated postings. |
| `includeDescription` | `false` | Adds 5–15 KB per job. Salary parsing works without it. |
| `includeSalary` | `true` | |
| `maxJobsPerCompany` / `maxJobsTotal` | `0` | 0 = no limit. Applied after filtering. |
| `concurrency` | `3` | Boards fetched at once. |

**Filters are applied before you are charged.** If you ask for engineering roles in Berlin, you pay for those — not for the other 540 postings that had to be fetched to find them.

### Output

One record per job:

```json
{
  "provider": "ashby",
  "company": "ramp",
  "companyName": "ramp",
  "id": "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)"],
  "workplaceType": "hybrid",
  "isRemote": true,
  "employmentType": "full-time",
  "employmentTypeRaw": "FullTime",
  "salary": { "min": 211400, "max": 290600, "currency": "USD", "interval": "year", "source": "structured", "raw": "$211.4K – $290.6K" },
  "publishedAt": "2026-04-07T17:12:35.753Z",
  "updatedAt": "2026-04-07T17:12:35.753Z",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d/application",
  "scrapedAt": "2026-08-17T09:00:00.000Z"
}
```

Two extra records are written to the key-value store: `SUMMARY` (counts by provider, how many carried salary, how many were remote) and `COMPANIES` (per-board outcome, including why any board failed).

### What this Actor will not do

- **No LinkedIn, Indeed or Glassdoor.** Those are login-walled or Cloudflare-defended, and scraping them means a permanent proxy bill and an anti-bot arms race. This Actor reads official public APIs only, which is why it doesn't break.
- **No SmartRecruiters.** Its `robots.txt` is `Disallow: /` for every agent but LinkedIn's. Greenhouse, Lever and Ashby all permit the paths used here.
- **No unlisted postings.** Ashby's `isListed: false` marks jobs a company deliberately hid from its public board; those are dropped.

### Field coverage by provider

Not every provider publishes every field. Rather than fabricate them, the Actor reports `null`:

| Field | Greenhouse | Lever | Ashby |
|---|---|---|---|
| Title, location, URL, dates | ✅ | ✅ | ✅ |
| Department / team | ✅ | ✅ | ✅ |
| Employment type | ❌ never published | ✅ | ✅ |
| Workplace type | ⚠️ inferred from location | ✅ | ✅ |
| Structured salary | ❌ parsed from text | ⚠️ rare | ✅ |
| Company display name | ✅ | ❌ slug used | ❌ slug used |

# Actor input Schema

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

Board slugs or board URLs. A slug is the last part of the board address — 'stripe' for boards.greenhouse.io/stripe. You can paste the full URL instead and the ATS is read straight off it, which skips detection.

## `provider` (type: `string`):

Leave on 'auto' unless you know every company on the list uses the same ATS. Auto tries Greenhouse, then Lever, then Ashby, and keeps whichever answers — that is usually why you'd use this Actor rather than a single-ATS one.

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

Keep only jobs whose title contains one of these (case-insensitive). Leave empty for all jobs.

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

Keep only jobs where any listed location contains one of these. Matches secondary locations too, so 'remote' catches postings that are remote in one region only.

## `departmentKeywords` (type: `array`):

Keep only jobs whose department or team contains one of these.

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

Keep only postings marked remote. Greenhouse publishes no remote flag, so for those boards this is inferred from the location text.

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

Keep only these types. Note that Greenhouse publishes no employment type at all — setting this filter will exclude every Greenhouse posting.

## `postedAfter` (type: `string`):

ISO 8601 date. Keeps only jobs first published on or after it. Postings with no publish date are excluded.

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

Adds descriptionHtml and descriptionText. Off by default because descriptions are 5–15 KB each and most buyers filter on title and location. Salary parsing works either way.

## `includeSalary` (type: `boolean`):

Reads Ashby's structured compensation and parses pay ranges out of Greenhouse and Lever descriptions. Check the salary.source field: 'structured' came from the provider, 'parsed' was read from prose.

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

0 means no limit. Applied after filtering, so a limit never silently biases which jobs you get.

## `maxJobsTotal` (type: `integer`):

0 means no limit. A hard ceiling on the run, and therefore on what you are charged.

## `concurrency` (type: `integer`):

How many company boards to fetch at once. Kept low on purpose — Lever's robots.txt asks for a 1 second crawl delay and this Actor honours it.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "ramp"
  ],
  "provider": "auto",
  "titleKeywords": [
    "engineer",
    "designer"
  ],
  "locationKeywords": [
    "berlin",
    "remote"
  ],
  "departmentKeywords": [
    "engineering"
  ],
  "remoteOnly": false,
  "postedAfter": "2026-01-01T00:00:00Z",
  "includeDescription": false,
  "includeSalary": true,
  "maxJobsPerCompany": 0,
  "maxJobsTotal": 0,
  "concurrency": 3
}
```

# Actor output Schema

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

Every posting as JSON — title, company, location, workplace type, employment type, parsed salary and apply URL.

## `jobsCsv` (type: `string`):

The same postings as a CSV download, with nested salary fields flattened into columns for spreadsheets.

## `overview` (type: `string`):

The key columns only — title, company, ATS, location, workplace, type, salary, published date and URL.

## `companies` (type: `string`):

Per-company outcome: which ATS was detected, how many jobs came back, and the error code for any board that failed. Check this before trusting a job count.

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

Totals for the run — jobs found, boards succeeded and failed, and how many postings carried salary or were remote.

## `runDetails` (type: `string`):

This run in Apify Console, including its log.

# 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": [
        "stripe",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp"
    ]
};

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

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,extractkit/greenhouse-lever-ashby-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/r9nCcJrtzzK0GtbdB/builds/PG8dkAMjTccu2L7X4/openapi.json
