# ATS Hiring Monitor: Greenhouse, Lever & Ashby jobs by company (`openroles/ats-hiring-monitor`) Actor

Track who is hiring and for what. Give company domains or names, get one row per company: open jobs, keyword matches, hiring spikes, remote share and new or closed postings. Reads Greenhouse, Lever and Ashby job boards through their public APIs.

- **URL**: https://apify.com/openroles/ats-hiring-monitor.md
- **Developed by:** [HERMENEGILDO ROMERO](https://apify.com/openroles) (community)
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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

### What does ATS Hiring Monitor do?

**ATS Hiring Monitor** tells you **who is hiring, how much and for what** across a list of companies. Give it **company domains, names or job board URLs** and get back **one row per company** with open jobs, jobs matching your keywords, hiring signals and, if you run it on a schedule, the **new and closed job postings since the last run**.

It reads the public job boards of **Greenhouse, Lever and Ashby** through their official public APIs, so results are fast and complete (no browser, no proxies).

Built for **B2B sales, recruiting agencies and competitive intelligence**: enrich an account list in **Clay** or your CRM, or get alerted when a target account starts hiring.

### What can it do?

- 🔎 **Finds the job board for you**: paste `stripe.com`, `Stripe` or `https://boards.greenhouse.io/stripe`. It checks the company website for a job board link and tries likely board names on the three platforms.
- 🏢 **One row per company**, always in the same shape, also for companies that were not found (so you can match the output with your list).
- 🎯 **Filters**: job title keywords (include / exclude), countries, remote only, posted in the last N days.
- 📈 **Hiring signals with published rules**: jobs posted in the last 7 and 30 days, hiring spike, share of remote jobs, top departments and locations. No black-box scores.
- 🔔 **Change tracking**: name a watchlist and each run tells you which postings are **new** and which have **closed**.
- 🧾 **Or one row per job posting** with a stable `jobKey`, title, department, locations, country, work model, salary (when published), dates and links.
- ⚡ Fast: 5 companies in parallel; in our local tests 30 companies took about 10-20 seconds.

### How to use it

1. Click **Try for free** (or **Start**).
2. In **Companies**, add one company per line: a domain (`ramp.com`), a name (`OpenAI`) or a job board URL (`https://jobs.lever.co/palantir`). Domains and URLs are the most reliable.
3. Optional: add **job title keywords** (e.g. `engineer*`, `account executive`), **countries** (`US`, `GB`) or **remote only**.
4. Optional: give a **watchlist name** (e.g. `target-accounts`) and **schedule** the Actor (for example daily). From the second run on, you get `newJobs` and `closedJobs` per company.
5. Click **Start** and open the **Companies** view in the Output tab (**Job postings** if you chose one row per job posting), or download the results as JSON, CSV or Excel.

#### Use it from Clay or any tool via API

Call the synchronous endpoint, which runs the Actor and returns the results in one request (it waits up to 300 seconds, so start with tens of companies per call and grow once you know your timing):

```
POST https://api.apify.com/v2/actors/openroles~ats-hiring-monitor/run-sync-get-dataset-items
Authorization: Bearer <your Apify API token>
Content-Type: application/json

{"companies": ["stripe.com"], "titleKeywords": ["account executive"]}
```

### Input

```json
{
  "companies": ["stripe.com", "https://jobs.lever.co/palantir", "OpenAI"],
  "outputMode": "companies",
  "titleKeywords": ["engineer*", "account executive"],
  "excludeTitleKeywords": ["intern*"],
  "postedWithinDays": 30,
  "countries": ["US", "GB"],
  "remoteOnly": false,
  "watchlistName": "target-accounts",
  "onlyChanges": false
}
```

| Field | What it does |
|---|---|
| `companies` | Required. Domains, names or job board URLs, one per line. |
| `outputMode` | `companies` (default): one row per company. `jobs`: one row per job posting. |
| `titleKeywords` | Keep postings whose title has one of these words or phrases. Whole words, not case or accent sensitive. End with `*` for word beginnings: `engineer*` matches engineer, engineers, engineering. |
| `excludeTitleKeywords` | Leave out postings with any of these. Exclusions win. |
| `postedWithinDays` | Only postings published in the last N days. |
| `countries` | 2-letter codes or English names. Uses the main location of each posting. |
| `remoteOnly` | Only postings marked as remote. |
| `watchlistName` | Turns on change tracking (letters, numbers and dashes). |
| `onlyChanges` | With a watchlist and `jobs` output, return only new and closed postings. |

### Output

#### One row per company (default)

A real row (Stripe, filtered to engineers and account executives in the last 30 days):

```json
{
  "input": "https://boards.greenhouse.io/stripe",
  "status": "found",
  "matchedBy": "url",
  "ats": "greenhouse",
  "board": "stripe",
  "boardUrl": "https://job-boards.greenhouse.io/stripe",
  "openJobs": 731,
  "matchingJobs": 93,
  "newJobs": 0,
  "closedJobs": 0,
  "postedLast7Days": 19,
  "postedLast30Days": 93,
  "postedPrevious30Days": 63,
  "hiringSpike": false,
  "remoteShare": 0.1,
  "topDepartments": ["1185 Account Executives (EMEA)", "1642 Product Sales - MaaS", "Global Operations"],
  "topLocations": ["Dublin", "London", "N/A"],
  "otherMatches": [],
  "error": null
}
```

`status` is `found`, `no_open_jobs`, `not_found` (no board on Greenhouse, Lever or Ashby) or `error` (could not be checked; see `error`). Companies that are not found still get a row.

#### One row per job posting (`outputMode: "jobs"`)

```json
{
  "jobKey": "ashby:ramp:34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "ats": "ashby",
  "board": "ramp",
  "sourceJobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "country": "US",
  "workplaceType": "hybrid",
  "employmentType": "full_time",
  "salary": {"min": 211400.0, "max": 290600.0, "currency": "USD", "interval": "year"},
  "publishedAt": "2026-04-07T17:12:35Z",
  "updatedAt": null,
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "change": "new"
}
```

Fields are always present; a value is `null` when the platform does not provide it.

### How are the hiring signals calculated?

Signals use the postings that pass your title, country and remote filters (not the date filter, because the signals measure time themselves).

- **`postedLast7Days` / `postedLast30Days`**: open postings published in the last 7 / 30 days. **`postedPrevious30Days`**: published 31 to 60 days ago.
- **`hiringSpike`** is `true` when at least **10** postings were published in the last 30 days **and** at least **twice** as many as in the 30 days before. Only open postings can be seen, so the last month always looks a bit bigger; asking for twice as many leaves room for that.
- **`remoteShare`**: postings marked as remote divided by all postings. Postings that do not state a work model count as not remote.
- **`topDepartments` / `topLocations`**: up to 3, most postings first.

### How does change tracking work?

- A posting is identified by its `jobKey`. **New**: it is there now and was not in the previous run of the watchlist. **Closed**: it was there and is gone.
- The **first run** for a company saves the reference, so `newJobs` and `closedJobs` are `null` until the second run.
- If a company cannot be checked (`error`), its reference is not touched, so a temporary failure never shows all its postings as closed.
- The reference is kept in a named key-value store `ats-watch-<watchlist name>`; named storages are not deleted by Apify's data retention.

### How do we read country and work model?

Lever and Ashby give them directly. **Greenhouse does not**, so we read them from the location text and the company's offices (for example `Remote - USA` → US, remote; `Toronto, ON, CA` → Canada). In our tests with 16 companies, the country was found for 99.5% of Greenhouse postings and the work model for 43%, because many companies simply do not publish it. When we cannot tell, the value is `null` rather than a guess.

### How much does it cost?

The Actor is free to use; you only pay for your Apify platform usage, which is very low: it only calls the job boards' public APIs (no browser, no proxy).

Measured on the Apify platform in October 2026:

| Run | Time | Platform usage |
| --- | --- | --- |
| 30 companies, one row per company | 6-15 s | $0.001-0.002 |
| 3 companies, one row per job posting (1,827 rows) | 4 s | under $0.001 |

Apify's free plan includes $5 of platform usage every month, so regular monitoring of a few hundred companies fits in it.

### Limitations

- Platforms: **Greenhouse, Lever and Ashby**. Companies on other systems (Workday, their own careers site...) come back as `not_found`.
- Greenhouse boards hosted in the EU (`job-boards.eu.greenhouse.io`) are not supported yet.
- Matching by name guesses the board code, so it can miss or, rarely, pick another company with the same name. Domains and board URLs are more reliable; check `matchedBy` and `otherMatches`.
- For Lever, `publishedAt` is the date the posting was created; long-running postings can be old.
- Job descriptions are not included.

### FAQ

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

The Actor only reads **publicly listed job postings** through the job board platforms' public APIs, and does not collect personal data. Whether your specific use is allowed depends on your jurisdiction and purpose; check with your own legal adviser.

ATS Hiring Monitor is an independent tool. It is not affiliated with, endorsed by or sponsored by Greenhouse, Lever or Ashby; their names are only used to say which job boards it reads.

#### Why is a company `not_found`?

It may use another hiring system, list jobs only on its own website, or have a board name we could not guess. Try its job board URL directly.

#### Can you add another platform?

Recruitee and Personio are on the roadmap. Open an issue in the **Issues** tab with the platforms and companies you need.

# Actor input Schema

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

One company per line: its job board URL (https://jobs.lever.co/acme), its website domain (acme.com) or its name (Acme). Board URLs and domains are the most reliable; names are matched by guessing the board code.

## `outputMode` (type: `string`):

"companies": one row per company with its status and hiring numbers (best for Clay or a CRM). "jobs": one row per open job posting (in the Output tab, open the "Job postings" view).

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

Only count (and, in "jobs" output, only return) postings whose title contains at least one of these words or phrases. Whole words, not case or accent sensitive: "AI" matches "AI Researcher" but not "Retail". End a keyword with \* to match word beginnings: "engineer\*" matches engineer, engineers, engineering. Empty: all postings.

## `excludeTitleKeywords` (type: `array`):

Leave out postings whose title contains any of these words or phrases (same rules as above). Exclusions win over inclusions.

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

Only postings published in the last N days. Postings without a date are left out. For Lever, the date is when the posting was created. Empty: any date.

## `countries` (type: `array`):

Only postings whose main location is in one of these countries. Use 2-letter codes (ES, US, GB) or English names (Spain, United States). Greenhouse has no country field, so it is read from the location text and offices; postings where no country can be found are left out. Empty: any country.

## `remoteOnly` (type: `boolean`):

Only postings marked as remote (including "City or Remote"). Many Greenhouse companies do not state the work model; those postings are left out.

## `watchlistName` (type: `string`):

Turns on change tracking. Each run is compared with the previous run of the same watchlist: postings that appeared are "new", postings that disappeared are "closed". The first run of a company only saves the reference (no new or closed yet). Use letters, numbers and dashes, e.g. "target-accounts". The reference is kept in a named key-value store called "ats-watch-<name>". Empty: no tracking.

## `onlyChanges` (type: `boolean`):

With a watchlist and "One row per job posting" output: return only new and closed postings, each marked in the "change" field. Needs a watchlist name.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "https://jobs.lever.co/palantir",
    "OpenAI"
  ],
  "outputMode": "companies",
  "titleKeywords": [],
  "excludeTitleKeywords": [],
  "countries": [],
  "remoteOnly": false,
  "onlyChanges": false
}
```

# Actor output Schema

## `companies` (type: `string`):

One row per company (when the output is "One row per company").

## `jobs` (type: `string`):

One row per job posting (when the output is "One row per job posting").

# 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/palantir",
        "OpenAI"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("openroles/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/palantir",
        "OpenAI",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("openroles/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/palantir",
    "OpenAI"
  ]
}' |
apify call openroles/ats-hiring-monitor --silent --output-dataset

```

## MCP server setup

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