# ATS Jobs Monitor: Greenhouse, Lever & Ashby (`veinwork/ats-jobs-monitor`) Actor

Lightweight structured jobs and hiring signals from public ATS boards. By Veinwork.

- **URL**: https://apify.com/veinwork/ats-jobs-monitor.md
- **Developed by:** [Veinwork](https://apify.com/veinwork) (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 $0.50 / 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

## Greenhouse, Lever & Ashby Jobs Scraper API — New & Closed Job Monitor

By Veinwork. A Greenhouse jobs scraper, Lever jobs scraper and Ashby jobs scraper in one Actor: get lightweight, structured job postings and hiring signals directly from company career boards (ATS job boards), ready for AI agents, spreadsheets and CRMs. Run it once for a jobs export, or schedule it as a job monitor that reports new and closed jobs. No login or API key needed.

### 1. Why use ATS Jobs Monitor?

- **$0.50 per 1,000 delivered jobs.** Closed-job notices and filtered-out jobs are free. A small start event also applies.
- **Small records for AI agents and tables.** Titles, departments, locations, remote status, salary numbers, dates and links without bulky descriptions. Our November 1, 2026 sample of 1,365 jobs averaged 0.75 KB per record; individual records vary. Open `postingUrl` for the full description.
- **Monitor hiring signals.** `onlyNew` delivers newly seen jobs with free closed-job notices. Each run writes a per-company `SUMMARY` with open, matching, new, delivered and closed counts for sales signals and investment research.
- **Transparent collection.** Official public APIs only, Lever crawl-delay respected, unlisted Ashby jobs excluded, and an honest User-Agent.

### 2. Input and examples

| Field | Default | Meaning |
|---|---|---|
| `companies` | Required | 1–100 board URLs, `ats:slug` entries or bare slugs for automatic detection. Prefixes: `greenhouse`, `lever`, `ashby`. Duplicate ATS/slug pairs are processed once. Entries that cannot be parsed are skipped and reported with status `invalid` in `SUMMARY` and the run warnings; the run fails only when no entry is usable. |
| `titleKeywords` | `[]` | Keep titles containing any keyword; up to 50 strings, case-insensitive substring matching. |
| `excludeTitleKeywords` | `[]` | Drop titles containing any keyword; up to 50 strings, case-insensitive. |
| `locationKeywords` | `[]` | Keep jobs with any listed location containing any keyword; up to 50 strings, case-insensitive. |
| `remoteOnly` | `false` | Keep only jobs identified as remote. |
| `postedWithinDays` | Unset | Optional integer 1–365; includes the cutoff. Jobs without a publication date are excluded when enabled. |
| `onlyNew` | `false` | Deliver newly seen jobs, plus free closure notices. State is saved under `STATE` in the named key-value store `ats-jobs-monitor-state`. |
| `maxJobsPerCompany` | `1000` | Maximum matching jobs delivered per company per run; integer 1–10000. |

One-time collection from three companies:

```json
{
  "companies": ["airbnb", "https://jobs.lever.co/zoox", "ashby:ramp"]
}
```

Remote engineering jobs:

```json
{
  "companies": ["greenhouse:airbnb", "lever:outreach", "ashby:ramp"],
  "titleKeywords": ["engineer"],
  "excludeTitleKeywords": ["intern"],
  "remoteOnly": true
}
```

Daily monitoring input (configure a daily Apify Schedule separately):

```json
{
  "companies": ["greenhouse:airbnb", "lever:outreach", "ashby:ramp"],
  "onlyNew": true,
  "maxJobsPerCompany": 1000
}
```

The first successful check treats all open jobs as new. Later checks compare against the named store. New jobs withheld by the per-company cap or cost limit remain new next run. Failed companies keep their previous state and do not generate false closure notices.

### 3. Output

The default dataset contains free `closed` rows first, then delivered `job` rows. Both use the same field set. Dates are UTC; missing values are `null`; currencies and salary intervals retain the source's meaning. `isNew` is included even when `onlyNew` is false. Illustrative examples follow.

A job row:

```json
{
  "recordType": "job",
  "jobKey": "greenhouse:airbnb:8241468",
  "ats": "greenhouse",
  "companySlug": "airbnb",
  "companyName": "Airbnb",
  "jobId": "8241468",
  "title": "Senior Demand Generation Specialist, Hotels (12M FTC)",
  "department": "Program Management",
  "team": null,
  "location": "France, Italy",
  "allLocations": [
    "France, Italy"
  ],
  "remote": null,
  "workplaceType": null,
  "employmentType": null,
  "salaryMin": 74000.0,
  "salaryMax": 92000.0,
  "salaryCurrency": "EUR",
  "salaryInterval": null,
  "postedAt": "2026-09-30T12:12:47Z",
  "updatedAt": "2026-10-12T18:02:19Z",
  "postingUrl": "https://careers.airbnb.com/positions/8241468?gh_jid=8241468",
  "applyUrl": "https://careers.airbnb.com/positions/8241468?gh_jid=8241468",
  "isNew": true,
  "closedDetectedAt": null
}
```

A closed-job row (illustrative):

```json
{
  "recordType": "closed",
  "jobKey": "greenhouse:airbnb:previous-role",
  "ats": "greenhouse",
  "companySlug": "airbnb",
  "companyName": null,
  "jobId": "previous-role",
  "title": "Previous engineering role",
  "department": null,
  "team": null,
  "location": null,
  "allLocations": [],
  "remote": null,
  "workplaceType": null,
  "employmentType": null,
  "salaryMin": null,
  "salaryMax": null,
  "salaryCurrency": null,
  "salaryInterval": null,
  "postedAt": null,
  "updatedAt": null,
  "postingUrl": null,
  "applyUrl": null,
  "isNew": false,
  "closedDetectedAt": "2026-11-01T12:00:00Z"
}
```

One company record in the `SUMMARY` array in the default key-value store:

```json
{
  "input": "greenhouse:airbnb",
  "ats": "greenhouse",
  "companySlug": "airbnb",
  "companyName": "Airbnb",
  "status": "ok",
  "openJobs": 3,
  "matchedJobs": 3,
  "newJobs": 2,
  "deliveredJobs": 2,
  "closedJobs": 1,
  "error": null
}
```

`openJobs` counts unique jobs before filters. `matchedJobs` counts matching jobs before the cap; `newJobs` counts newly seen matching jobs; `deliveredJobs` reflects actual delivery after the cost limit. `closedJobs` counts eligible closure notices. Output links expose the dataset overview and `SUMMARY`. Summary `status` values are `ok`, `not_found`, `error`, and `invalid`. Company errors appear in the summary and warnings; other companies can still succeed.

### 4. Pricing and cost limits

| Event | Price | Charged for |
|---|---|---|
| `job` | $0.0005 per job ($0.50 per 1,000) | Each delivered job row |
| `apify-actor-start` | $0.0005 per GB-memory start event | A run start; default 512 MB counts as one event |

Closed-job notices, summaries and filtered-out jobs are not charged. A zero-new-job run can still incur the start event. Under pay-per-event pricing, delivered job rows equal job events charged.

When the configured cost limit prevents further delivery, the run stops pushing jobs and reports `stopped at the cost limit`. Free closure notices are delivered first, even with zero job budget. Undelivered new jobs remain eligible next run. Reaching the limit exactly after the final job is complete delivery, not a truncated run.

### 5. Sources and responsible collection

Only official public APIs are used:

- Greenhouse: `https://boards-api.greenhouse.io/v1/boards/{slug}/jobs?pay_transparency=true`, with departments from `https://boards-api.greenhouse.io/v1/boards/{slug}/departments`.
- Lever: `https://api.lever.co/v0/postings/{slug}?mode=json`.
- Ashby: `https://api.ashbyhq.com/posting-api/job-board/{slug}?includeCompensation=true`.

No full job descriptions or employer-written long text are exported. Open `postingUrl` for the original. Lever's crawl-delay is respected: requests, including retries and detection probes, start at least one second apart. Greenhouse embed URLs may be parsed as input but are never requested. Ashby jobs with `isListed: false` are excluded; compensation is exported only when explicitly marked for display.

Requests identify themselves as `VeinworkATSJobsMonitor/0.1 (+https://apify.com/veinwork/ats-jobs-monitor)`. No login, authentication headers, rotating proxies or browser impersonation are used. If a provider requires authentication or blocks access, the service is withdrawn rather than bypassing the restriction.

### 6. Known limitations

- Only Greenhouse, Lever and Ashby US-region API hosts are supported. EU hosts `job-boards.eu.greenhouse.io` and `jobs.eu.lever.co` are not supported. This host restriction does not exclude jobs located outside the US.
- This is a company-board monitor, not a global job database or web-wide keyword search engine.
- Greenhouse does not provide team, employment type or salary interval in this output. Lever and Ashby do not provide company display names. Missing fields remain `null`.
- Jobs missing an ID, title or posting link are omitted. Descriptions and salary narrative text are not included.
- Newness is tracked per company, independently of filters: filtered-out new jobs still count as seen. Changing filters does not make those jobs new again. Runs using the same named store share history; avoid overlapping monitoring runs that could overwrite it.
- Closure notices use title filters only; location, remote and date filters cannot be applied to a stored historical title.
- Many Lever companies make runs slower because requests are spaced by at least one second. Detection and retries can add requests and time.
- A run accepts at most 100 company inputs. Detection tries Greenhouse, Lever, then Ashby; exhausted request errors stop detection for that company instead of guessing.

### 7. FAQ

**How do I find the slug?**

Copy a supported board URL such as `https://jobs.lever.co/zoox`. Its first path segment, `zoox`, is the slug. You can enter `lever:zoox` directly. A bare slug such as `airbnb` enables automatic detection; an explicit prefix avoids extra probes. Whitespace in company names is removed and the result is lowercased for automatic detection; for example, `Open AI` is detected as `openai`.

**How do I run this daily?**

Save monitoring input as an Apify task and attach a daily Schedule. Set `onlyNew` to `true` and retain the same state store. Starting this Actor once does not create a recurring schedule.

**How do I connect a webhook?**

Configure an Apify integration or webhook for a successful run. Your receiver can retrieve jobs and closures from the run's default dataset and company counts from the default key-value store's `SUMMARY`. Check status and summary errors before treating a successful run as complete coverage.

# Actor input Schema

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

Up to 100 companies: use a board URL, ats:slug (greenhouse, lever or ashby), or a bare slug for automatic ATS detection.

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

Keep jobs whose title contains any keyword, ignoring case. Empty means all titles.

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

Exclude jobs whose title contains any keyword, ignoring case.

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

Keep jobs with any listed location containing any keyword, ignoring case.

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

Keep only jobs explicitly identified as remote.

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

Keep jobs published within this many days, including the boundary. Omit to disable the date filter.

## `onlyNew` (type: `boolean`):

Deliver only jobs first seen since the previous run, plus free closed-job notices. State is saved in the named key-value store ats-jobs-monitor-state.

## `maxJobsPerCompany` (type: `integer`):

Maximum matching job rows delivered per company per run. New jobs withheld by this limit remain new next run.

## Actor input object example

```json
{
  "companies": [
    "airbnb",
    "https://jobs.lever.co/zoox",
    "ashby:ramp"
  ],
  "titleKeywords": [],
  "excludeTitleKeywords": [],
  "locationKeywords": [],
  "remoteOnly": false,
  "onlyNew": false,
  "maxJobsPerCompany": 1000
}
```

# Actor output Schema

## `jobs` (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 = {
    "companies": [
        "airbnb",
        "https://jobs.lever.co/zoox",
        "ashby:ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("veinwork/ats-jobs-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": [
        "airbnb",
        "https://jobs.lever.co/zoox",
        "ashby:ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("veinwork/ats-jobs-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": [
    "airbnb",
    "https://jobs.lever.co/zoox",
    "ashby:ramp"
  ]
}' |
apify call veinwork/ats-jobs-monitor --silent --output-dataset

```

## MCP server setup

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