# Greenhouse, Lever & Ashby Job Scraper API (`arjun_code/ats-job-scraper`) Actor

Collect current job postings from Greenhouse, Lever, and Ashby company boards in one source-traceable dataset. Get stable job IDs, full descriptions, locations, compensation, timestamps, apply links, raw source data, and per-board notices for job boards, HR tech, and automation workflows.

- **URL**: https://apify.com/arjun_code/ats-job-scraper.md
- **Developed by:** [Arjun Data](https://apify.com/arjun_code) (community)
- **Categories:** Jobs, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 dataset results

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 API** turns a verified list of company job boards into one source-traceable current-jobs Dataset. Mix Greenhouse, Lever global or EU, and Ashby boards in one run, then export the same output contract to your database, job site, HR product, or automation workflow.

The Actor reads the ATS vendors' public job APIs and does not require ATS credentials. It is designed as an ingestion component for teams that already know which companies they cover. It preserves full descriptions, stable source-scoped IDs, source-native compensation, timestamp meaning, raw source records, and the exact input behind every result.

This independently developed Actor is not affiliated with, sponsored by, or endorsed by Greenhouse, Lever, or Ashby.

### Quick start

1. Open the Actor, paste one or more hosted Greenhouse, Lever, or Ashby job board URLs into **Verified company job board URLs**, and optionally set `userData.companyName`.
2. Keep **Include Greenhouse salary details** disabled for the fastest run, or enable it when you need structured Greenhouse pay ranges.
3. Select **Start**. Open **Results** for job rows and readable notices, or export the Dataset as JSON, CSV, or Excel.

The same input can be run through the Apify API, scheduled, or connected to n8n, Make, Zapier, webhooks, and other Apify integrations.

### Pricing

This Actor uses pay-per-event pricing with platform usage included:

| Event | Price |
|---|---:|
| Actor start | `$0.00005` per GB of configured memory, minimum one event; the default 1 GB run is `$0.00005` |
| Dataset result | `$0.001` per row, or `$1.00` per 1,000 rows |

A Dataset result is either a job record or a board notice. With the default 1 GB memory, a run returning 1,000 jobs costs about `$1.00005`. Empty or unavailable boards can produce one notice row so the result explains what happened instead of silently returning an empty Dataset. Set a maximum run charge in Apify when you need a hard spending limit.

### Who this is for

- Automation studios and data consultants delivering company-list-to-job-data workflows for clients.
- Custom job boards, talent marketplaces, and job communities with a curated employer list and their own job database.
- HR tech, matching, recommendation, and AI products that already select the companies they cover and need current postings as an input.

The best fit is a recurring workflow that can consume JSON, Dataset exports, or the Apify API and owns its downstream storage. This version produces a current snapshot. It does not discover companies, maintain cross-run job history, send new-job alerts, search the whole job market, or access applicants and internal ATS data.

### What the Actor gives your pipeline

- One mixed-ATS input and one documented result shape.
- `job_key` for source-scoped database upserts without merging unrelated cross-platform postings.
- Complete source descriptions, application links, explicit workplace fields, and available compensation.
- Separate publication, creation, update, and observation timestamps instead of treating them as interchangeable.
- `source_record` for fields your downstream product needs beyond the normalized core.
- Per-input traceability, readable notice rows, and a run summary that distinguishes successful, empty, duplicate, and failed boards.

### Input

```json
{
  "startUrls": [
    {"url": "https://jobs.ashbyhq.com/ashby", "userData": {"companyName": "Ashby"}},
    {"url": "https://job-boards.greenhouse.io/greenhouse", "userData": {"companyName": "Greenhouse"}},
    {"url": "https://jobs.lever.co/spotify", "userData": {"companyName": "Spotify"}}
  ],
  "includeGreenhouseDetails": false
}
```

| Field | Description |
|---|---|
| `startUrls` | Required, nonempty list of hosted company job board URLs. Each entry has a `url` property and can optionally set `userData.companyName` to a verified display name. |
| `includeGreenhouseDetails` | Boolean, default `false`. When enabled, fetch each Greenhouse job's details to include published structured pay ranges. Adds one request per Greenhouse job. Descriptions are already included in the basic mode. |

Supported hosts: `jobs.ashbyhq.com`, `boards.greenhouse.io`, `job-boards.greenhouse.io`, `jobs.lever.co`, and `jobs.eu.lever.co`.

For `startUrls`, the Actor reads only `url` and the optional `userData.companyName`. Method, headers, payload, unique key, label, and other fields shown by Apify's generic URL editor under **Advanced settings** are ignored.

Use the exact board identifier from the company's actual careers link, including capitalization. Custom company careers domains must first be resolved to their hosted ATS board. All current listed jobs on each input board are collected; duplicate boards in the same run are skipped. Ashby jobs with `isListed=false` are excluded.

### Output

The default **Results** table contains job records and notices. `record_type=job` identifies a source job posting. `record_type=notice` identifies an unavailable or empty job board, with a machine-readable `notice_code`, readable `message`, its input URL and available HTTP status. Notices have no job ID and are not counted as jobs. Similar postings across platforms are not merged. Additional **Job details**, **Compensation & source**, and **Raw source** views expose the full output without crowding the default table.

| Fields | Meaning |
|---|---|
| `record_type` | `job` for an actual posting; `notice` for a board-level message. Filter to `job` when importing postings into another system. |
| `notice_code`, `message`, `http_status` | For notices: machine-readable reason, suggested action, and the HTTP status when available. |
| `job_key`, `source_job_id` | Source-scoped key (`source:region:board:id`) and original job ID. |
| `input_position`, `input_url`, `input_company_name` | Position in `startUrls`, its job board URL, and the optional `userData.companyName`. Duplicate boards are attributed to the first collected input. |
| `source`, `region`, `board` | ATS, global/EU region, and company board identifier. |
| `company`, `company_source` | Display name priority is input `userData.companyName` (`input`), API company name (`api`), then job board identifier (`board_identifier`). A board identifier such as `claylabs` is a label, not a verified company name. |
| `title` | Source job title. |
| `job_url`, `apply_url`, `apply_url_kind` | Job page and application entry point. `direct` identifies a source-provided application URL; `job_page` identifies the job page as the entry point. |
| `description_html`, `description_text` | Full source description and its text representation. Lever descriptions include the section lists, additional content, and available salary description. |
| `locations`, `departments`, `team`, `employment_type` | Job locations and organizational fields. Greenhouse locations use the job's location label; office hierarchy remains in `source_record`. |
| `workplace_type` | `remote`, `hybrid`, `on_site`, or `null`, based on explicit source fields. |
| `source_published_at`, `publication_time_kind` | Greenhouse first publication or Ashby last publication; otherwise `null`. |
| `source_updated_at`, `source_created_at`, `observed_at` | Source update/creation timestamps when available, plus this collection's UTC observation time. |
| `compensation`, `compensation_source` | The source-native compensation structure and its origin. |
| `source_record` | The original source job record. Greenhouse detail mode preserves both `list` and `detail`. |

Example job result:

```json
{
  "record_type": "job",
  "job_key": "ashby:global:ashby:example-id",
  "source_job_id": "example-id",
  "input_position": 1,
  "input_url": "https://jobs.ashbyhq.com/ashby",
  "input_company_name": "Ashby",
  "source": "ashby",
  "region": "global",
  "board": "ashby",
  "company": "Ashby",
  "company_source": "input",
  "title": "Software Engineer",
  "job_url": "https://jobs.ashbyhq.com/ashby/example-id",
  "apply_url": "https://jobs.ashbyhq.com/ashby/example-id/application",
  "locations": [{"name": "Remote", "country": "US"}],
  "workplace_type": "remote",
  "observed_at": "2026-10-24T12:00:00+00:00"
}
```

Example notice result:

```json
{
  "record_type": "notice",
  "notice_code": "board_not_found",
  "message": "Job board not found (HTTP 404). Check the company job board URL and its spelling.",
  "input_position": 2,
  "input_url": "https://boards.greenhouse.io/example",
  "http_status": 404,
  "source": "greenhouse",
  "board": "example"
}
```

Keep source salary units and time semantics when using the output:

- Greenhouse `min_cents` and `max_cents` remain in cents. Do not assume an annual period when the source does not supply one.
- Ashby compensation can contain multiple tiers, locations, currencies and components. A nonempty object can still contain no salary amount.
- Lever `createdAt` is a creation timestamp, not a publication date. Zero-valued salary ranges are retained as source values.
- Missing optional fields stay empty. Workplace type is not inferred from descriptions, and `observed_at` does not mean first seen.

### Run summary and failures

Open **Results** to see jobs and notices directly. **Run summary & notices** opens the `RUN_SUMMARY` key-value record with overall counts, the applied options, and one result for every input entry. Duplicate entries use `status=skipped_duplicate` and identify the first input position. `jobs_saved` counts only job postings; `notices_saved` counts notices. Dataset item count is their sum. Read the summary's `message` for the overall result and `warnings` for the affected URLs and helpful next steps.

Handled job board failures, including an HTTP 404 or all boards being unavailable, finish with an Apify run status of **SUCCEEDED** and a **Completed with warnings** status message. Other boards continue to be collected. Each unavailable board adds a notice row to Results, such as: "Job board not found (HTTP 404). Check the company job board URL and its spelling." A valid board with no public jobs also adds a notice explaining the empty result.

`RUN_SUMMARY.status` describes collection completeness: `succeeded` means every board was read, `partial` means some were read, and `failed` means none could be read. It is separate from the Apify run status. A valid board with no public jobs is a successful collection and receives an explicit empty-result message. Input validation and Dataset write errors can still fail the run.

Input errors are reported clearly. `includeGreenhouseDetails` must be a JSON boolean, not the string `"false"`. Each run produces its own current snapshot. Historical changes, company discovery, applicant data and job matching are outside this version's scope.

### Use the snapshot in your database

For each run:

1. Import only rows with `record_type=job` and upsert them on `job_key`.
2. Group records by `input_url` or by `source`, `region`, and `board`.
3. Read `RUN_SUMMARY.boards` before reconciling jobs that disappeared. A successfully read board is a complete current snapshot for that board. A failed board is an unavailable source for this run and must not be interpreted as having no open jobs.
4. Treat an empty but successfully read board as having no current public jobs. Its notice explains the empty result.

This lets a downstream database manage its own current-state or history policy without losing the source evidence used to make that decision.

### Export and integration

Use the Dataset export controls to download JSON, CSV or Excel, or access the Dataset through the Apify API. The full JSON retains nested source locations and compensation. Apify schedules and integrations can pass each completed run to n8n, Make, Zapier, a webhook, or your own application. For spreadsheet use, select the fields needed by your workflow.

### FAQ and support

#### Does this Actor need Greenhouse, Lever, or Ashby credentials?

No. It reads public company job board APIs and does not access applicants, private openings, recruiter accounts, or internal ATS data.

#### Why did a successful run return a notice instead of jobs?

The notice explains that a board was empty, unavailable, unsupported, or entered incorrectly. Check `notice_code`, `message`, and `RUN_SUMMARY.boards`. Do not interpret a failed board as having no open jobs.

#### Can I use a company's custom careers domain?

Use the hosted ATS URL behind the careers page. Supported hosts are `jobs.ashbyhq.com`, `boards.greenhouse.io`, `job-boards.greenhouse.io`, `jobs.lever.co`, and `jobs.eu.lever.co`.

#### Can I monitor new and removed jobs?

Each run is a current snapshot. Schedule the Actor and compare snapshots in your own database using `job_key`. The Actor does not retain cross-run history or send alerts in this version.

#### Is it legal to use the data?

The Actor retrieves publicly available job postings. You are responsible for your use of the data and for complying with the source websites' terms and applicable laws.

For help, open an issue from the Actor's **Issues** tab and include the input URL, run ID, and the relevant notice or `RUN_SUMMARY` entry. Do not include credentials or private applicant information.

# Actor input Schema

## `startUrls` (type: `array`):

Paste exact hosted Ashby, Greenhouse or Lever job board URLs from your curated company list. Optionally set userData.companyName to a verified display name. Only url and userData.companyName are used; other fields in the URL editor's Advanced settings are ignored. The Actor returns a current snapshot for ingestion into your own job database, website, or automation workflow, with every result traced to its input. Lever EU boards are supported.

## `includeGreenhouseDetails` (type: `boolean`):

Request each Greenhouse job's details to include published pay ranges when available. This adds one request per Greenhouse job. Descriptions are already included without this option.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://jobs.ashbyhq.com/ashby",
      "userData": {
        "companyName": "Ashby"
      }
    },
    {
      "url": "https://job-boards.greenhouse.io/greenhouse",
      "userData": {
        "companyName": "Greenhouse"
      }
    },
    {
      "url": "https://jobs.lever.co/spotify",
      "userData": {
        "companyName": "Spotify"
      }
    }
  ],
  "includeGreenhouseDetails": false
}
```

# Actor output Schema

## `results` (type: `string`):

Job records and visible notices for unavailable or empty job boards. Use record_type to distinguish jobs from notices.

## `runSummary` (type: `string`):

Read this if no jobs were returned or any boards were skipped: completion message, helpful notices and per-board results.

# 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 = {
    "startUrls": [
        {
            "url": "https://jobs.ashbyhq.com/ashby",
            "userData": {
                "companyName": "Ashby"
            }
        },
        {
            "url": "https://job-boards.greenhouse.io/greenhouse",
            "userData": {
                "companyName": "Greenhouse"
            }
        },
        {
            "url": "https://jobs.lever.co/spotify",
            "userData": {
                "companyName": "Spotify"
            }
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arjun_code/ats-job-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 = { "startUrls": [
        {
            "url": "https://jobs.ashbyhq.com/ashby",
            "userData": { "companyName": "Ashby" },
        },
        {
            "url": "https://job-boards.greenhouse.io/greenhouse",
            "userData": { "companyName": "Greenhouse" },
        },
        {
            "url": "https://jobs.lever.co/spotify",
            "userData": { "companyName": "Spotify" },
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("arjun_code/ats-job-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 '{
  "startUrls": [
    {
      "url": "https://jobs.ashbyhq.com/ashby",
      "userData": {
        "companyName": "Ashby"
      }
    },
    {
      "url": "https://job-boards.greenhouse.io/greenhouse",
      "userData": {
        "companyName": "Greenhouse"
      }
    },
    {
      "url": "https://jobs.lever.co/spotify",
      "userData": {
        "companyName": "Spotify"
      }
    }
  ]
}' |
apify call arjun_code/ats-job-scraper --silent --output-dataset

```

## MCP server setup

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