# Job Postings Scraper - Greenhouse, Lever & Ashby (Multi-ATS) (`bujhmml/ats-jobs-scraper`) Actor

Scrape live job postings from any Greenhouse, Lever, or Ashby careers board by slug, URL, or auto-detect. Returns title, location, department, employment type, remote flag, salary, apply URL and dates. Built-in keyword, location, department and remote filters. HTTP-first, no auth, no anti-bot.

- **URL**: https://apify.com/bujhmml/ats-jobs-scraper.md
- **Developed by:** [Ihor Bielievskiy](https://apify.com/bujhmml) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 11 total users, 8 monthly users, 95.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.40 / 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/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

## Job Postings Scraper - Greenhouse, Lever & Ashby (Multi-ATS)

Pull live job openings from any company that hires on **Greenhouse**, **Lever**, or **Ashby**. Give it a company slug, paste a careers-board URL, or let it auto-detect the platform for you. Every posting comes back as one clean, normalized row no matter which ATS it came from: title, location, department, team, employment type, remote flag, salary (where the ATS exposes it), the listing and apply URLs, created/updated dates, and the full description in both plain text and HTML. Export as JSON, CSV, or Excel.

It calls each ATS's own public job-board API directly instead of driving a headless browser, so it's fast, needs no login, and doesn't trip anti-bot. Mix as many boards and ATS types as you want in a single run.

### Three ways to point it at a board

You can put any of these in the `sources` array, and mix them freely:

1. **Explicit object** — `{ "ats": "greenhouse", "company": "stripe" }`
2. **Pasted board URL** — `"https://jobs.lever.co/spotify"` or `"https://boards.greenhouse.io/stripe"` (the ATS and slug are read straight out of the URL)
3. **Bare company slug** — `"ramp"` (the actor probes Greenhouse, Lever, and Ashby and uses whichever one has a board)

The `company` slug is the identifier in the careers-page URL: `boards.greenhouse.io/`**`stripe`**, `jobs.lever.co/`**`spotify`**, `jobs.ashbyhq.com/`**`ramp`**.

Underlying endpoints:

- **`greenhouse`** — `boards-api.greenhouse.io/v1/boards/{company}/jobs`
- **`lever`** — `api.lever.co/v0/postings/{company}`
- **`ashby`** — `api.ashbyhq.com/posting-api/job-board/{company}`

### Filters

Narrow the run before anything is billed — filtered-out rows never cost you a thing:

- `titleKeyword` — title must contain this text
- `locationKeyword` — location must contain this text
- `department` — department or team must contain this text
- `remoteOnly` — keep only jobs detected as fully remote

### Input

| Field | Type | Description |
|-------|------|-------------|
| `sources` | array | Board objects `{ats, company}`, board URLs, or bare slugs (any mix). Required. |
| `titleKeyword` | string | Keep only jobs whose title contains this (case-insensitive). |
| `locationKeyword` | string | Keep only jobs whose location contains this (case-insensitive). |
| `department` | string | Keep only jobs whose department or team contains this (case-insensitive). |
| `remoteOnly` | boolean | Keep only fully-remote jobs. |
| `maxItems` | integer | Stop after this many rows across all boards (0 = no limit). |
| `impersonate` | string | Browser TLS fingerprint (`chrome` by default). |

```json
{
  "sources": [
    { "ats": "greenhouse", "company": "stripe" },
    "https://jobs.lever.co/spotify",
    "ramp"
  ],
  "titleKeyword": "engineer",
  "remoteOnly": true,
  "maxItems": 500
}
```

### Output fields

One row per posting. A check means the field is populated when that ATS provides it; otherwise it's `null`.

| Field | Type | Greenhouse | Lever | Ashby |
|-------|------|:---:|:---:|:---:|
| `source_ats` | string | ✅ | ✅ | ✅ |
| `company` | string | ✅ | ✅ | ✅ |
| `job_id` | string | ✅ | ✅ | ✅ |
| `global_id` | string `{ats}:{company}:{job_id}` | ✅ | ✅ | ✅ |
| `title` | string | ✅ | ✅ | ✅ |
| `location` | string | ✅ | ✅ | ✅ |
| `department` | string | ✅ | ✅ | ✅ |
| `team` | string | — | ✅ | ✅ |
| `employment_type` | string | — | ✅ | ✅ |
| `remote` | boolean | inferred | ✅ | ✅ |
| `remote_type` | `remote`/`hybrid`/`onsite` | inferred | ✅ | ✅ |
| `salary` | string | — | when set | when set |
| `url` | string (listing) | ✅ | ✅ | ✅ |
| `apply_url` | string | — | ✅ | ✅ |
| `created_at` | ISO 8601 | ✅ | ✅ | ✅ |
| `updated_at` | ISO 8601 | ✅ | — | when set |
| `posted_at` | ISO 8601 | ✅ | ✅ | ✅ |
| `scraped_at` | ISO 8601 | ✅ | ✅ | ✅ |
| `description` | plain text | ✅ | ✅ | ✅ |
| `description_html` | HTML | ✅ | ✅ | ✅ |

`global_id` is stable across runs, so you can diff datasets or join against your own systems. Greenhouse's board API has no employment-type, team, or salary, no explicit remote flag, and no separate apply endpoint — `remote`/`remote_type` there are inferred from the location and title text (treat them as best-effort for that source), and `apply_url` is `null` since the listing `url` is the only link Greenhouse exposes.

### Example output

```json
{
  "source_ats": "lever",
  "company": "spotify",
  "job_id": "88499546-e9f7-4403-87a5-240050bd7c5b",
  "global_id": "lever:spotify:88499546-e9f7-4403-87a5-240050bd7c5b",
  "title": "Accounts Payable Analyst",
  "location": "New York, NY",
  "department": "Finance",
  "team": "Accounting",
  "employment_type": "Permanent",
  "remote": false,
  "remote_type": "hybrid",
  "salary": null,
  "url": "https://jobs.lever.co/spotify/88499546-e9f7-4403-87a5-240050bd7c5b",
  "apply_url": "https://jobs.lever.co/spotify/88499546-e9f7-4403-87a5-240050bd7c5b/apply",
  "created_at": "2026-05-11T11:20:11.285000+00:00",
  "updated_at": null,
  "posted_at": "2026-05-11T11:20:11.285000+00:00",
  "scraped_at": "2026-06-24T09:15:02.110000+00:00",
  "description": "Spotify is looking for an Accounts Payable Analyst ..."
}
```

### Why this one

- **One shape for every ATS.** Greenhouse, Lever, and Ashby each return a different JSON structure; this maps all of them to the same fields, so you can dump multiple companies into one dataset and not care where each row came from.
- **Paste a URL or just a name.** No need to know which ATS a company uses — paste the careers link or the bare slug and it figures the rest out.
- **Nothing fails silently.** A wrong slug, a failed request, or an API that changes shape gives you a typed error row (`fetch_failed`, `parse_failed`, `invalid_source`, `invalid_input`) instead of a quietly empty run. One bad board never aborts the rest, but if *every* source fails the run is marked failed (not a green empty dataset), and a rate-limit/anti-bot block (HTTP 403/429/5xx) is reported as a block rather than a false "no board found".
- **You're billed per valid posting delivered**, so error rows, duplicates, and rows you filter out don't cost you anything.
- **Duplicates are removed** within a run, per ATS, by job id.

### Notes

Only public, unauthenticated job-board APIs are used — the same data these companies publish on their own careers pages. Follow each provider's Terms and the laws that apply to you, and use the data responsibly.

### Who built this

I build scrapers for my own projects and publish the ones that turn out genuinely useful. This is one of them. If you need a custom scraper, a data pipeline, or a change to this actor, I'm available for freelance work.

GitHub: [github.com/bujhmml](https://github.com/bujhmml) · Site: [bujhmml.fun](https://bujhmml.fun)

# Actor input Schema

## `sources` (type: `array`):

Boards to scrape. Each item can be: an object `{ "ats": "greenhouse|lever|ashby", "company": "<slug>" }`, a pasted board URL (e.g. https://boards.greenhouse.io/stripe, https://jobs.lever.co/spotify, https://jobs.ashbyhq.com/ramp), or a bare company slug as a string (the ATS is auto-detected by probing all three platforms). The slug is the identifier in the careers URL: boards.greenhouse.io/<stripe>.

## `titleKeyword` (type: `string`):

Keep only jobs whose title contains this text (case-insensitive). Filtered-out rows are not billed.

## `locationKeyword` (type: `string`):

Keep only jobs whose location contains this text (case-insensitive). Filtered-out rows are not billed.

## `department` (type: `string`):

Keep only jobs whose department or team contains this text (case-insensitive). Filtered-out rows are not billed.

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

Keep only jobs detected as fully remote. Filtered-out rows are not billed.

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

Stop after this many job rows across all boards. 0 = no limit.

## `impersonate` (type: `string`):

curl\_cffi impersonation target used for requests.

## Actor input object example

```json
{
  "sources": [
    {
      "ats": "greenhouse",
      "company": "stripe"
    },
    {
      "ats": "lever",
      "company": "spotify"
    },
    {
      "ats": "ashby",
      "company": "ramp"
    }
  ],
  "remoteOnly": false,
  "maxItems": 100,
  "impersonate": "chrome"
}
```

# 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 = {
    "sources": [
        {
            "ats": "greenhouse",
            "company": "stripe"
        },
        {
            "ats": "lever",
            "company": "spotify"
        },
        {
            "ats": "ashby",
            "company": "ramp"
        }
    ],
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("bujhmml/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 = {
    "sources": [
        {
            "ats": "greenhouse",
            "company": "stripe",
        },
        {
            "ats": "lever",
            "company": "spotify",
        },
        {
            "ats": "ashby",
            "company": "ramp",
        },
    ],
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("bujhmml/ats-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sources": [
    {
      "ats": "greenhouse",
      "company": "stripe"
    },
    {
      "ats": "lever",
      "company": "spotify"
    },
    {
      "ats": "ashby",
      "company": "ramp"
    }
  ],
  "maxItems": 100
}' |
apify call bujhmml/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=bujhmml/ats-jobs-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/acts/V7atnH5iHhZ4agtjQ/builds/DCr6fSU6Ei4URnTlJ/openapi.json
