# ATS Job Monitor: Greenhouse, Lever & Ashby Scraper (`melioristic_rafter/ats-job-monitor`) Actor

Get live jobs from Greenhouse, Lever and Ashby in one exportable feed. Track new, changed, reopened and removed listings with cautious history checks. No login or proxy needed.

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

## Pricing

from $1.00 / 1,000 job events

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 Job Monitor: Greenhouse, Lever & Ashby Scraper

Get live job postings from company career boards in one consistent feed. Export current jobs, or schedule a feed of **new, changed, reopened and removed listings** for a job board, recruiting workflow or hiring research.

Paste board URLs, choose your filters, and download JSON, CSV or Excel from Apify. No source API key, browser or proxy is needed.

### Quick start

1. Copy a company's Greenhouse, Lever or Ashby job-board URL.
2. Add it under **Company job boards** and click **Start**.
3. Open **Job events** to inspect or download your results. Open **Run summary** for coverage and board health.

```json
{
  "boards": [
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/leverdemo",
    "https://jobs.ashbyhq.com/Ashby"
  ],
  "mode": "snapshot",
  "maxResults": 5000
}
```

These providers were validated on Apify cloud on October 17, 2026. Availability and job counts change.

### Monitor new and removed jobs

```json
{
  "boards": ["greenhouse:stripe", "ashby:Ashby"],
  "mode": "changes",
  "historyKey": "daily-hiring",
  "titleKeywords": ["engineer", "data"],
  "maxJobsPerBoard": 5000,
  "maxResults": 10000
}
```

Save this input as an Apify task and schedule it daily. Reuse the history name in your account. The first complete run returns matching jobs as `new`. Later runs skip unchanged jobs in changes mode. Apify integrations can deliver the dataset to your own database, n8n, Make or Google Sheets.

Use one schedule per history name. A server-managed lock protects the entire monitoring run. An overlapping run exits with `HISTORY_BUSY`, reads no boards and incurs no board or row fees; the automatic start fee still applies. After an abrupt stop, the lock can take up to two minutes to expire. Different history names operate independently.

Changing title/location/remote filters or removal-confirmation settings creates a fresh baseline. Changing description visibility or switching output mode preserves it. Use a new history name to reset monitoring. A deleted history cannot be reconstructed.

### Pricing

Launch pricing (configured and verified privately on Apify):

| Event | Price | What it covers |
| --- | ---: | --- |
| Readable board scan | $0.015 | A valid source read and a board report, including empty or unchanged boards |
| Job event | $0.001 | One current job or lifecycle row saved to the dataset |
| Actor start | $0.00005 | Apify's automatic event per container start at 512 MB, including resurrection |

Examples: one board with 100 returned jobs costs **$0.11505**; the same unchanged board in changes mode costs **$0.01505**. Ten boards with 1,000 total job events cost **$1.15005**. Pay per event + usage is disabled. Source errors and malformed feeds have no board-scan charge; the start event still applies. A readable board over your job limit is a charged scan with a partial snapshot in snapshot mode and no rows in changes mode.

**Analyzed jobs are not charged per row.** In snapshot mode every matching current job is returned and charged. In changes mode only matching new, changed, reopened or newly confirmed removed rows are returned and charged. For example, scanning 1,000 unchanged jobs on one board costs $0.01505 in changes mode, versus $1.01505 for an unfiltered snapshot. The scan fee makes periodic monitoring sustainable even when there are no changes. The first monitoring baseline is charged for its returned new rows.

Set **Maximum cost per run** in Apify Run options, as well as `maxResults`. Maximum at default input limits: `boards × $0.015 + maxResults × $0.001 + $0.00005`. Source failures can make a run unproductive; use board reports to correct invalid URLs. Budget-limited boards do not advance history when output is incomplete. A new run is a separately billed scan and can repeat rows from an incomplete baseline. To recover a crashed run, use **Resurrect** on that same run instead of **Start**.

### Supported input

| Source | Accepted board reference |
| --- | --- |
| Greenhouse | `greenhouse:stripe`, `https://boards.greenhouse.io/stripe`, `https://job-boards.greenhouse.io/stripe` |
| Lever global | `lever:leverdemo`, `https://jobs.lever.co/leverdemo` |
| Lever EU | `lever-eu:company`, `https://jobs.eu.lever.co/company` |
| Ashby | `ashby:Ashby`, `https://jobs.ashbyhq.com/Ashby` |

Slugs retain their case. A supported job-detail URL is reduced to its board. Only listed API hosts are contacted; arbitrary company websites are not crawled. Lever EU must be selected explicitly. An empty list, unknown host or invalid option stops the run before source requests. Duplicate board references are removed.

You supply the companies. There is no global job search or company discovery. Workday, LinkedIn, Indeed and applicant/candidate data are outside this product's scope.

| Option | Default | Meaning |
| --- | --- | --- |
| `mode` | `snapshot` | Current matching jobs; with history, also freshly confirmed removals |
| `historyKey` | empty | Optional persistent history name; required for `changes` |
| `titleKeywords`, `locationKeywords` | empty lists | Case-insensitive OR within each list; AND between the two lists |
| `remoteOnly` | false | Require a source remote label or “remote” in the location |
| `includeDescription` | true | Plain text, email addresses masked, maximum 8,000 characters |
| `maxJobsPerBoard` | 3,000 | Maximum processed jobs per board; allowed 1–5,000 |
| `maxResults` | 10,000 | Total saved job rows; allowed 1–20,000 |
| `maxRunSeconds` | 300 | Application time budget, 10–1,800 seconds |
| `requestTimeoutSeconds` | 25 | Per-attempt timeout, 2–60 seconds |
| `missingConfirmations` | 2 | Complete missing readings before removal, 2–5 |
| `minMissingIntervalSeconds` | 3,600 | Minimum spacing between missing readings, 300–86,400 seconds |

Up to 50 boards per run. Transient network errors, HTTP 429 and selected server errors receive up to two retries. Long Retry-After values stop that board instead of retrying early. A response may contain up to 20 MiB of decoded JSON. HTTP 401/403, HTML challenges and redirects are not bypassed.

### Output

Each dataset row is a job or job lifecycle event, with a stable `id`, a deterministic `eventId`, source, board, title, department, location, workplace type, source dates, URLs, description and observation dates. Missing values are `null`.

| Field | Interpretation |
| --- | --- |
| `changeType` | `snapshot`, `new`, `changed`, `unchanged`, `removed`, `reopened` |
| `firstSeenAt`, `lastSeenAt` | Your monitor's observations, not source publication dates |
| `publishedAt`, `updatedAt` | Source dates when available; never invented |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval` | Source-structured pay fields when exposed, mainly Lever; no estimated or annualized salaries |
| `compensationText` | Ashby's published compensation summary when available |
| `descriptionTruncated` | Whether the displayed description was shortened |
| `contentHash` | Detects meaningful normalized changes, including text beyond display truncation |

Greenhouse's list endpoint does not provide the detail endpoint's pay-transparency ranges; those are left `null`. Ashby's compensation bands are not flattened into a guessed global numeric range. `company` is normally the board slug rather than a verified legal company name. Country and seniority are not inferred. Unknown remote status stays unknown; a location-text match is a heuristic.

Full schema: `schemas/job.schema.json`. Real local sample: `reports/sample-jobs.json` in the source package. Apify provides CSV/XLSX export automatically; use full JSON when you need all fields, or choose columns for a spreadsheet export.

The run key-value store contains `RUN_SUMMARY`, `BILLING_AUDIT` and one `BOARD-…` report per visited board. `analyzedJobs` counts all normalized current postings before filters and caps; `rows` counts delivered dataset rows; `emittedEventCounts` breaks those rows down by type. `newJobsDetected` counts new postings, while `actualChangesDetected` counts changed, reopened and removed postings before output filters. `lifecycleCounts` classifies processed postings; for a size-limited board it only covers the processed slice. `chargeableBoards` counts valid readable scans; `billedBoards` counts scans submitted under PPE, and `BILLING_AUDIT` records the SDK event counters. Non-monetized runs have zero billed counters. Board reports remain available even when no job matches.

### How removal detection works

A posting is marked `removed` only after it is absent from multiple **complete, successful** source reads with the configured spacing. Missing from a public board does not prove that a vacancy was filled. Failures, malformed feeds, job caps and incomplete output never advance the board's history.

If at least half of a previously active board with 10 or more jobs disappears, a consistent extra reading is required before missing confirmations begin. This also delays removal when an employer legitimately empties its board. Reappearing removed jobs become `reopened`. Removed metadata is retained for 90 days, then discarded; later appearances become `new`. Old descriptions are not stored in history, so removal rows contain metadata and `description: null`.

History stores at most 20,000 entries per board. Cloud runs save their work plan and operation journal before delivering or billing results. Resurrecting the **same run ID** resumes the frozen plan, verifies an interrupted append against the existing dataset and never resubmits a previously attempted board or row charge. This protection remains effective beyond Apify's three-minute API idempotency window. A completed run can be resurrected without creating another scan or another copy of its rows.

Charges with an uncertain outcome are never retried; an unconfirmed fee may be waived in the customer's favor. Apify automatically charges another $0.00005 start event on resurrection; the Actor reserves it within the remaining budget. If an interrupted append cannot be verified, the run stops with `RECOVERY_UNCERTAIN`, preserving history and attempting no additional row write or fee. Inspect that run before starting a fresh scan. The default dataset is append-only, so the product prioritizes preventing duplicate delivery and charges over silently retrying an ambiguous write. A recovery may wait up to about two minutes for the previous process's lock to expire.

Keep the run's key-value storage intact until recovery is complete. Missing journals, changed input or a conflicting newer history stop recovery safely. Runs created before the recovery upgrade cannot be resurrected with the new build if they already contain data or charges: they stop with `LEGACY_RUN_CANNOT_RESUME`. New run IDs have independent billing; the protection is not an unlimited free rescan across distinct runs. `eventId` remains useful for downstream deduplication.

### Data access and responsible use

The Actor reads the documented public [Greenhouse Job Board API](https://docs.greenhouse.io/job-board.html), [Lever Postings API](https://github.com/lever/postings-api) and [Ashby Job Postings API](https://developers.ashbyhq.com/docs/public-job-posting-api). It does not authenticate, submit applications, collect candidates or circumvent access controls. Public access does not grant unrestricted reuse rights: use boards and job content only as permitted by the source, employer and applicable terms. Email masking reduces incidental exposure; descriptions can still contain names or other personal information. Review retained data and your reuse rights before redistribution.

Limited Apify permissions are sufficient. History and its lock queue are created in the running user's account. Store names hash the Actor ID, running user ID and history name; different users and different monitors have separate namespaces. No data is sent to an external analytics service. Stop the schedule and delete its named history store and lock queue to remove retained metadata; manage dataset and run-storage retention in your Apify account separately. This product is independent of the ATS vendors.

### Troubleshooting

- `HTTP_404`: verify the board's exact slug and region. A company may have moved ATS.
- `HTTP_401` / `HTTP_403`: access was denied; the Actor stops that source.
- `INVALID_SCHEMA` / `INCOMPLETE_SOURCE`: the source changed or returned incomplete data; history is preserved.
- `PARTIAL_SOURCE_LIMIT`: increase `maxJobsPerBoard` within the hard limit, or monitor a smaller board. Snapshot mode returns a partial untracked snapshot; changes mode returns no rows for that board. History stays unchanged.
- `HISTORY_BUSY`: another run holds this monitor's lock. Retry after it finishes; allow two minutes after a crash.
- `RECOVERY_UNCERTAIN`: an interrupted append has no verifiable receipt. It was not repeated or billed again; inspect existing output, then start a fresh scan if needed.
- `RECOVERY_HISTORY_CONFLICT` / `RECOVERY_IDENTITY_MISMATCH`: history or input changed while the run was interrupted. Recovery stops without overwriting newer data.
- `LEGACY_RUN_CANNOT_RESUME`: start a new run with the current build; the older run has no recovery journal.
- `PARTIAL_OUTPUT_LIMIT` / `LIMIT_REACHED`: raise your result, time or cost limit for a complete board sync.
- No changes: inspect the board reports; unchanged boards still produce scan reports and charges.

When every board fails, the run fails. Mixed source failures produce a usable partial run with `PARTIAL_FAILURE` in `RUN_SUMMARY`. Monitor this summary in addition to Apify run status. Report an issue with your run ID and a non-sensitive board URL.

### Developer commands

```sh
pnpm install --frozen-lockfile --ignore-scripts
pnpm test
pnpm check
node scripts/run-local.js examples/snapshot.json
pnpm smoke
```

`smoke` reads live public sources without calling Apify runs. It uses in-memory state and simulated event charges. Deployment, prices, operational checks and private cloud acceptance evidence are documented in `docs/` and `reports/cloud/`. The private validation harness is excluded from the customer Actor. Monetization activation and paid receipts are separate from technical acceptance; see the current launch report.

# Actor input Schema

## `boards` (type: `array`):

1–50 board URLs or source:slug values. Supported: greenhouse, lever, lever-eu, ashby. You supply the companies; this does not search the whole web.

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

Changes requires a history key. The first complete run returns all matching jobs as new. Snapshot with history can also include newly confirmed removals.

## `historyKey` (type: `string`):

Optional name, e.g. daily-tech-jobs. Reuse it across runs in your account. History is isolated per user and monitor. Concurrent runs of the same monitor exit HISTORY_BUSY; one schedule per name is recommended.

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

Optional case-insensitive OR filter. Changing filters creates a fresh baseline.

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

Optional text filter. No inferred country or geocoding.

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

Keeps source-labeled remote jobs or locations containing remote. Unknown workplace types are excluded.

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

Up to 8,000 characters per job; email addresses masked. Removal events retain metadata without old description text.

## `maxJobsPerBoard` (type: `integer`):

Larger boards yield a partial snapshot in snapshot mode and no rows in changes mode. The readable scan is charged; history is preserved and no removals are inferred.

## `maxResults` (type: `integer`):

Caps row charges. Also set Maximum cost per run in Run options. Boards whose rows cannot all be delivered do not advance history.

## `maxRunSeconds` (type: `integer`):

Stops new work at this limit; allow extra time in Apify Run options for saving outputs.

## `requestTimeoutSeconds` (type: `integer`):

Up to two retries for transient failures. Access denied responses are never bypassed.

## `missingConfirmations` (type: `integer`):

Complete readings only. Missing from a board does not prove the position was filled.

## `minMissingIntervalSeconds` (type: `integer`):

Default one hour. Sudden losses of at least half a board with 10+ active jobs require an extra consistent reading.

## Actor input object example

```json
{
  "boards": [
    "https://jobs.ashbyhq.com/Ashby"
  ],
  "mode": "snapshot",
  "historyKey": "",
  "titleKeywords": [],
  "locationKeywords": [],
  "remoteOnly": false,
  "includeDescription": true,
  "maxJobsPerBoard": 3000,
  "maxResults": 10000,
  "maxRunSeconds": 300,
  "requestTimeoutSeconds": 25,
  "missingConfirmations": 2,
  "minMissingIntervalSeconds": 3600
}
```

# Actor output Schema

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

No description

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

No description

## `billing` (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 = {
    "boards": [
        "https://jobs.ashbyhq.com/Ashby"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("melioristic_rafter/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 = { "boards": ["https://jobs.ashbyhq.com/Ashby"] }

# Run the Actor and wait for it to finish
run = client.actor("melioristic_rafter/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 '{
  "boards": [
    "https://jobs.ashbyhq.com/Ashby"
  ]
}' |
apify call melioristic_rafter/ats-job-monitor --silent --output-dataset

```

## MCP server setup

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