# ATS Job Board Scraper - Greenhouse, Lever, Ashby, Workable (`eszetael_lab/ats-jobs-scraper`) Actor

Beta (v0.1): Scrape live job postings directly from Greenhouse, Lever, Ashby, and Workable career sites with zero index delay. Deduplicates multi-city postings into one record, filters full job descriptions without timeouts, tracks NEW/ACTIVE/CLOSED hiring signals, and supports a free dryRun count.

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

## Pricing

$0.75 / 1,000 job posting records

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

## ATS Job Board Scraper - Greenhouse, Lever, Ashby, Workable

The **ATS Job Board Scraper** is an ultra-fast **company career site job listing API** and **Greenhouse, Lever, Ashby, and Workable job scraper** designed as a real-time **new and expired jobs tracker** with built-in **hiring signals**.

It connects directly to the public career site endpoints used by modern companies to publish their job openings, providing structured, clean, and deduplicated job data with zero indexing latency.

***

### What It Does

- **Direct ATS Integration**: Extracts live job postings straight from public JSON feeds of **Greenhouse**, **Lever**, **Ashby**, and **Workable**.
- **Zero Index Delay**: Unlike secondary job aggregators that refresh hourly or daily, data is fetched in real time directly from the employer's applicant tracking system, preserving exact `postedAt` timestamps.
- **Flexible Company Input**: Accepts bare company slugs (e.g. `stripe`, `notion`, `figma`) or full career board URLs (`boards.greenhouse.io/stripe`, `jobs.lever.co/netflix`, `jobs.ashbyhq.com/linear`, `apply.workable.com/invision`). Bare slugs are automatically probed across all supported ATS platforms.
- **Deep In-Memory Filtering**:
  - **Title keywords**: Include and exclude keywords (case-insensitive substring matching).
  - **Full description keywords**: Filter by full job description locally in-memory — instant, reliable, and completely immune to network timeouts.
  - **Location & country filtering**: Built-in normalization for city aliases (`Munich` = `München`, `Cologne` = `Köln`, `Warsaw` = `Warszawa`) and country codes/names (`US`, `UK`, `PL`, `DE`, `CA`, etc.).
  - **Workplace type**: Target `remote`, `hybrid`, or `onsite` roles.
- **Hiring Signals & Delta Tracking**:
  - Automatically identifies whether an opening is `NEW`, `ACTIVE`, or `CLOSED`.
  - Tracks `firstSeenAt` and computes `daysOpen` for every job.
  - **Delta Mode (`onlyNew`)**: Emits only newly posted jobs since the previous run.
  - **Closed Jobs Tracking (`emitClosed`)**: Emits jobs that disappeared since the last run to detect hiring freezes or filled positions.
- **Multi-Location Deduplication**: Postings advertised across multiple cities are consolidated into a single record with a clean `locations[]` list and ISO-2 `countries[]` array.
- **Free Pre-Flight Check (`dryRun`)**: Verify filters and count matching listings without pushing dataset items or incurring event charges.

***

### Why Choose Us

| Feature | Typical Market Alternatives | ATS Job Board Scraper |
|---|---|---|
| **Data Freshness** | Stale secondary indexes (1–2 hour delay) | **Direct from source (zero index delay)** |
| **Delta & Expired Jobs** | Paid add-on ($20+/month separate actor) | **Included natively in standard pricing** |
| **Full Description Filtering** | Slow or causes gateway timeouts | **Fast, in-memory local filtering (no timeouts)** |
| **Multi-City Postings** | Emits separate records (charges multiple times) | **1 job = 1 record with `locations[]` array** |
| **Pre-Flight Estimation** | Not available | **Free `dryRun` mode ($0 cost)** |
| **Company Discovery** | Unverified bloated lists | **Precise targeting via your slugs or board URLs** |

#### Specific Advantages

1. **Direct from the Source with Zero Index Delay**:
   Many scrapers query third-party aggregators whose indexes lag by 1 to 2 hours. This actor queries the public career feeds of the ATS providers directly, delivering openings the moment they go live with accurate source timestamps.
2. **Delta Mode & Closed Job Detection Included**:
   Tracking expired roles and changes typically requires paying for an auxiliary actor or a costly monthly subscription. Our scraper tracks state per board in the Key-Value Store (`ats-state`), delivering `NEW`, `ACTIVE`, and `CLOSED` hiring signals at no extra monthly fee.
3. **Full-Description Filtering Without Timeouts**:
   Competitors often warn that filtering through full job descriptions is *"VERY intensive, risk of time-out"* or disable it entirely. We fetch the complete payload and filter locally in memory, giving you fast, bulletproof keyword matching across the full text.
4. **Consolidated Multi-City Postings**:
   When an employer lists a single position across multiple locations (e.g., London, Berlin, and Paris), other scrapers create separate duplicate items and charge you for each one. We normalize the listing into a single record containing a unified `locations[]` array, saving you budget and downstream cleanup effort.
5. **Free `dryRun` Verification**:
   Test new keyword combinations, location filters, or company lists without spending your budget. Setting `"dryRun": true` calculates matching job counts and stores the breakdown in your Key-Value Store under `DRY_RUN` at $0 cost.

***

### Input

Configure your target companies, platforms, filters, and run modes using standard JSON input:

```json
{
  "companies": [
    "stripe",
    "airbnb",
    "https://jobs.ashbyhq.com/linear",
    "https://apply.workable.com/invision"
  ],
  "atsPlatforms": [
    "greenhouse",
    "lever",
    "ashby",
    "workable"
  ],
  "titleInclude": ["engineer", "developer"],
  "titleExclude": ["intern", "senior"],
  "descriptionInclude": ["python", "typescript"],
  "descriptionExclude": ["clearance required"],
  "locations": ["US", "remote", "Berlin"],
  "workplaceType": "remote",
  "onlyNew": false,
  "emitClosed": false,
  "dryRun": false,
  "includeDescription": true,
  "maxItems": 500
}
```

#### Input Parameters Reference

| Field | Type | Default | Description |
|---|---|---|---|
| `companies` | `string[]` | *Required* | List of company slugs (e.g. `"stripe"`, `"notion"`) or full board URLs (`boards.greenhouse.io/...`, `jobs.lever.co/...`, `jobs.ashbyhq.com/...`, `apply.workable.com/...`). |
| `atsPlatforms` | `string[]` | `["greenhouse", "lever", "ashby", "workable"]` | Priority order of ATS platforms to check when a bare slug is provided. |
| `titleInclude` | `string[]` | `[]` | Keep only jobs whose title contains at least one of these keywords (case-insensitive). |
| `titleExclude` | `string[]` | `[]` | Exclude any job whose title contains any of these keywords. |
| `descriptionInclude` | `string[]` | `[]` | Keep only jobs whose full description contains at least one of these keywords (evaluated locally). |
| `descriptionExclude` | `string[]` | `[]` | Exclude any job whose description contains any of these keywords. |
| `locations` | `string[]` | `[]` | Filter by city, state, or country with automatic alias expansion (e.g., `"US"`, `"UK"`, `"Munich"`, `"NY"`). |
| `workplaceType` | `string` | `""` | Filter by workplace type: `"remote"`, `"hybrid"`, `"onsite"`, or `""` for any. |
| `onlyNew` | `boolean` | `false` | Delta mode: emit only newly added jobs not present in the previous run. |
| `emitClosed` | `boolean` | `false` | Also emit postings that were removed since the previous run with `hiringSignal="CLOSED"`. |
| `dryRun` | `boolean` | `false` | Count matching jobs for free. Writes count breakdown to Key-Value Store key `DRY_RUN` and emits 0 dataset items ($0 charge). |
| `includeDescription` | `boolean` | `true` | Include plain-text job description in the `descriptionText` field. |
| `maxItems` | `integer` | `1000` | Maximum number of records to return (controls maximum spend per run; `0` = safety cap of 50,000). |

***

### Output

Each scraped job posting is normalized into a flat, consistent schema ready for databases, spreadsheets, or AI pipelines:

```json
{
  "id": "gh-6124589002",
  "atsPlatform": "greenhouse",
  "companySlug": "stripe",
  "companyName": "Stripe",
  "title": "Staff Software Engineer - Infrastructure",
  "department": "Engineering",
  "team": "Core Platform",
  "employmentType": "full_time",
  "workplaceType": "remote",
  "locations": [
    "San Francisco, CA",
    "Seattle, WA",
    "Remote - US"
  ],
  "countries": [
    "US"
  ],
  "salaryMin": 185000,
  "salaryMax": 235000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "postedAt": "2026-10-24T14:30:00Z",
  "updatedAt": "2026-10-25T09:15:00Z",
  "firstSeenAt": "2026-10-24T15:00:00Z",
  "daysOpen": 2,
  "applyUrl": "https://boards.greenhouse.io/stripe/jobs/6124589002#app",
  "jobUrl": "https://boards.greenhouse.io/stripe/jobs/6124589002",
  "descriptionText": "As a Staff Software Engineer on Core Platform, you will design and scale...",
  "hiringSignal": "ACTIVE",
  "scrapedAt": "2026-10-26T12:00:00Z"
}
```

***

### Pricing

This Actor operates on a transparent **Pay-Per-Event (PPE)** model: you only pay for the exact job records successfully delivered to your dataset.

- **Standard Price**: **$0.00075 per job** (**$0.75 per 1,000 jobs**)
- **Actor Startup Cost**: **$0.00** (Free to start)
- **`dryRun` Pre-Flight Check**: **$0.00** (Zero dataset items = zero charges)

> **No Hidden Fees**: Unlike tools requiring a separate monthly subscription to track expired positions or delta changes, delta monitoring (`onlyNew`) and closed job signals (`emitClosed`) are included at standard per-record rates.

***

### FAQ

#### 1. Which ATS platforms are supported and how are they identified?

We support **Greenhouse**, **Lever**, **Ashby**, and **Workable**. If you pass a full career page URL (such as `jobs.ashbyhq.com/linear`), the platform is identified automatically. If you provide a bare slug (such as `stripe`), the scraper sequentially checks each platform until it finds an active board.

#### 2. Why is SmartRecruiters not supported?

SmartRecruiters' API endpoint explicitly specifies `Disallow: /` in its `robots.txt` file. We strictly respect web scraping ethics and legal compliance rules, meaning we do not bypass `robots.txt` restrictions. As a result, SmartRecruiters is deliberately excluded.

#### 3. How do delta mode (`onlyNew`) and hiring signals work?

The scraper stores job board state in an Apify Key-Value Store named `ats-state`. In delta mode (`onlyNew: true`), it compares current active openings against previously observed jobs, emitting only newly listed positions. When `emitClosed: true` is enabled, it additionally emits positions that have closed or been unlisted, marking them with `hiringSignal: "CLOSED"` and calculating total `daysOpen`.

#### 4. How does the free `dryRun` mode work?

When `"dryRun": true` is enabled, the scraper contacts the job boards, applies your filters, and calculates the exact number of matching postings. Instead of writing items to the dataset, it saves a breakdown object (`{count, perCompany}`) to the Key-Value Store key `DRY_RUN`. Because zero dataset items are written, you are billed **$0.00**.

#### 5. How are multi-location postings handled?

Employers often post a single position with multiple eligible cities (e.g., London, Paris, and Remote). Rather than splitting these into multiple entries and billing you multiple times, our normalization engine merges all cities into a clean `locations: [...]` list and deduplicated ISO-2 `countries: [...]` array.

***

### Legality & Compliance

- **Publicly Accessible Data**: The Actor extracts listings solely from public career endpoints made freely available by ATS vendors for embedding job listings on company career pages.
- **No Authentication Bypass**: The scraper does not access private accounts, bypass login walls, crack CAPTCHAs, or circumvent paywalls.
- **Privacy & GDPR Compliance**: The output contains only public job vacancy information. Recruiter contact information (personal names, emails, phone numbers) and applicant data are never collected or stored.
- **Ethical Crawling**: Honors `robots.txt` directives across all endpoints, respects crawl delays (including Lever's 1-second delay), bounds concurrency to a maximum of 4 requests, respects `Retry-After` headers, and declares a transparent User-Agent.
- **User Responsibility**: The Actor collects publicly posted job requisitions. Users are responsible for ensuring that their downstream use and storage comply with third-party source terms of service and applicable local regulations (e.g. respecting Workable's `Content-Signal: ai-train=no`).

***

### Feedback & Reviews

If this Actor saved you time or powered your hiring data pipeline, **please consider leaving a rating and review on the Apify Store**! Your feedback helps us maintain these integrations, add new ATS platforms, and continually improve data quality. If you encounter any issues or have feature suggestions, feel free to open an issue in the Actor's repository.

# Actor input Schema

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

Company board slugs (e.g. 'stripe', 'airbnb', 'notion') or full board URLs (boards.greenhouse.io/x, jobs.lever.co/x, jobs.ashbyhq.com/x, apply.workable.com/x). Bare slugs are probed across the selected atsPlatforms until the first match. SmartRecruiters is disabled (robots.txt Disallow: /).

## `atsPlatforms` (type: `array`):

Which ATS platforms to try (in order) when an entry in 'companies' is a bare slug rather than a full URL. Defaults to all 4 supported platforms.

## `titleInclude` (type: `array`):

Keep only jobs whose title contains at least one of these substrings (case-insensitive). Leave empty to include all titles.

## `titleExclude` (type: `array`):

Exclude any job whose title contains any of these substrings (case-insensitive).

## `descriptionInclude` (type: `array`):

Keep only jobs whose full plain-text description contains at least one of these substrings (case-insensitive). Evaluated locally without extra requests or timeouts.

## `descriptionExclude` (type: `array`):

Exclude any job whose full plain-text description contains any of these substrings (case-insensitive).

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

Filter by country codes, states, or cities with built-in alias expansion (e.g. 'US'='USA'='United States', 'UK'='GB'='United Kingdom', 'PL'='Poland'='Polska', 'NY'='New York', 'Munich'='München').

## `workplaceType` (type: `string`):

Filter by workplace arrangement (remote, hybrid, onsite), or leave empty for any.

## `onlyNew` (type: `boolean`):

Emit only jobs that were not seen in the previous run for each company board (state stored in the 'ats-state' Key-Value Store).

## `emitClosed` (type: `boolean`):

Also emit jobs that disappeared since the previous run as hiringSignal='CLOSED', preserving firstSeenAt and computing daysOpen.

## `dryRun` (type: `boolean`):

Fetch and filter jobs, then write {count, perCompany} to Key-Value Store key 'DRY_RUN' without pushing dataset items or charging PPE events ($0 cost).

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

Include plain-text job description ('descriptionText') in output records. Description filters always run before stripping.

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

Hard cap on delivered records (0 = up to safety cap of 50,000). Controls your maximum PPE spend per run.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "airbnb",
    "notion"
  ],
  "atsPlatforms": [
    "greenhouse",
    "lever",
    "ashby",
    "workable"
  ],
  "workplaceType": "",
  "onlyNew": false,
  "emitClosed": false,
  "dryRun": false,
  "includeDescription": true,
  "maxItems": 100
}
```

# Actor output Schema

## `allJobs` (type: `string`):

Normalized, multi-city deduplicated job records across Greenhouse, Lever, Ashby, and Workable.

## `hiringSignals` (type: `string`):

Compact view of hiring activity with signal, company, title, locations, firstSeenAt, daysOpen, and applyUrl.

## `dryRunSummary` (type: `string`):

Count of matching jobs total and per company stored in Key-Value Store key DRY_RUN.

# 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",
        "airbnb",
        "notion"
    ],
    "atsPlatforms": [
        "greenhouse",
        "lever",
        "ashby",
        "workable"
    ],
    "maxItems": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("eszetael_lab/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 = {
    "companies": [
        "stripe",
        "airbnb",
        "notion",
    ],
    "atsPlatforms": [
        "greenhouse",
        "lever",
        "ashby",
        "workable",
    ],
    "maxItems": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("eszetael_lab/ats-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",
    "airbnb",
    "notion"
  ],
  "atsPlatforms": [
    "greenhouse",
    "lever",
    "ashby",
    "workable"
  ],
  "maxItems": 100
}' |
apify call eszetael_lab/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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