# Greenhouse, Lever & Ashby Jobs API: ATS Job Postings Scraper (`terracotta_hauyne/ats-jobs-api`) Actor

Greenhouse jobs API, Lever API and Ashby jobs in one scraper. Paste company slugs or careers URLs, get normalized job postings from their official public job board APIs (no API key, no login). Filter by keyword, location, remote, department, date. $1.50 per 1,000 jobs.

- **URL**: https://apify.com/terracotta_hauyne/ats-jobs-api.md
- **Developed by:** [Kevin](https://apify.com/terracotta_hauyne) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Security:** Limited permissions
- **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 postings

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 Jobs API: ATS Job Postings Scraper

A **Greenhouse jobs API, Lever API and Ashby jobs scraper in one actor**, for anyone who needs a job postings scraper that works on company career boards. Get **open jobs from any company that hires with Greenhouse, Lever or Ashby**, in one clean, identical
format. Paste company slugs or careers URLs, filter by keyword, location, remote, department, employment
type and date, and get one row per job: title, department, team, locations, remote flag, employment type,
salary range, posted date, apply link and the full description (text, Markdown or HTML).

The data comes from each ATS's **official public job board API**, the same JSON feed that powers the
company's own careers page. No login, no API key, no proxies, no HTML scraping.

### Who is it for

- **Recruiters and sourcers** who track which companies are hiring and for which roles.
- **Sales and lead-generation teams** that use hiring as a buying signal (a company opening ten data roles is investing in data).
- **Job boards, aggregators and labour-market researchers** that need one schema across Greenhouse, Lever and Ashby.
- **AI / RAG builders** who want job descriptions as clean text or Markdown.
- **Developers** who prefer one API call to three different job board APIs.

### Quick answers

- **Is this a Greenhouse jobs API?** It reads Greenhouse's official public job board API (and Lever's and Ashby's) and returns the same fields for all three. You choose the companies; it does not index every board.

- **Do I need an API key?** No. The three job board endpoints are public, so there is no key, login or proxy to set up.

- **Price:** $1.50 per 1,000 jobs (pay per event: one event per job saved), plus Apify's tiny actor-start fee. Companies with no matching jobs and boards that are not found cost nothing.

- **Which companies?** Any company with a public Greenhouse, Lever (US or EU) or Ashby job board. You give the board slug (`stripe`) or a URL. There is **no built-in company list**: you choose the companies.

- **How is the ATS detected?** From the URL (`boards.greenhouse.io/stripe`, `jobs.lever.co/spotify`, `jobs.ashbyhq.com/ashby`, `job-boards.greenhouse.io`, `jobs.eu.lever.co`, and the API URLs). A bare slug is tried on all three ATS; write `lever:spotify` or set the ATS field to skip the guessing.

- **My company has its own careers site (`https://stripe.com/jobs`).** The actor cannot see which ATS sits behind a custom page without scraping it, so it takes the company name from the domain (`stripe`) and tries all three ATS. If that is not the board slug, give the real slug or the ATS board URL.

- **One schema for all three ATS:** every item has exactly the same fields (see below); fields an ATS does not publish are `null`.

- **Salary:** filled when the ATS publishes it (Ashby with compensation shown, Greenhouse pay ranges, Lever salary range). Otherwise `null`; the actor never guesses.

- **What if a company is wrong or down?** The run does not fail. Each company gets a status in the `OUTPUT` record (`ok`, `not_found`, `ats_not_detected`, `error`) with a clear message.

- **Typical run time:** 10 companies in about 5 seconds. Large boards (Stripe, 750+ jobs) are one request each.

- **Politeness:** at most 3 parallel requests by default, retries with backoff, honours `Retry-After` on HTTP 429.

### How to use

1. Click **Start**. The prefilled input fetches 10 jobs each from Stripe (Greenhouse), Spotify (Lever) and Ashby, with no setup.
2. Replace the **Companies** list with yours, add filters if you want, and run.
3. Export the dataset as JSON, CSV or Excel, or call it from the Apify API, a schedule, Make, Zapier or n8n.

**Example 1: every open job at three companies**

```json
{
  "companies": ["https://boards.greenhouse.io/stripe", "https://jobs.lever.co/spotify", "https://jobs.ashbyhq.com/ashby"]
}
```

**Example 2: remote engineering jobs posted in the last 30 days, no descriptions**

```json
{
  "companies": ["lever:spotify", "ashby:linear", "greenhouse:figma"],
  "keywords": ["engineer"],
  "remoteOnly": true,
  "postedSince": "30 days",
  "descriptionFormat": "none"
}
```

**Example 3: jobs in London or Berlin with a Markdown description (for LLMs and RAG)**

```json
{
  "companies": ["spotify", "notion", "airbnb"],
  "locations": ["London", "Berlin"],
  "descriptionFormat": "markdown",
  "maxResultsPerCompany": 25
}
```

Bare slugs such as `spotify` are tried on Greenhouse, Lever and Ashby. If the same company has an old board on a second ATS you get the jobs of both, each labelled in `source`. Set **ATS** to avoid that.

### Use cases

- **Greenhouse jobs API for a company list:** pull every open job from a list of `boards.greenhouse.io` boards, then filter by department or location.
- **Lever API / Lever jobs scraper:** get postings from `jobs.lever.co` (US) and `jobs.eu.lever.co` boards with salary ranges when published.
- **Ashby jobs:** fetch `jobs.ashbyhq.com` boards including published compensation.
- **Job postings scraper for hiring-signal alerts:** schedule a daily run with `postedSince: "24 hours"` and send the new jobs to a webhook, Slack, Make, Zapier or n8n.
- **Remote jobs feed:** `remoteOnly: true` plus keywords gives a remote engineering job feed from the companies you pick.
- **LLM / RAG dataset:** `descriptionFormat: "markdown"` returns descriptions ready to embed.

### Pricing at a glance

$1.50 per 1,000 job postings (pay per event: $0.0015 per job saved) plus Apify's $0.00005 actor-start fee. You pay only for jobs that pass your filters, not for companies without matches or boards that are not found.

### Input

| Field | Meaning |
|---|---|
| `companies` | Board slugs or careers URLs (up to 2,000). Prefix `greenhouse:`, `lever:` or `ashby:` to force the ATS. |
| `ats` | `auto` (default), `greenhouse`, `lever`, `ashby`: applies to bare slugs. |
| `keywords` / `excludeKeywords` | Match any word in title, department or team (case-insensitive). |
| `searchInDescription` | Apply the keyword lists to the full description too. |
| `locations` | Any location or country containing one of these texts. |
| `remoteOnly` | Only jobs the company marks as remote. |
| `departments` | Department or team contains any of these texts. |
| `employmentTypes` | `full_time`, `part_time`, `contract`, `internship`, `temporary`, `other`. |
| `postedSince` | `YYYY-MM-DD` or relative: `7 days`, `24 hours`, `2 weeks`. |
| `descriptionFormat` | `text` (default), `markdown`, `html`, `none`. Same price for all. |
| `maxResults`, `maxResultsPerCompany` | Caps; also respects your maximum cost per run. |
| `maxConcurrency` | Parallel requests (1 to 10, default 3). |

All filters are combined with AND; the values inside one list are combined with OR.

### Output

One item per job, same fields for every ATS:

| Field | Meaning |
|---|---|
| `source` | `greenhouse`, `lever` or `ashby` |
| `company`, `companyName` | Board slug; company name (official on Greenhouse, derived from the slug on Lever and Ashby, e.g. `acme-corp` becomes `Acme Corp`) |
| `jobId` | Job id in the ATS. `source` + `company` + `jobId` is a unique key. |
| `title`, `department`, `team` | As published. `team` is empty on most Greenhouse boards. |
| `location`, `locations`, `country` | Primary location text, all locations, country (Lever and Ashby) |
| `remote`, `workplaceType` | `true` when the company marks the job remote; `remote`, `hybrid`, `onsite` or `null` |
| `employmentType`, `employmentTypeRaw` | Normalized (`full_time`, `part_time`, `contract`, `internship`, `temporary`, `other`) and the original text. Greenhouse rarely publishes it. |
| `compensationMin`, `compensationMax`, `compensationCurrency`, `compensationInterval`, `compensationSummary` | Published salary range (`hour`, `day`, `week`, `month`, `year`) and the text |
| `postedAt`, `updatedAt` | ISO 8601 UTC. Lever and Ashby have no update date. |
| `applyUrl`, `jobUrl` | Apply link and job page link |
| `description`, `descriptionFormat` | Description in the chosen format, `null` if left out |
| `scrapedAt` | Time of the run |

A real item (description shortened):

```json
{
  "source": "lever",
  "company": "spotify",
  "companyName": "Spotify",
  "jobId": "93b9a474-0e4e-4214-9c9a-974496478068",
  "title": "Artist & Label Partnerships Manager - GSA (15-month fixed-term)",
  "department": "Content",
  "team": "Music Business",
  "location": "Berlin",
  "locations": ["Berlin"],
  "country": "DE",
  "remote": false,
  "workplaceType": "onsite",
  "employmentType": "full_time",
  "employmentTypeRaw": "Permanent",
  "compensationMin": null,
  "compensationMax": null,
  "compensationCurrency": null,
  "compensationInterval": null,
  "compensationSummary": null,
  "postedAt": "2026-10-21T17:11:15Z",
  "updatedAt": null,
  "applyUrl": "https://jobs.lever.co/spotify/93b9a474-0e4e-4214-9c9a-974496478068/apply",
  "jobUrl": "https://jobs.lever.co/spotify/93b9a474-0e4e-4214-9c9a-974496478068",
  "description": "We have a great opportunity for an Artist & Label Partnerships Manager to come and join the band in Berlin. …",
  "descriptionFormat": "text",
  "scrapedAt": "2026-11-04T22:35:00Z"
}
```

An Ashby item with a salary range has, for example, `"compensationMin": 150000, "compensationMax": 250000, "compensationCurrency": "EUR", "compensationInterval": "year", "compensationSummary": "€150K - €250K"`.

#### Run summary (`OUTPUT` record)

Every run also stores a summary with one entry per company. Problems are reported here, they never stop the run:

```json
{
  "totalJobs": 165,
  "companiesRequested": 13,
  "companiesByStatus": {"ok": 12, "not_found": 1},
  "companies": [
    {"input": "https://boards.greenhouse.io/stripe", "slug": "stripe", "ats": ["greenhouse"], "status": "ok", "jobsFound": 758, "jobsMatched": 758, "jobsReturned": 15, "message": null},
    {"input": "this-company-does-not-exist-zq9", "slug": "this-company-does-not-exist-zq9", "ats": [], "status": "not_found", "jobsFound": 0, "jobsMatched": 0, "jobsReturned": 0, "message": "No public job board found for slug \"this-company-does-not-exist-zq9\" on Greenhouse, Lever, Ashby. Check the slug (the part after the host in your careers URL) or set the ATS."}
  ]
}
```

Statuses: `ok` (including boards with zero jobs), `not_found` (no such board), `ats_not_detected` (the URL gives no usable name), `invalid_input`, `error` (server or network problem that persisted after 4 attempts).

### Limits and notes

- **You supply the companies.** There is no daily index of all boards. To find a slug, open the company's careers page: if the job links go to `boards.greenhouse.io/<slug>`, `jobs.lever.co/<slug>` or `jobs.ashbyhq.com/<slug>`, that is the slug.
- Only jobs the company has published are returned (the same list as its public careers page). Unlisted Ashby jobs are skipped.
- A job can be reposted with a new id by the company; the actor reports what the board lists.
- Ashby and Lever do not publish an update date; Greenhouse does (`updatedAt`).
- Greenhouse `employmentType` and `team` come from custom board fields and are often `null`.

### Responsible use

Job postings are public, but they can mention recruiters or hiring managers by name. The actor outputs job-level fields only and does not collect applicant or employee data. You are responsible for lawful use of the data you export.

Not affiliated with or endorsed by Greenhouse, Lever or Ashby.

# Actor input Schema

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

Board slugs or careers URLs. The ATS is detected from the URL: https://boards.greenhouse.io/stripe, https://jobs.lever.co/spotify, https://jobs.ashbyhq.com/ashby (also job-boards.greenhouse.io, jobs.eu.lever.co and the API URLs). A bare slug such as 'stripe' is tried on all three ATS unless you set the ATS below or write 'lever:spotify'. A company's own careers URL (https://stripe.com/jobs) is tried with the name taken from the domain. Up to 2,000 entries.

## `ats` (type: `string`):

Applies to entries that are not a URL and have no 'ats:' prefix. Choosing one ATS is faster and avoids stale duplicate boards.

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

Keep jobs whose title, department or team contains any of these words, e.g. 'engineer', 'data scientist'.

## `excludeKeywords` (type: `array`):

Drop jobs whose title, department or team contains any of these words, e.g. 'intern', 'director'.

## `searchInDescription` (type: `boolean`):

Apply the keywords and exclude keywords to the full job description too, not only title, department and team.

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

Keep jobs with any location (or country) containing one of these texts, e.g. 'London', 'Germany', 'CA'. Remote jobs also match the text 'remote'.

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

Keep only jobs the company marks as remote (Lever and Ashby workplace type, Greenhouse location or workplace-type field).

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

Keep jobs whose department or team contains any of these texts, e.g. 'Engineering', 'Sales'.

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

Keep only these types. Jobs where the company does not publish a type (common on Greenhouse) are dropped when this is set.

## `postedSince` (type: `string`):

Keep jobs first published on or after this date: YYYY-MM-DD, or relative like '7 days', '24 hours', '2 weeks'. Uses the posted date, or the last update when no posted date exists.

## `descriptionFormat` (type: `string`):

Format of the 'description' field. The same price applies to all formats; 'Leave out' gives the smallest dataset.

## `maxResults` (type: `integer`):

Stop after this many jobs across all companies. Leave empty for no limit other than your maximum cost per run.

## `maxResultsPerCompany` (type: `integer`):

At most this many jobs from each company entry (after filters). Leave empty for all.

## `maxConcurrency` (type: `integer`):

How many job board requests run at the same time. Default 3 is polite; the actor also retries with backoff and honours HTTP 429 Retry-After.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "lever:spotify",
    "ashby"
  ],
  "ats": "auto",
  "keywords": [
    "engineer"
  ],
  "searchInDescription": false,
  "locations": [
    "London"
  ],
  "remoteOnly": false,
  "postedSince": "7 days",
  "descriptionFormat": "text",
  "maxResults": 30,
  "maxResultsPerCompany": 10,
  "maxConcurrency": 3
}
```

# Actor output Schema

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

One item per job posting (default dataset).

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

Per-company status (ok, not_found, ats_not_detected, error), jobs found/returned, request count.

# 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://boards.greenhouse.io/stripe",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ashby"
    ],
    "maxResults": 30,
    "maxResultsPerCompany": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("terracotta_hauyne/ats-jobs-api").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://boards.greenhouse.io/stripe",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/ashby",
    ],
    "maxResults": 30,
    "maxResultsPerCompany": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("terracotta_hauyne/ats-jobs-api").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://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/ashby"
  ],
  "maxResults": 30,
  "maxResultsPerCompany": 10
}' |
apify call terracotta_hauyne/ats-jobs-api --silent --output-dataset

```

## MCP server setup

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

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/Jll47VnmQfi0uQlHV/builds/RvuLfWtH26MvUSpLm/openapi.json
