# Career Site & ATS Jobs Scraper - Greenhouse, Lever, Ashby (`ntriqpro/career-site-ats-jobs-scraper`) Actor

Scrape public company career sites and ATS boards: Greenhouse, Lever, Ashby, SmartRecruiters, Personio, Recruitee, Workable. Returns every open role with location, department, employment type and salary, plus new, reposted and closure signals when you pass a previous dataset id. Unofficial.

- **URL**: https://apify.com/ntriqpro/career-site-ats-jobs-scraper.md
- **Developed by:** [daehwan kim](https://apify.com/ntriqpro) (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 $9.00 / 1,000 job results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## Career Site & ATS Jobs Scraper - Greenhouse, Lever, Ashby and more

Give this Actor a company career page, a public ATS board URL or just a domain. It reads the board the company already publishes and returns **every open role**, one row per job, with location, department, employment type, salary, a description excerpt and the skills, benefits and experience requirements we read out of the posting.

Point it at the same boards again next week with the previous dataset id and it also tells you **what changed**: which roles are new, which look like reposts, which disappeared, and how each company's open headcount moved.

Supported public boards: **Greenhouse, Lever, Ashby, SmartRecruiters, Personio, Recruitee, Workable**.

> Unofficial. This Actor is not affiliated with, endorsed by or sponsored by Greenhouse, Lever, Ashby, SmartRecruiters, Personio, Recruitee, Workable or any employer named in your input. All product names are trademarks of their respective owners and are used only to describe which public boards are read.

### What it is good for

- **Sales and recruiting signals** - a company that opened twelve engineering roles this month is a different prospect than one that closed five.
- **Job boards and aggregators** - keep a live copy of hundreds of company boards without writing seven different clients.
- **Market research** - salary, seniority, department and location across a whole portfolio of companies.
- **Competitor hiring watch** - run it weekly, pass the previous dataset id, and read only the diff.

### Input - every field is an array

| Field | What it does |
|---|---|
| `boardUrls` | Public board URLs, or `platform:token` shorthand such as `greenhouse:stripe`. Up to 50 per run. |
| `companyDomains` | Domains such as `acme.com`. Each domain is tried against every supported board pattern, so one domain can return a whole board. |
| `atsPlatforms` | Restrict to `greenhouse`, `lever`, `ashby`, `smartrecruiters`, `personio`, `recruitee`, `workable`. Empty means all. |
| `titleIncludes` / `titleExcludes` | Keep or drop jobs by title keyword. |
| `locationIncludes` | Keep jobs whose location contains one of these terms. |
| `postedWithinDays` | One integer. `0` disables the date filter. |
| `maxResults` | One integer, 1 to 5000. Upper bound on charged rows. |
| `includeDescription` | One boolean. Description excerpt and the derived skill, benefit, language and experience fields. |
| `fetchDetailPages` | One boolean. Fetch the public job page for boards whose list feed carries no description (SmartRecruiters, Personio). |
| `previousDatasetIds` | One dataset id from an earlier run of this Actor. This is what turns on change detection. |
| `comparisonModes` | `off` or `compare`. Use `compare` together with `previousDatasetIds`. |
| `detectClosures` | One boolean. Emit `closure_suspected` rows. Those rows are free. |
| `proxyModes` | `NONE`, `DATACENTER` or `RESIDENTIAL`. Public ATS endpoints answer a direct connection, so `NONE` is the default. |

Accepted board URL shapes include `https://job-boards.greenhouse.io/<token>`, `https://boards-api.greenhouse.io/v1/boards/<token>/jobs`, `https://jobs.lever.co/<token>`, `https://jobs.ashbyhq.com/<token>`, `https://jobs.smartrecruiters.com/<Company>`, `https://<company>.jobs.personio.de/`, `https://<company>.recruitee.com/` and `https://apply.workable.com/<token>/`.

#### Example

```json
{
  "boardUrls": ["https://job-boards.greenhouse.io/stripe", "https://jobs.lever.co/leverdemo"],
  "companyDomains": ["vanta.com"],
  "titleIncludes": ["engineer", "data"],
  "postedWithinDays": [30],
  "maxResults": [500],
  "includeDescription": [true],
  "previousDatasetIds": ["PASTE_YOUR_PREVIOUS_DATASET_ID"],
  "comparisonModes": ["compare"]
}
```

### Output

One row per job. A few of the fields:

| Field | Meaning |
|---|---|
| `jobKey`, `jobId`, `url`, `applyUrl` | Stable key and the public links. `jobKey` is `platform:token:jobId`. |
| `title`, `companyName`, `location`, `isRemote`, `workModel` | The basics, normalised across all seven platforms. |
| `department`, `team`, `jobFunction`, `employmentType`, `seniorityLevel` | Classification as the board publishes it. |
| `salary`, `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval`, `hasSalary` | Compensation when the board exposes it. |
| `descriptionSnippet`, `descriptionChars`, `descriptionTruncated`, `descriptionUrl` | First 500 characters of the posting, its true length, and the public page that holds the rest. |
| `skillsMentioned`, `benefitsMentioned`, `languagesMentioned`, `yearsOfExperience`, `requiresVisaSponsorshipMention` | Facts we derive from the posting text so you can filter without the prose. |
| `postedAt`, `updatedAt`, `validThrough`, `ageDays`, `observedAt` | Dates, always ISO 8601. |
| `atsPlatform`, `companyBoardToken`, `boardUrl`, `sourceDomain`, `evidenceUrls` | Exactly which public page each field came from. |
| `isNew`, `isReposted`, `suspectedClosed`, `previousJobId`, `postedAtChanged`, `changeReasons` | Change detection. |
| `companyCurrentCount`, `companyPreviousCount`, `companyDelta` | Open roles per company, now and in your baseline. |
| `comparisonStatus`, `coverageComplete`, `scopeFingerprint` | Whether the comparison is trustworthy and why. |
| `fieldAvailability` | Per field: `observed`, `not_measured` or `excluded_by_policy`. |

Aliases such as `jobTitle`, `company`, `organization`, `jobUrl`, `link` and `postedDate` are included so the rows drop straight into pipelines built for other job scrapers.

#### Why there is no full description field

The posting text is the employer's copyrighted work. We do not republish it. Each row carries a 500-character excerpt, the real character count, the structured facts we derive locally, and `descriptionUrl`, the employer's own public page where the full text lives. `descriptionText`, `description` and `descriptionHtml` exist in the schema but are always `null` with `excluded_by_policy`, so a pipeline expecting those keys will not break and will not silently receive republished prose.

#### Change detection is honest about what it knows

- **No `previousDatasetIds`** - every change field is `null` and `comparisonStatus` is `no_baseline`. Nothing is guessed.
- **Baseline scope differs from this run** - `scope_mismatch`, change fields stay `null`.
- **This run or the baseline did not cover the whole board** - `incomplete`, change fields stay `null` and no closure rows are produced.
- **`comparable`** - `isNew`, `isReposted`, `companyDelta` and closure rows are filled in.

`suspectedClosed` means the job was in your baseline and is absent from a board listing that was read in full in this run. It is a strong signal, not a statement from the employer, which is why the field is named *suspected*.

The run summary in the key-value store record `OUTPUT` reports per board how many jobs were discovered, which boards failed and why, and how many descriptions were fetched. It keeps two coverage flags apart: `coverageComplete` says the board listings were read in full, which is what change detection and closure rows depend on, while `descriptionsComplete` says every description we went looking for came back. A board that answered its listing but rate-limited one detail page leaves `coverageComplete: true` and `descriptionsComplete: false`.

### Pricing

| Event | What triggers it |
|---|---|
| Actor start | Charged once per run by the platform, based on memory. |
| Job result | One validated, unique open job row delivered to the dataset. |

You are **not** charged for closure rows, duplicate rows, boards that returned an error, boards that do not exist, or runs that produce no jobs beyond the start event. Tiered pricing applies, so paid Apify plans pay less per row than the free tier. Current prices are on the Actor's pricing tab and always take precedence over this page.

If you set a maximum total charge for the run, the Actor plans its output to fit inside it, stops cleanly, keeps what it already delivered and marks `coverageComplete: false`.

### How it collects

Only public, unauthenticated endpoints that each ATS publishes for its own job boards, with an honest bot user agent, at most a few requests per second, one request at a time per board and exponential backoff on transient errors. There is no login, no cookie, no captcha solving, no account pool and no IP rotation to get around a block. A board that refuses us is reported as refused.

Personal data is out of scope by design: contact emails, phone numbers and recruiter identities are never collected, and those fields are always `null` with `excluded_by_policy` in `fieldAvailability`.

### Your responsibility as the operator

You choose the companies and boards. You are responsible for having the right to collect and use that data, for complying with the terms of the sites you point this Actor at, and for any onward use of the output, including GDPR and equivalent obligations where you are the data controller. We provide the tool and report what each public page returned.

### Leave a review

A short review on the **Reviews** tab, good or bad, helps other people find this Actor and tells us what to build next. Found a bug or need a feature? Open an issue on the **Issues** tab and we'll look into it.

# Actor input Schema

## `boardUrls` (type: `array`):

Array input. Public ATS board URLs or `platform:token` shorthand. Greenhouse, Lever, Ashby, SmartRecruiters, Personio, Recruitee and Workable public boards are supported. Up to 50 per run.

## `companyDomains` (type: `array`):

Array input. Company domains such as "example.com". Each domain is tried against every public ATS board pattern, so one domain can return a whole board.

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

Array input. Restrict collection to these platforms: greenhouse, lever, ashby, smartrecruiters, personio, recruitee, workable. Empty means all of them.

## `titleIncludes` (type: `array`):

Array input. Keep only jobs whose title contains at least one of these terms.

## `titleExcludes` (type: `array`):

Array input. Drop jobs whose title contains any of these terms.

## `locationIncludes` (type: `array`):

Array input. Keep only jobs whose location contains at least one of these terms.

## `postedWithinDays` (type: `array`):

Array input with one integer. Keep only jobs posted within this many days. 0 disables the date filter.

## `maxResults` (type: `array`):

Array input with one integer, 1 to 5000. Upper bound on charged job rows. Reaching the limit marks coverage incomplete and disables closure detection.

## `includeDescription` (type: `array`):

Array input with one boolean. Include the job description text and HTML in every row.

## `fetchDetailPages` (type: `array`):

Array input with one boolean. Fetch the public job page for boards whose list feed omits the description (SmartRecruiters, Personio).

## `previousDatasetIds` (type: `array`):

Array input with one dataset id produced by an earlier run of this Actor. Without it, every change field is null and comparisonStatus is no\_baseline.

## `comparisonModes` (type: `array`):

Array input with one value: "off" or "compare". Use "compare" together with previousDatasetIds.

## `detectClosures` (type: `array`):

Array input with one boolean. Emit closure\_suspected rows for baseline jobs that are absent from a board list read in full. Closure rows are never charged.

## `proxyModes` (type: `array`):

Array input with one value: NONE, DATACENTER or RESIDENTIAL. Public ATS endpoints answer a direct connection, so NONE is the default.

## Actor input object example

```json
{
  "boardUrls": [
    "https://job-boards.greenhouse.io/greenhouse",
    "https://jobs.lever.co/leverdemo"
  ],
  "companyDomains": [],
  "atsPlatforms": [],
  "titleIncludes": [],
  "titleExcludes": [],
  "locationIncludes": [],
  "postedWithinDays": [
    0
  ],
  "maxResults": [
    25
  ],
  "includeDescription": [
    true
  ],
  "fetchDetailPages": [
    true
  ],
  "previousDatasetIds": [],
  "comparisonModes": [
    "off"
  ],
  "detectClosures": [
    true
  ],
  "proxyModes": [
    "NONE"
  ]
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

## `summary` (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 = {
    "boardUrls": [
        "https://job-boards.greenhouse.io/greenhouse",
        "https://jobs.lever.co/leverdemo"
    ],
    "companyDomains": [],
    "atsPlatforms": [],
    "titleIncludes": [],
    "titleExcludes": [],
    "locationIncludes": [],
    "postedWithinDays": [
        0
    ],
    "maxResults": [
        25
    ],
    "includeDescription": [
        true
    ],
    "fetchDetailPages": [
        true
    ],
    "previousDatasetIds": [],
    "comparisonModes": [
        "off"
    ],
    "detectClosures": [
        true
    ],
    "proxyModes": [
        "NONE"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("ntriqpro/career-site-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 = {
    "boardUrls": [
        "https://job-boards.greenhouse.io/greenhouse",
        "https://jobs.lever.co/leverdemo",
    ],
    "companyDomains": [],
    "atsPlatforms": [],
    "titleIncludes": [],
    "titleExcludes": [],
    "locationIncludes": [],
    "postedWithinDays": [0],
    "maxResults": [25],
    "includeDescription": [True],
    "fetchDetailPages": [True],
    "previousDatasetIds": [],
    "comparisonModes": ["off"],
    "detectClosures": [True],
    "proxyModes": ["NONE"],
}

# Run the Actor and wait for it to finish
run = client.actor("ntriqpro/career-site-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 '{
  "boardUrls": [
    "https://job-boards.greenhouse.io/greenhouse",
    "https://jobs.lever.co/leverdemo"
  ],
  "companyDomains": [],
  "atsPlatforms": [],
  "titleIncludes": [],
  "titleExcludes": [],
  "locationIncludes": [],
  "postedWithinDays": [
    0
  ],
  "maxResults": [
    25
  ],
  "includeDescription": [
    true
  ],
  "fetchDetailPages": [
    true
  ],
  "previousDatasetIds": [],
  "comparisonModes": [
    "off"
  ],
  "detectClosures": [
    true
  ],
  "proxyModes": [
    "NONE"
  ]
}' |
apify call ntriqpro/career-site-ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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