# ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workable (`ntriqpro/ats-jobs-scraper`) Actor

Live ATS jobs scraper for Greenhouse, Lever, Ashby and Workable. Paste company slugs or careers URLs, get every open role — title, location, remote, department, salary, apply URL, posting age — read live with no 5,000-job cap, plus ghost-job risk, seniority and a hiring-momentum summary.

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

## Pricing

Pay per event

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?

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

## ATS Jobs Scraper — Greenhouse, Lever, Ashby & Workable

Paste a list of companies. Get back **every open role on their job board**, read **live at the moment you run it**, with a judged one-line verdict on each posting.

No database of yesterday's jobs. No 5,000-job ceiling. No API key.

```
in  →  ["https://boards.greenhouse.io/dropbox", "https://jobs.lever.co/binance"]
out →  one row per open role  +  one hiring-momentum summary per company
```

***

### Why this one

Most ATS scrapers on the store hand you a **cached copy of someone else's crawl**, capped at a few thousand rows per run, and then point you at a paid API when you need more. This Actor calls the company's own job board at run time, so:

| | This Actor |
|---|---|
| **Freshness** | Read live, per run. A role posted an hour ago is in your first run. |
| **Per-run job limit** | **None.** A 824-posting board comes back whole. |
| **Coverage** | Greenhouse, Lever, Ashby and Workable, auto-detected from a slug or URL. |
| **Judgement** | Every row is scored, not just copied — see below. |

#### The judgement layer

Other scrapers give you fields. This one gives you a verdict:

- **`headline`** — the row in one line: `Senior Backend Engineer — Remote (United States), posted 3d ago, $180k–220k`
- **`freshness`** — `fresh` (≤30d), `aging` (≤90d), `stale` (older)
- **`postedAgeDays`** — how long this role has actually been open
- **`ghostJobRisk`** — `none` / `low` / `medium` / `high`, with the reasons in `ghostJobFlags`: open over 90 or 180 days, never updated, or a duplicate title sitting on the same board
- **`seniority`** — `intern` / `junior` / `senior` / `principal` / `manager` / `director` / `executive`, read from the title
- **`salary`** — `min`, `max`, `currency` parsed out of whatever the board published
- **`remote`** and **`workplaceType`** — normalised across four boards that each say it differently
- **`keywordHits`** — which of your keywords this role matched

And once per company, a **`company-summary`** row: department and seniority mix, remote share, average posting age, top locations, and a **hiring-momentum** verdict — `hiring hard`, `steady`, `slowing` or `stalled`, based on how many roles opened in the last 30 days.

***

### Input

| Field | Type | Description |
|---|---|---|
| `companies` | array | Company slugs or careers-page URLs. Greenhouse, Lever, Ashby and Workable are detected automatically. |
| `company` | string | Alternative to the list — a single company. |
| `keywords` | array | Keep only roles whose **title** contains one of these words. Empty = every role. |
| `locations` | array | Keep only roles whose location mentions one of these (`Remote` works). Empty = everywhere. |
| `postedWithinDays` | integer | Keep only roles first published within N days. `0` = no date filter. |
| `maxJobsPerCompany` | integer | Ceiling per company. **`0` = unlimited**, which is the default. |
| `includeDescription` | boolean | Add the full job description as plain text. Off by default — descriptions are large. |
| `monitorMode` | boolean | Return only what changed since your previous run (default `false`). See **Monitor mode** below. |
| `monitorKey` | string | Optional name for the watch, e.g. `competitor-hiring-weekly`. Empty = derived from your input. |
| `includeUnchanged` | boolean | Monitor mode only — also return roles that are still open and unchanged (default `false`). |
| `disclaimerAck` | boolean | Acknowledges how this Actor sources data (see *Data sources & responsibility*). |

Accepted address formats:

```
dropbox                                      (bare slug — probed across all four boards)
https://boards.greenhouse.io/dropbox
https://job-boards.greenhouse.io/stripe/jobs/12345
https://jobs.lever.co/binance
https://jobs.ashbyhq.com/linear
https://apply.workable.com/blueground
```

A URL is always faster and unambiguous. A bare slug costs up to four probe requests.

#### Example

```json
{
  "companies": [
    "https://boards.greenhouse.io/databricks",
    "https://jobs.ashbyhq.com/linear"
  ],
  "keywords": ["engineer", "designer"],
  "locations": ["Remote", "United States"],
  "postedWithinDays": 30,
  "maxJobsPerCompany": 0,
  "includeDescription": false,
  "disclaimerAck": true
}
```

***

### Output

One row per open role (`rowType: "job"`):

| Field | Description |
|---|---|
| `headline` | The one-line verdict |
| `company`, `companySlug`, `inputTarget` | Who this role belongs to, and what you typed |
| `ats`, `atsLabel` | `greenhouse` / `lever` / `ashby` / `workable` |
| `jobTitle` | Role title |
| `location`, `locations` | Primary location and every listed location |
| `remote`, `workplaceType` | `true`/`false`/`null`, and `remote` / `hybrid` / `onsite` |
| `department`, `departments` | Team and department names |
| `employmentType` | Full-time, contract, intern, … where the board publishes it |
| `postedAt`, `updatedAt` | ISO dates from the board |
| `postedAgeDays`, `updatedAgeDays` | Age in days |
| `freshness` | `fresh` / `aging` / `stale` / `unknown` |
| `ghostJobRisk`, `ghostJobFlags` | Risk band and the reasons behind it |
| `seniority` | Inferred from the title |
| `salary` | `{ min, max, currency, raw }` when the posting publishes pay |
| `keywordHits` | Your keywords that matched |
| `applyUrl` | Direct application link |
| `jobId`, `requisitionId` | Board identifiers |
| `jobDescription` | Plain text, when `includeDescription` is on |
| `sourceUrl`, `scrapedAt` | Exactly which endpoint was read, and when |
| `changeType`, `changeDetail` | Monitor mode only — `new` / `changed` / `removed` / `unchanged`, and what moved |

One row per company (`rowType: "company-summary"`): `hiringMomentum`, `hiringMomentumReason`, `totalJobsOnBoard`, `jobsMatchingFilters`, `jobsDelivered`, `remoteJobs`, `remoteSharePercent`, `newLast30DaysPercent`, `averagePostingAgeDays`, `jobsWithPublishedSalary`, `departmentDistribution`, `seniorityDistribution`, `topLocations`.

Rows with `rowType: "notice"` are status messages — an address with no board behind it, a company with no open roles, a limit reached. **Notice rows are never charged.**

Invalid input ends the run as SUCCEEDED with a notice row.

***

### Monitor mode (weekly change alerts)

Hiring is a moving target. Turn on **Monitor mode** and the Actor compares this run with your previous run and returns only what moved on those boards:

| `changeType` | Meaning |
|---|---|
| `new` | A role that was not on the board the last time you ran this — the one recruiters and job-seekers actually want |
| `changed` | The title or location moved; `changeDetail` shows what it said before |
| `removed` | No longer listed: filled, withdrawn or expired |
| `unchanged` | Still open, still identical — withheld by default, set `includeUnchanged` to receive it |

Every run also returns one free change-report row, e.g. `12 new job postings, 3 gone, 1 changed since 2026-08-19`, with `newCount`, `removedCount`, `changedCount` and `unchangedCount`. A quiet week returns just that row and no job rows.

The company summary row still describes the **whole board** (hiring momentum, department mix, remote share) so the change rows always have context.

#### Put it on a weekly schedule (3 steps)

1. Run the Actor once with **Monitor mode** on — the first run saves a baseline and returns every open role.
2. On the run's page click **Actions ▸ Schedule Actor**, and pick `Every week`.
3. Add an integration on the schedule (Slack, email, webhook) so new roles at the companies you watch reach you without opening Apify.

Runs are matched by **monitor key**. Leave `monitorKey` empty and it is derived from your input, so one schedule keeps one history automatically; set it (e.g. `competitor-hiring-weekly`) to keep several independent watches. State is kept in a named key-value store holding only job identifiers and a short title/location label — never descriptions.

A board that could not be read this run is left untouched: its roles are never reported as "removed" because of a network error.

### Pricing

Pay per event:

| Event | Price | When |
|---|---|---|
| `run-started` | **$0.005** | Once per run, when your company list is accepted and the boards are read. |
| `job-listed` | **$0.003** | Once per job posting written to the dataset, with its full judged row. |
| `company-summary` | **$0.01** | Once per company, for the summary row. Only written when at least one job row was delivered for that company. |

A 40-role company therefore costs `0.005 + 40 × 0.003 + 0.01 = $0.135`.

Notice rows — unreadable board, no open roles, no matching filters, limit reached, and the monitor change report — are **not** charged.

In **Monitor mode** the same `job-listed` price applies to each delivered change row, `removed` rows included: a role that has just been filled is exactly the answer you scheduled the run for. A week with no movement delivers no job rows and therefore costs only the run-start fee.

**Free Apify plans** receive up to 25 billable rows per run and finish normally with a notice explaining the cap. Paid plans have no per-run job limit.

***

### Data sources & responsibility

This Actor reads the **public, unauthenticated job-board endpoints** that each applicant tracking system serves to a company's own careers page:

```
Greenhouse   https://boards-api.greenhouse.io/v1/boards/<slug>/jobs?content=true
Lever        https://api.lever.co/v0/postings/<slug>?mode=json
Ashby        https://api.ashbyhq.com/posting-api/job-board/<slug>?includeCompensation=true
Workable     https://apply.workable.com/api/v1/widget/accounts/<slug>?details=true
```

- Exactly **one request per company**, two companies at a time, with an honest bot User-Agent (`ntriq-ats-jobs/1.0`).
- **No login, no captcha, no anti-bot measure is bypassed.** Nothing behind an authentication wall is read.
- Only job-posting data is returned. No recruiter names, no applicant data, no personal contact details.

**This Actor is unofficial and is not affiliated with, endorsed by, or sponsored by Greenhouse, Lever, Ashby, Workable, or any employer whose board you read.** You are responsible for complying with the terms of service of each applicant tracking system and each employer, and for using the results lawfully. **You are the data controller** for everything you collect with it, including any obligations under GDPR or comparable law.

`ghostJobRisk` is a heuristic built from posting age, update dates and duplicate titles. It is a signal to investigate, **not** a statement about any employer's hiring intentions.

***

### Notes

- Boards that publish no posting date are still returned; their `freshness` is `unknown` and `postedWithinDays` never filters them out.
- Ashby and Lever publish pay ranges more often than Greenhouse and Workable, so `salary` is populated unevenly. It is parsed from what the board actually published — nothing is estimated.
- A company whose board exists but has no open roles returns a free `no-jobs` notice, not an error.

### Works well with

- **[whois-domain-lookup](https://apify.com/ntriqpro/whois-domain-lookup)** — Qualify the hiring company by its domain: registrar, age and registrant details.
- **[lighthouse-website-audit](https://apify.com/ntriqpro/lighthouse-website-audit)** — Score the company site for performance and SEO before you pitch or apply.
- **[theharvester-osint](https://apify.com/ntriqpro/theharvester-osint)** — Collect public emails and hosts for the same company domain.

Every run of this Actor also returns these suggestions in the `nextSteps` field of its summary row.

# Actor input Schema

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

Company slugs or careers-page URLs. Greenhouse, Lever, Ashby and Workable are detected automatically — paste https://boards.greenhouse.io/dropbox, https://jobs.lever.co/binance, https://jobs.ashbyhq.com/linear or https://apply.workable.com/blueground. A bare name like "dropbox" is probed across all four boards; a URL is always faster and unambiguous.

## `company` (type: `string`):

Alternative to the list above — one company slug or careers-page URL.

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

Keep only roles whose title contains one of these words, e.g. engineer, designer, sales. Leave empty to get every open role. Matching is case-insensitive.

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

Keep only roles whose location mentions one of these, e.g. London, Remote, United States. Leave empty for every location.

## `postedWithinDays` (type: `integer`):

Keep only roles first published within this many days. 0 or empty means no date filter. Roles whose board publishes no posting date are always kept.

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

Ceiling on rows returned per company. 0 means unlimited — this Actor reads the whole board live and has no 5,000-job cap.

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

Add the full job description as plain text to every row. Off by default because descriptions make the dataset much larger.

## `monitorMode` (type: `boolean`):

Compare this run with your previous run and return only what changed — roles posted since last time, roles whose title or location moved, and roles that are no longer listed (filled, withdrawn or expired). Every row gets a changeType of new / changed / removed / unchanged. Schedule this Actor weekly to get only what changed. The first run saves a baseline and returns every open role.

## `monitorKey` (type: `string`):

Name this watch, e.g. "competitor-hiring-weekly". Runs sharing a key are compared with each other, so you can keep several independent watches. Leave empty and the key is derived from your input automatically — which is what you want when one schedule always sends the same companies and filters.

## `includeUnchanged` (type: `boolean`):

Monitor mode only. Off by default, so a quiet week returns just the change report. Turn on to also receive every role that is still open and unchanged (each one is a result row and is charged).

## `disclaimerAck` (type: `boolean`):

This Actor reads only the public, unauthenticated job-board endpoints that each ATS serves to a company's own careers page. It is unofficial and not affiliated with Greenhouse, Lever, Ashby, Workable or any employer. You are responsible for complying with each ATS's and each employer's terms of service, and you are the data controller for everything you collect.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/dropbox",
    "https://jobs.lever.co/binance",
    "https://jobs.ashbyhq.com/linear",
    "https://apply.workable.com/blueground"
  ],
  "keywords": [],
  "locations": [],
  "includeDescription": false,
  "monitorMode": false,
  "includeUnchanged": false,
  "disclaimerAck": true
}
```

# Actor output Schema

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

No description

# 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/dropbox",
        "https://jobs.lever.co/binance",
        "https://jobs.ashbyhq.com/linear",
        "https://apply.workable.com/blueground"
    ],
    "keywords": [],
    "locations": [],
    "postedWithinDays": 0,
    "maxJobsPerCompany": 0,
    "includeDescription": false,
    "monitorMode": false,
    "includeUnchanged": false,
    "disclaimerAck": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("ntriqpro/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": [
        "https://boards.greenhouse.io/dropbox",
        "https://jobs.lever.co/binance",
        "https://jobs.ashbyhq.com/linear",
        "https://apply.workable.com/blueground",
    ],
    "keywords": [],
    "locations": [],
    "postedWithinDays": 0,
    "maxJobsPerCompany": 0,
    "includeDescription": False,
    "monitorMode": False,
    "includeUnchanged": False,
    "disclaimerAck": True,
}

# Run the Actor and wait for it to finish
run = client.actor("ntriqpro/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": [
    "https://boards.greenhouse.io/dropbox",
    "https://jobs.lever.co/binance",
    "https://jobs.ashbyhq.com/linear",
    "https://apply.workable.com/blueground"
  ],
  "keywords": [],
  "locations": [],
  "postedWithinDays": 0,
  "maxJobsPerCompany": 0,
  "includeDescription": false,
  "monitorMode": false,
  "includeUnchanged": false,
  "disclaimerAck": true
}' |
apify call ntriqpro/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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