# Greenhouse, Lever & Ashby Job Scraper API (`tidyfetch/ats-jobs-api`) Actor

Job scraper for Greenhouse, Lever and Ashby using their official public APIs. One schema, filtered and de-duplicated, no browser.

- **URL**: https://apify.com/tidyfetch/ats-jobs-api.md
- **Developed by:** [Tidyfetch](https://apify.com/tidyfetch) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 job listeds

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 Job Scraper (ATS Jobs API)

Get filtered, de-duplicated jobs from the **official public job-board APIs** of three applicant tracking systems (ATS), in one schema: the same JSON the companies' own career pages read. **$1 per 1,000 delivered jobs plus $0.005 per run; platform usage is included.** Jobs removed by your filters or by de-duplication are not charged. You bring the company board ids.

No Indeed, no LinkedIn, no browser, no proxies: one HTTP request per board, normalized, de-duplicated, filtered.

| ATS | Public API used | Company id you pass |
|---|---|---|
| Greenhouse | `boards-api.greenhouse.io/v1/boards/{board_token}/jobs?content=true` | board token, e.g. `airbnb` from `boards.greenhouse.io/airbnb` |
| Lever | `api.lever.co/v0/postings/{site}?mode=json` | site name, e.g. `leverdemo` from `jobs.lever.co/leverdemo` |
| Ashby | `api.ashbyhq.com/posting-api/job-board/{jobBoardName}` | job board name, e.g. `ashby` from `jobs.ashbyhq.com/ashby` |

Use it to monitor competitors' hiring, build a niche job board, feed a lead-generation pipeline (which companies are hiring for what), or track when a role you care about opens.

SmartRecruiters was supported until 2026-09-26 and has been removed: SmartRecruiters' API policy limits its public Posting API to customers' own career sites, and its API's `robots.txt` disallows other crawlers. Inputs with `"ats": "smartrecruiters"` now fail with an explanation.

This Actor is not affiliated with Greenhouse, Lever or Ashby. It uses their public job-board endpoints; that a posting is publicly readable does not by itself give permission to republish it, so check how you may reuse the data (see Limits).

### Input

```json
{
  "companies": [
    { "ats": "greenhouse", "id": "airbnb" },
    { "ats": "lever", "id": "leverdemo" },
    { "ats": "ashby", "id": "ashby" },
    { "ats": "auto", "id": "figma" }
  ],
  "postedAfter": "2026-09-01",
  "titleKeywords": ["engineer", "developer"],
  "locationKeywords": ["remote", "berlin"],
  "remoteOnly": false,
  "maxJobsPerCompany": 500,
  "includeDescription": true
}
```

| Field | Type | Default | Meaning |
|---|---|---|---|
| `companies` | array of `{ ats, id }` | required | `ats` is `greenhouse`, `lever`, `ashby` or `auto`. With `auto` the Actor tries the three APIs in that order and uses the first one that knows the id. |
| `postedAfter` | ISO date | – | Keep jobs first published on or after this date. Jobs whose ATS does not expose a publish date are kept. |
| `titleKeywords` | string\[] | – | Keep a job if its title contains any keyword (case-insensitive). |
| `locationKeywords` | string\[] | – | Keep a job if its location contains any keyword (case-insensitive). |
| `remoteOnly` | boolean | `false` | Keep only jobs flagged remote by the ATS or whose title/location says "remote". |
| `maxJobsPerCompany` | integer | `500` | Stop after this many jobs were output for a company (counted after filters). The Console form starts at 20 so a first test run stays small. |
| `includeDescription` | boolean | `true` | Add `descriptionText`. With `false`, Greenhouse boards are fetched without job content, which also leaves `department` empty for Greenhouse jobs. |

### Output

One dataset row per job:

```json
{
  "id": "8184174",
  "ats": "greenhouse",
  "company": "Airbnb",
  "companyId": "airbnb",
  "title": "Account Manager",
  "department": "Business Development",
  "location": "London, United Kingdom",
  "remote": false,
  "url": "https://careers.airbnb.com/positions/8184174?gh_jid=8184174",
  "postedAt": "2026-09-09T08:35:19.000Z",
  "updatedAt": "2026-09-24T22:49:00.000Z",
  "descriptionText": "Airbnb was born in 2007 when two hosts welcomed three guests ...",
  "salary": null,
  "source": "https://boards-api.greenhouse.io/v1/boards/airbnb/jobs?content=true"
}
```

When the ATS publishes a salary range (Lever, Ashby), `salary` looks like `{ "min": 110000, "max": 185000, "currency": "EUR", "interval": "1 YEAR", "text": "€110K - €185K" }`.

- `remote` is an estimate: the ATS flag when there is one (Lever `workplaceType`, Ashby `isRemote`, Greenhouse "Workplace Type" metadata), otherwise a text match on location/title.
- `salary` is present only when the ATS exposes it (Lever `salaryRange`, Ashby compensation). `null` otherwise.
- `descriptionText` is plain text converted from the HTML description. **Email addresses and phone numbers inside it are masked** (`[email hidden]`, `[phone hidden]`). Phone masking is rule-based (international, parenthesised and national formats; a bare 7-digit number such as `555-1234` is masked only after a cue like "call" or "tel"); dates, version numbers and numeric ranges are left alone.
- `updatedAt` is only available from Greenhouse.
- Duplicates are removed across all companies in the run, by URL (ignoring `utm_*`, `gh_src` and `lever-*` tracking parameters) and by `ats:id`.
- Jobs are written before their `Job listed` charge is requested, and these steps are serialized so parallel boards cannot race past your maximum cost. If a charge request itself fails, the written jobs are kept and the failure is recorded in `SUMMARY`, so the row count and the charged-event count can differ in that case.
- A run summary (per-company counts, errors, charged events) is stored in the key-value store as `SUMMARY`. Unknown board ids are reported there and in the log; they do not fail the run.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Run started | $0.005 per run |
| Job listed | $0.001 per job written to the dataset |

1,000 jobs cost about $1.005; 20 jobs cost $0.025. The run fee applies to every run, even one that returns no jobs. Jobs removed by filters or de-duplication are not charged. Set a maximum cost per run in Apify Console; the Actor stops cleanly when the limit is reached.

### Limits and good to know

- **You supply the company ids.** There is no built-in company directory yet; find the token in the company's career-page URL (see table above).
- Ashby jobs with `isListed: false` are skipped. Only published postings are ever visible on any of the three APIs.
- The Actor makes at most 4 concurrent requests, waits at least 1 s between Lever requests (Lever's `robots.txt` asks for `Crawl-delay: 1`), and retries `429`/`5xx` responses with exponential backoff. It reads data only (GET); it never applies to jobs.
- Rate limits of the three APIs are not documented; very large boards (thousands of postings) work but take longer.
- Workday, Workable, iCIMS, Personio, BambooHR, SmartRecruiters and other ATSs are not supported in this version.
- Data comes from the employers' public job boards. The Actor does not access candidate records or recruiter profiles. It applies pattern-based email and phone masking to `descriptionText` only; names and unusual contact formats may remain, so review the output before you redistribute it.
- The job texts belong to the employers. The three public APIs are documented for companies' own career pages; as of 2026-09-26 we found no published terms on third-party aggregation either way; if you republish the data, link to the original posting and follow the employer's terms.

### Disclosure

This Actor was designed and written by an AI coding agent (Claude) under human direction and review; a human maintainer reviews issues and the replies to them. Report problems through the Issues tab.

# Changelog

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

# Actor input Schema

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

One object per company: {"ats": "greenhouse|lever|ashby|auto", "id": "\<board token / site / jobBoardName>"}. With "auto" the Actor tries Greenhouse, Lever and Ashby in that order and uses the first API that knows the id.

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

Only return jobs first published on or after this date (ISO 8601, e.g. 2026-09-01 or 2026-09-01T00:00:00Z). Jobs without a known publish date are kept.

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

Keep a job only if its title contains at least one of these words or phrases (case-insensitive substring match).

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

Keep a job only if its location contains at least one of these words or phrases (case-insensitive substring match).

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

Keep only jobs the ATS marks as remote, or whose title/location mentions 'remote'.

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

Stop after this many jobs have been output for a company (counted after filters). The form starts at 20 so a first test run stays small; the API default is 500.

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

Include the job description as plain text (descriptionText). Emails and phone numbers inside descriptions are masked by pattern (names and unusual formats may remain).

## Actor input object example

```json
{
  "companies": [
    {
      "ats": "greenhouse",
      "id": "airbnb"
    },
    {
      "ats": "lever",
      "id": "leverdemo"
    },
    {
      "ats": "ashby",
      "id": "ashby"
    }
  ],
  "remoteOnly": false,
  "maxJobsPerCompany": 20,
  "includeDescription": true
}
```

# Actor output Schema

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

One row per job, de-duplicated across all companies in the run.

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

Per-company counts, errors and charged events.

# 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": [
        {
            "ats": "greenhouse",
            "id": "airbnb"
        },
        {
            "ats": "lever",
            "id": "leverdemo"
        },
        {
            "ats": "ashby",
            "id": "ashby"
        }
    ],
    "maxJobsPerCompany": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("tidyfetch/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": [
        {
            "ats": "greenhouse",
            "id": "airbnb",
        },
        {
            "ats": "lever",
            "id": "leverdemo",
        },
        {
            "ats": "ashby",
            "id": "ashby",
        },
    ],
    "maxJobsPerCompany": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("tidyfetch/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": [
    {
      "ats": "greenhouse",
      "id": "airbnb"
    },
    {
      "ats": "lever",
      "id": "leverdemo"
    },
    {
      "ats": "ashby",
      "id": "ashby"
    }
  ],
  "maxJobsPerCompany": 20
}' |
apify call tidyfetch/ats-jobs-api --silent --output-dataset

```

## MCP server setup

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