# Greenhouse, Lever & Ashby Job Monitor — New Job Alerts (`lone_fascia/ats-job-monitor`) Actor

Greenhouse jobs, Lever jobs and Ashby jobs monitor: get every open job from company career pages, then only what changed (new, updated, closed, reposted), with optional Slack/webhook alerts. Official public APIs, no login, no proxies. From $0.001 per job.

- **URL**: https://apify.com/lone_fascia/ats-job-monitor.md
- **Developed by:** [mullvad lu](https://apify.com/lone_fascia) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 jobs

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

## Job Postings Monitor for Greenhouse, Lever & Ashby

**Watch a list of companies and get alerted when they post, change or close a job.**

- Paste career-page links (`boards.greenhouse.io/…`, `job-boards.greenhouse.io/…`, `jobs.lever.co/…`, `jobs.ashbyhq.com/…`) — guaranteed to work — or just a company domain and let the Actor try to detect the job board.
- **Snapshot mode**: all current jobs, normalized fields, filters for keywords, departments, locations, remote only, posted within N days.
- **Monitor mode**: first run saves a baseline; every later run returns only changes: new, updated (with `changedFields` and the previous title/location), closed (only after 2 consecutive misses, to avoid false alarms from a temporary glitch), reposted.
- Optional webhook (monitor mode): a Slack-formatted summary to your Slack incoming webhook, or JSON to any HTTPS endpoint, only when something changed.
- Up to 500 companies per run. Contact info is stripped from descriptions by default.

### Use it as a monitor in 3 steps

Snapshot mode (the default) shows you all current jobs once. The real product is Monitor mode — it tells you only what changed:

1. Set **Mode** to `monitor` and give your watchlist a **Monitor ID** (e.g. `my-targets`). The pre-filled example companies work as-is.
2. Run it once. This first run only records a baseline — you will not get hundreds of "new" jobs.
3. Schedule it daily in Apify **Schedules** and set a **Webhook URL** (Slack or any https endpoint). From now on you only hear about new, updated, closed and reposted jobs.

### Who it's for

Recruiters and sourcers tracking target startups and mid-size tech companies, sales teams using hiring as a buying signal, job seekers watching dream companies, and analysts tracking hiring trends. Best fit: companies that hire through Greenhouse, Lever or Ashby (common at startups and scale-ups). Large enterprises on Workday or other systems are not covered yet.

### How to add companies

Paste one entry per line. You can mix the formats.

| Input | Example | Reliability |
|---|---|---|
| **Job board URL (recommended)** | `https://job-boards.greenhouse.io/duolingo`, `https://boards.greenhouse.io/airbnb`, `https://jobs.lever.co/spotify`, `https://jobs.ashbyhq.com/posthog` | Guaranteed: read directly |
| **ats:slug** | `greenhouse:airbnb`, `lever:spotify`, `ashby:ramp` | Guaranteed: read directly |
| Company domain (bonus) | `stripe.com` | Auto-detected, see below |
| **Company pack** (optional) | *YC startups (verified boards)* | Verified boards, read directly |

Tip: open the company's careers page, click a job, and copy the board part of the URL (`jobs.lever.co/<slug>`, etc.). Slugs are case-sensitive on some boards; we keep them exactly as written (URL-decoded).

**Duplicate inputs for the same board are merged and charged once**: if one company is entered in several forms (board link, `ats:slug`, domain, name or a pack entry), it is scraped once and charged once per run; the extra inputs get a free `duplicate_merged` row and the job rows list them in `mergedInputs`.

#### Company packs

Select **Company packs** to add a ready-made watchlist to your own companies (you can use a pack alone or together with your list):

- **YC startups (verified boards)**: 88 Y Combinator startups (active, hiring, 20+ staff; from a random sample of 200 in our Oct 2026 detection test) whose Greenhouse, Lever or Ashby board was found via a link on the company website (73) or via a name guess that the job content confirmed (15). Every board was re-checked on 2026-10-24 against the public ATS APIs and had open jobs (about 2,100 jobs in total: 66 Ashby, 18 Greenhouse, 4 Lever). Boards that share a name with an unrelated company on another ATS were left out. It is a sample, not all YC companies.

Pack boards are treated exactly like pasted board links (detection `input`, never charged as `company-resolved`). Duplicates with your own entries are removed (your entry is kept), and the total including packs is capped at 500 companies. In snapshot mode a pack can return a couple of thousand jobs (charged as `job`), so use filters to keep it small.

#### Domain auto-detection (bonus path)

1. **Website links**: we read the company's homepage, `/careers`, `/jobs`, `/company/careers` and `/about/careers` (one short request each) and look for Greenhouse, Lever or Ashby board links. A linked board found this way is `confirmed`. A board that exists but currently has 0 jobs still counts: it is detected and can be monitored.
2. **Name guessing (fallback)**: we try board names derived from the domain (e.g. `linear.app` → `linear`) on all three ATS. A hit is `guessed`, and becomes `confirmed` only if the company domain appears in the job content or links. If the guessed name exists on two or more ATS it is `ambiguous`: we don't use it and list the candidates so you can pick the right one.

**Confirm-first for guessed boards (monitor mode).** A `guessed` board might belong to a different company with the same name, so by default monitor mode does **not** monitor it and charges nothing for it (no `monitor-check`, no `job-change`, no `company-resolved`). Instead you get a free Companies row with status `needs_confirmation`, the found `ats`, `board` and `boardUrl`, and a message. If it is the right board, paste that board URL (or `ats:slug`) into Companies: from then on it is a normal `input` board. Turn on **Monitor guessed boards** (`monitorGuessedCompanies`) to monitor guessed boards anyway (charged like any other company). In **snapshot** mode guessed boards are still output: their jobs are returned and charged as `job`, with a free Companies row warning that the board was guessed (never charged as `company-resolved`).

Every row has a `detection` field: `input` (you gave the board), `confirmed`, `guessed`, `ambiguous`, `not_found`. Results are cached for 30 days in a key-value store `ats-resolver-cache` in your account.

Measured coverage (Oct 2026, 200 random YC startups with 20+ staff): about **45% high-confidence** detection (website links 36.5%; guessing adds more). Large enterprises use Workday and other systems more often, so coverage is lower there. **Use the board URL when you can.**

### Modes

#### Snapshot

Every current job that matches your filters, one row per job.

#### Monitor

Give the watchlist a **Monitor ID** (e.g. `fintech-targets`) and schedule the Actor (e.g. daily) in Apify **Schedules**. Each run compares the boards with the previous run of the same Monitor ID:

| changeType | Meaning |
|---|---|
| `new` | A job id we have not seen before |
| `updated` | Same job id, but the title, location, salary or description changed (`changedFields` tells which; `previous` holds the previous title and location only, not previous salary or description) |
| `closed` | The job id was missing in **2 consecutive checks** (one miss is tolerated to avoid false alarms from a temporary glitch) |
| `reposted` | A new job with the same title and location as a job closed in the last 30 days (or a closed id that came back); `repostOf` points to the old id, `previous` has its title, location and `closedAt` |

- The **first check of a company is a baseline**: its jobs are recorded and nothing is reported, so you don't get hundreds of "new" jobs on day one. Turn on *Output all jobs on the first monitor run* to also get them (as `changeType: baseline`).
- If a board can't be loaded (network/server error), nothing is reported and nothing changes for that company; it is checked again next run.
- Domains whose board was only `guessed` are not monitored unless you turn on *Monitor guessed boards* (see *Confirm-first* above); they get a free `needs_confirmation` row.
- State is stored in a named key-value store `monitor-<yourUserId>-<monitorId>` in your account. Delete that store to reset the monitor.
- Keep the Monitor ID fixed when you add or remove companies. Without a Monitor ID, one is derived from the company list and filters, so changing them starts a new baseline. Changing the filters of an existing Monitor ID also records a new baseline.

#### Push

In monitor mode, set **Webhook URL** to get one POST per run when there are changes: a Slack incoming webhook (`hooks.slack.com/...`, short summary message) or any https endpoint (JSON with counts and all change rows, without descriptions). If delivery fails (3 attempts), the undelivered changes are saved with the monitor and re-sent together with the next run's changes (up to 1,000 kept). Every change is always in the run's dataset, whatever happens to the webhook. You can also use Apify integrations (Zapier, Make, Google Sheets, webhooks) on the run's dataset.

### Filters

Title keywords, departments/teams, locations, remote only, and posted within N days (snapshot only; ignored in monitor mode so aging jobs are not reported as closed). Matching is case-insensitive "contains".

### Output

Dataset, one row per job (snapshot) or per change (monitor):

`company, companyDomain, ats, board, jobId, title, department, team, location, isRemote, workplaceType, employmentType, salaryMin, salaryMax, currency, salaryInterval, postedAt, updatedAt, url, applyUrl, descriptionText, detection` plus in monitor mode `changeType, changedFields, previous, repostOf, firstSeen, lastSeen, detectedAt, monitorId`.

Free **company rows** (`recordType: company`) report `not_found`, `ambiguous`, `guessed`, `needs_confirmation` (monitor mode) and errors, include `boardUrl`, and in monitor mode one status row per company (baseline or change counts). Use the dataset views *Jobs*, *Changes* and *Companies*. A run summary is saved to the key-value record `OUTPUT`.

Example job row:

```json
{"recordType": "job", "company": "Duolingo", "ats": "greenhouse", "board": "duolingo", "jobId": "8851677002",
 "title": "Ad Sales Lead, Programmatic", "department": "Business Development", "location": "New York, NY",
 "isRemote": false, "salaryMin": 152000, "salaryMax": 228000, "currency": "USD", "postedAt": "2026-09-28T13:09:53Z",
 "url": "https://careers.duolingo.com/jobs/8851677002?gh_jid=8851677002", "detection": "input"}
```

Notes on fields: salary is filled only when the employer publishes it (Greenhouse pay transparency ranges, Lever `salaryRange`, Ashby compensation). `postedAt` is Greenhouse `first_published`, Lever `createdAt`, Ashby `publishedAt`; employers can refresh these, so monitoring relies on job ids, not dates. Ashby jobs marked unlisted are never output. By default, emails and phone numbers in descriptions are replaced with `[email removed]` / `[phone removed]`.

### Pricing (pay per event)

| Event | Price | When |
|---|---|---|
| `job` | $0.001 | Each job row in snapshot mode (and baseline rows if you turn them on) |
| `company-resolved` | $0.002 | A domain was auto-detected with detection `confirmed` (not for board URLs / ats:slug, cache hits, `guessed`, `ambiguous` or not found) |
| `monitor-check` | $0.002 | Monitor mode: each company checked per run (including the baseline check). Not for `needs_confirmation` (guessed, not monitored) companies |
| `job-change` | $0.01 | Monitor mode: each new / updated / closed / reposted row |

No start fee. Not found, ambiguous, failed and `needs_confirmation` companies are free. Company packs cost nothing extra: pack boards are charged like pasted board links (`job` / `monitor-check` / `job-change` only). Examples: watching 50 companies daily ≈ $0.10 per day in checks ($3 per month), plus $0.01 per change; snapshot of 3,000 jobs ≈ $3; monitoring 50 companies daily with ~5 changes a day ≈ $3 + $1.50 = ~$4.50 per month.

**Max cost per run** is respected: the Actor stops outputting before your limit. In monitor mode, changes that didn't fit are not lost: they are reported on the next run.

### Supported and not supported

- Supported: Greenhouse, Lever, Ashby public job board APIs (US instances).
- Not supported yet: Workday, Workable, SmartRecruiters, Recruitee, BambooHR, Gem, Dover, Wellfound, YC Work at a Startup, Greenhouse/Lever EU-hosted boards, iframes rendered only by JavaScript (auto-detection can miss these; paste the board URL instead).
- Max 500 companies per run.
- Domain auto-detection found a high-confidence board for roughly 45% of startups in our internal test (200 YC startups with 20+ staff) and less for large companies, so career-page links are the reliable path. Not affiliated with Greenhouse, Lever or Ashby.

### FAQ

**How do I get an alert when a company posts a new job?**
Add the company (board URL, `ats:slug`, domain, or a company pack), set Mode to `monitor` with a Monitor ID, and schedule the Actor daily. Add a Webhook URL to get the changes pushed to Slack or your own endpoint.

**Which job boards are supported?**
Greenhouse, Lever and Ashby public job boards. Workday and other systems are not covered yet — most startups and scale-ups use one of the three supported boards.

**How is this different from a job scraper?**
A scraper sells you the full list every time. This Actor remembers the previous state and charges mainly for what changed: new, updated, closed and reposted jobs.

**Can I just enter a company domain?**
Yes — the Actor tries to detect the board (about 45% high-confidence on startups in our test). Board URLs and `ats:slug` inputs always work and are the recommended path. Guessed boards are never monitored or charged until you confirm them.

**What does it cost to watch 30 companies?**
30 companies checked daily ≈ $1.80/month in monitor checks, plus $0.01 per detected change.

### Data source and responsible use

Data comes only from the employers' **public** job boards through the ATS providers' public, read-only job board endpoints (no login, no application endpoints, no internal jobs). Requests are rate-limited (max 2 per second per ATS) and identify this Actor in the User-Agent. Users must comply with the terms of the employers and ATS providers and applicable law. This Actor is not affiliated with or endorsed by Greenhouse, Lever or Ashby. Employers who want their company excluded can ask through the Actor's **Issues** tab and we will stop reading that board.

# Actor input Schema

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

One per line (max 500 in total, including company packs; duplicates are removed). Leave empty if you only use a company pack. Guaranteed path: paste the company's job board URL, e.g. https://job-boards.greenhouse.io/duolingo, https://boards.greenhouse.io/airbnb, https://jobs.lever.co/spotify, https://jobs.ashbyhq.com/posthog, or write ats:slug (greenhouse:airbnb, lever:spotify, ashby:ramp). Bonus: a plain company domain (stripe.com) is auto-detected from links on the company website, then by guessing the board name; check the 'detection' field (confirmed / guessed / ambiguous / not_found). Boards that can't be found get a free row.

## `companyPacks` (type: `array`):

Add a preset list of verified job boards to your companies. 'YC startups (verified boards)': 88 Y Combinator startups (active, hiring, 20+ staff, from a random sample of 200) whose Greenhouse, Lever or Ashby board was found via a link on the company website or a name guess confirmed by the job content, and re-checked on 2026-10-24 to have open jobs. Pack boards count like pasted board links (detection 'input', never charged as company-resolved) and toward the 500-company limit. Tip: in snapshot mode use filters (keywords, locations, posted within N days) to keep the run small; the pack had ~2,100 open jobs at verification.

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

Snapshot outputs every matching job. Monitor compares with the previous run of the same monitor ID and outputs only changes. The first monitor run of a company records a baseline and outputs no changes (see 'Output all jobs on the first monitor run'). Schedule monitor runs (e.g. daily) in Apify Schedules.

## `monitorId` (type: `string`):

Name of this watchlist (letters, digits, hyphens), e.g. 'fintech-targets'. Runs with the same monitor ID share state, so keep it fixed when you add or remove companies. If empty, an ID is derived from the company list and filters (changing them starts a new baseline). State lives in a named key-value store 'monitor-<yourUserId>-<monitorId>' in your account.

## `outputBaselineOnFirstRun` (type: `boolean`):

Monitor mode only. Off (default): the first check of a company only records a baseline (no rows, only the monitor-check event). On: also output all its current jobs with changeType 'baseline' (charged as 'job', like snapshot).

## `monitorGuessedCompanies` (type: `boolean`):

Monitor mode only. Off (default, confirm-first): a company domain whose board was only guessed from its name and not confirmed (detection 'guessed') is NOT monitored and NOT charged; you get a free Companies row with status 'needs_confirmation' and the found board URL. Paste that URL (or ats:slug) into Companies to confirm it. On: guessed boards are monitored like the others (monitor-check / job-change charged).

## `webhookUrl` (type: `string`):

Monitor mode only. If set and there are changes, one POST is sent at the end of the run to this https URL: a Slack incoming webhook (hooks.slack.com, posts a short summary) or any URL (generic JSON with all change rows, without descriptions). Not charged. You can also use Apify integrations (Zapier, Make, webhooks). If delivery fails, the undelivered changes are re-sent with the next run's changes; all changes are always in the dataset.

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

Keep only jobs whose title contains at least one of these (case-insensitive), e.g. engineer, sales. Empty = all jobs.

## `departments` (type: `array`):

Keep only jobs whose department or team contains one of these (case-insensitive). Empty = all.

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

Keep only jobs whose location contains one of these (case-insensitive), e.g. London, Remote, Germany. Empty = all.

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

Keep only jobs marked remote by the ATS or with 'remote' in the location.

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

Snapshot mode only: keep jobs posted (or, on Greenhouse, first published) within the last N days. 0 = no limit. Ignored in monitor mode so that aging jobs are not reported as closed.

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

Add the plain-text job description (descriptionText) to every job row.

## `stripContactInfo` (type: `boolean`):

Replaces email addresses and phone numbers in descriptionText with \[email removed] / \[phone removed] (recruiter contact details).

## `websiteLookup` (type: `boolean`):

For domain inputs only. On (default): first look for Greenhouse, Lever or Ashby links on the company's homepage, /careers, /jobs, /company/careers and /about/careers (one short request each), then fall back to guessing the board name. Off: guess only.

## Actor input object example

```json
{
  "companies": [
    "https://boards.greenhouse.io/duolingo",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/posthog"
  ],
  "companyPacks": [],
  "mode": "snapshot",
  "monitorId": "my-targets",
  "outputBaselineOnFirstRun": false,
  "monitorGuessedCompanies": false,
  "keywords": [],
  "departments": [],
  "locations": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "includeDescription": true,
  "stripContactInfo": true,
  "websiteLookup": true
}
```

# Actor output Schema

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

Job postings (snapshot) or changes (monitor).

## `changes` (type: `string`):

New, updated, closed and reposted jobs.

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

Per-company status, change counts, events requested.

# 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/duolingo",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/posthog"
    ],
    "monitorId": "my-targets"
};

// Run the Actor and wait for it to finish
const run = await client.actor("lone_fascia/ats-job-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": [
        "https://boards.greenhouse.io/duolingo",
        "https://jobs.lever.co/spotify",
        "https://jobs.ashbyhq.com/posthog",
    ],
    "monitorId": "my-targets",
}

# Run the Actor and wait for it to finish
run = client.actor("lone_fascia/ats-job-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": [
    "https://boards.greenhouse.io/duolingo",
    "https://jobs.lever.co/spotify",
    "https://jobs.ashbyhq.com/posthog"
  ],
  "monitorId": "my-targets"
}' |
apify call lone_fascia/ats-job-monitor --silent --output-dataset

```

## MCP server setup

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