# ATS Hiring Monitor: Greenhouse, Lever, Ashby, SmartRecruiters (`vitaenox/ats-hiring-monitor`) Actor

Watch a list of companies' job boards. Auto-detects Greenhouse, Lever, Ashby or SmartRecruiters, returns normalised job postings, and in incremental mode outputs only roles that are new, changed or removed since the last run.

- **URL**: https://apify.com/vitaenox/ats-hiring-monitor.md
- **Developed by:** [James Bailey](https://apify.com/vitaenox) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 job postings

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 Hiring Monitor: Greenhouse, Lever, Ashby & SmartRecruiters

Give it a watchlist of companies. It finds each company's job board, pulls every open role into one clean format, and on later runs tells you **only what's new, what changed and what was taken down** since last time.

Use it to:

- **Track hiring signals** for sales and investment research: which target accounts just opened a sales, security or data team, or quietly pulled a batch of roles.
- **Job hunt without refreshing 30 careers pages**: schedule it daily with a title filter like `engineer` and get only fresh postings.
- **Feed a jobs board, newsletter or spreadsheet** with normalised postings from many companies at once.

Everything comes from the ATS providers' official public job-board APIs (the same feeds companies use to embed jobs on their own sites). No logins, no browser, no proxies, and no candidate or recruiter personal data.

### Supported ATS providers

| Provider | Detected from | Notes |
|---|---|---|
| Greenhouse | `boards.greenhouse.io/<token>`, `job-boards.greenhouse.io/<token>`, embed scripts | Company name, department, first-published and last-updated dates |
| Lever | `jobs.lever.co/<slug>` (global and EU hosting) | Team, department, commitment, workplace type, salary range when published |
| Ashby | `jobs.ashbyhq.com/<name>` | Team, department, workplace type, published compensation |
| SmartRecruiters | `jobs.smartrecruiters.com/<Company>`, `careers.smartrecruiters.com/<Company>` | Company name, function, employment type, remote/hybrid flags |

Workday, Workable, Recruitee, Teamtailor, Personio and BambooHR are planned.

### How company detection works

For each line in **Companies** the Actor:

1. Uses it directly if it's a job-board URL (`https://jobs.lever.co/spotify`) or an explicit pair (`greenhouse:stripe`).
2. Otherwise turns the name or domain into likely board IDs (`Rocket Lab` → `rocketlab`, `rocket-lab`; `stripe.com` → `stripe`) and checks all four providers.
3. For domains, if that fails, reads the company's own `/careers`, `/jobs` and home page looking for links to a supported ATS (this is how `octopus.energy` resolves to Lever board `octoenergy`).

If a company turns up on more than one provider (common after an ATS migration), the board with the most live postings wins and the others are listed under `alsoFoundOn` in the run summary. **For a watchlist you care about, paste the board URL or `ats:board` pair**: it's exact, and skips detection requests.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `companies` | array of strings | (required) | Company names, domains, job-board URLs or `ats:board` pairs, one per line |
| `mode` | `incremental` / `full` | `full` | Full outputs every live posting. Incremental outputs only new, changed and removed postings (a repeat run with no changes returns nothing) |
| `titleKeywords` | array of strings | `[]` | Only output postings whose title contains any of these (case-insensitive) |
| `locationKeywords` | array of strings | `[]` | Only output postings whose location or workplace type contains any of these, e.g. `london`, `remote` |
| `includeDescription` | boolean | `false` | Add plain-text job descriptions (Greenhouse, Lever, Ashby) |
| `stateStoreName` | string | `ats-hiring-monitor-state` | Named key-value store that remembers postings between runs. Use one per independent watchlist |
| `maxConcurrency` | integer | `5` | Companies fetched in parallel |

#### Example input

```json
{
    "companies": [
        "stripe.com",
        "https://jobs.lever.co/spotify",
        "ashby:ramp",
        "CERN",
        "Rocket Lab"
    ],
    "mode": "incremental",
    "titleKeywords": ["engineer", "analyst"],
    "locationKeywords": ["london", "remote"]
}
```

### Incremental mode

The Actor keeps a snapshot of every board it has seen in a named key-value store (`stateStoreName`), so it survives between runs. Each run compares live postings with that snapshot:

- **`new`**: a posting that wasn't there last run
- **`changed`**: same posting ID, but the title, location(s), department, team, employment type, workplace type, compensation or URL changed. `changedFields` lists each change with its previous and current value
- **`removed`**: was there last run and is gone now (closed, filled or unpublished)
- **`unchanged`**: only output in full mode

Details worth knowing:

- The **first run** for a board has nothing to compare against, so everything is `new`.
- Last-updated timestamps and description edits are deliberately ignored, because ATSs bump them for invisible admin edits and they'd bury the real changes.
- If a board can't be fetched in a run (API down, typo), its saved state is left alone, so you never get a false wave of "removed" followed by "new".
- Keyword filters only affect what's output, not what's tracked, so you can change filters between runs without corrupting the history.
- Schedule the Actor (daily is typical) with the same `stateStoreName` to get a clean change feed. Different watchlists should use different store names.

### Output

One dataset item per posting. Example (incremental run, a role that moved from hybrid to remote):

```json
{
    "changeType": "changed",
    "ats": "lever",
    "boardId": "spotify",
    "jobId": "2193db3f-77c5-43b8-b030-8f92c9882bf1",
    "company": "Spotify",
    "title": "Android Engineer - Experience",
    "location": "London",
    "locations": ["London", "Stockholm"],
    "department": "Engineering",
    "team": "Experience",
    "employmentType": "full-time",
    "workplaceType": "remote",
    "compensation": null,
    "url": "https://jobs.lever.co/spotify/2193db3f-77c5-43b8-b030-8f92c9882bf1",
    "applyUrl": "https://jobs.lever.co/spotify/2193db3f-77c5-43b8-b030-8f92c9882bf1/apply",
    "postedAt": "2026-06-23T11:29:45.805Z",
    "updatedAt": null,
    "description": null,
    "changedFields": [
        { "field": "workplaceType", "previous": "hybrid", "current": "remote" }
    ],
    "firstSeenAt": "2026-09-21T06:00:04.112Z",
    "removedAt": null,
    "input": "https://jobs.lever.co/spotify",
    "detectedVia": "explicit",
    "scrapedAt": "2026-09-28T06:00:03.871Z"
}
```

| Field | Notes |
|---|---|
| `changeType` | `new`, `changed`, `unchanged` (full mode only) or `removed` |
| `ats`, `boardId`, `jobId` | Together these uniquely identify a posting |
| `company` | From the ATS where it provides one (Greenhouse, SmartRecruiters), otherwise from your input |
| `location`, `locations` | Primary location plus every listed location |
| `employmentType` | Normalised to `full-time`, `part-time`, `contract`, `internship`, `temporary` where recognisable |
| `workplaceType` | `remote`, `hybrid`, `onsite` or `null` when the ATS doesn't say |
| `compensation` | Salary text where the company publishes it (mostly Ashby and Lever) |
| `postedAt`, `updatedAt` | ISO 8601. `updatedAt` is only available from Greenhouse |
| `firstSeenAt`, `removedAt` | When this Actor first saw the posting, and when it noticed it gone |
| `detectedVia` | `explicit`, `slug-guess` or `careers-page:<url>`, so you can audit detection |

Fields a provider doesn't expose are `null` rather than missing, so every item has the same shape.

The run's `OUTPUT` record (default key-value store) has a per-company summary: which board was detected, how many live postings, counts of new/changed/removed, and any company that couldn't be matched (`not_found`) or errored.

### Pricing

Pay per event: **$0.001 per job posting** delivered to your dataset, plus **$0.01 per run**. In incremental mode you only pay for postings that are new, changed or removed, which is usually a small fraction of a board, so daily monitoring of a large watchlist stays cheap. A run where nothing changed costs just the $0.01 start fee.

### Limitations

- Only the four providers above for now. Companies on Workday, Workable and others come back as `not_found`.
- Auto-detection can match a different company that happens to use the same board slug. Check `detectedVia` and `alsoFoundOn` in the run summary, and pin exact board URLs for anything important.
- Careers-page detection only sees links present in the page's HTML; job widgets that load entirely from JavaScript won't be found. Paste the board URL instead.
- SmartRecruiters descriptions aren't included yet (they need one extra request per posting).
- Boards are capped at 10,000 postings for SmartRecruiters.

### Is this legal?

The Actor reads each ATS provider's public job-board API, which exists so that job postings can be displayed and shared. It doesn't log in, bypass any access control, or collect personal data. You're still responsible for how you use the results, particularly if you republish them.

### Support

Found a bug or a company that won't resolve? Open an issue on this Actor's **Issues** tab with the company name and what you expected.

# Actor input Schema

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

One per line. Each can be a company name ("Palo Alto Networks"), a domain ("stripe.com"), a job board URL ("https://jobs.lever.co/spotify"), or an explicit ats:board pair ("greenhouse:stripe", "lever:spotify", "ashby:ramp", "smartrecruiters:CERN"). Board URLs and explicit pairs skip auto-detection and are the most precise.

## `mode` (type: `string`):

Full (default) outputs every live posting, each tagged new/changed/unchanged. Incremental outputs only postings that are new, changed or removed since the previous run with the same state store (the first run outputs everything as new; a repeat run with no changes outputs nothing). Both modes update the saved state.

## `titleKeywords` (type: `array`):

Only output postings whose title contains at least one of these (case-insensitive). Leave empty for all. Filtering doesn't affect change tracking, so you can change it between runs safely.

## `locationKeywords` (type: `array`):

Only output postings whose location (or workplace type, e.g. "remote") contains at least one of these (case-insensitive). Leave empty for all.

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

Add the plain-text job description to each posting (Greenhouse, Lever and Ashby; not yet SmartRecruiters). Makes results much larger.

## `stateStoreName` (type: `string`):

Name of the key-value store that remembers postings between runs. Use a different name per independent watchlist so their histories don't mix. Letters, digits and hyphens only.

## `maxConcurrency` (type: `integer`):

How many companies to fetch in parallel.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "https://jobs.lever.co/spotify",
    "ashby:ramp",
    "CERN"
  ],
  "mode": "full",
  "includeDescription": false,
  "stateStoreName": "ats-hiring-monitor-state",
  "maxConcurrency": 5
}
```

# Actor output Schema

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

One item per job posting, tagged new, changed, removed or unchanged.

## `summary` (type: `string`):

Per-company summary: detected board, live postings, counts of new, changed and removed, and any company that was not found or errored.

# 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.com",
        "https://jobs.lever.co/spotify",
        "ashby:ramp",
        "CERN"
    ],
    "mode": "full"
};

// Run the Actor and wait for it to finish
const run = await client.actor("vitaenox/ats-hiring-monitor").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.com",
        "https://jobs.lever.co/spotify",
        "ashby:ramp",
        "CERN",
    ],
    "mode": "full",
}

# Run the Actor and wait for it to finish
run = client.actor("vitaenox/ats-hiring-monitor").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.com",
    "https://jobs.lever.co/spotify",
    "ashby:ramp",
    "CERN"
  ],
  "mode": "full"
}' |
apify call vitaenox/ats-hiring-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,vitaenox/ats-hiring-monitor"
        }
    }
}
```

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/FsRhYwp1ZK5zvFrPN/builds/FwPKap6hoUFKR5xdv/openapi.json
