# ATS New Job Postings Monitor — Greenhouse, Lever & Ashby (`magnova/ats-new-postings-monitor`) Actor

New job postings from Greenhouse, Lever and Ashby boards as clean records — company, title, location, department, team, employment type, posted date, job and apply URLs. Monitor mode: only postings you have not seen before. No API key. $0.01 per posting, no start fee, empty runs free.

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

## Pricing

from $7.00 / 1,000 job posting record fetcheds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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 New Job Postings Monitor — Greenhouse, Lever & Ashby

This Actor monitors Greenhouse, Lever and Ashby job boards and returns new job postings as structured data. Give it a list of boards — a Greenhouse board token, a Lever company slug, an Ashby board name — and each run fetches the boards' current postings through the platforms' own public job-board APIs and writes one clean record per posting: company, job title, location, department, team, employment type, workplace type, posted date, and the job and apply URLs. Run it on a schedule with only-new mode on, and every run delivers just the postings you have not seen before, deduplicated against a state store you own. Inputs are the board lists plus optional filters (posted-within days, title keywords, location keywords, departments) and a delivery cap; there is no API key, account or login to configure, because the three platforms publish these feeds openly for exactly this kind of reuse.

### What it does

Hiring is one of the strongest public signals a company sends. A company that just opened ten engineering roles is growing, has budget, and is about to buy tooling, services and office space; a company hiring its first sales leader is entering a new phase. Those signals appear first on the company's applicant-tracking system (ATS) job board — days or weeks before they show up anywhere else. This Actor turns three of the most common ATS platforms into one normalized, monitorable feed:

- **Greenhouse** — boards at `boards.greenhouse.io/<token>` (e.g. `airbnb`).
- **Lever** — company boards at `jobs.lever.co/<company>` (e.g. `leverdemo`).
- **Ashby** — boards at `jobs.ashbyhq.com/<name>` (e.g. `Ramp`).

Each run writes three kinds of rows to the dataset:

- **posting** rows — one per job posting, with the fields listed in the output table below. These are the only rows you pay for.
- **error** rows — one per board that could not be fetched (unknown board, HTTP error, unexpected response), with the source, board and reason. Never charged, and one bad board never stops the other boards from delivering.
- One **summary** row at the end of every run: the boards covered, how many postings were fetched per source, delivered, skipped as duplicates, filtered out, or failed. Never charged.

### Use with AI agents / MCP

The input is deliberately minimal so an agent can call this Actor zero-shot: the board lists come prefilled with working example boards and `postedWithinDays` is the only required field. A minimal call looks like this:

```json
{
  "postedWithinDays": 7
}
```

A filtered call — new engineering roles at three companies, remote-friendly locations only:

```json
{
  "greenhouseBoards": ["airbnb"],
  "leverCompanies": ["leverdemo"],
  "ashbyBoards": ["Ramp"],
  "titleKeywords": ["Engineer"],
  "locationKeywords": ["Remote"],
  "postedWithinDays": 14,
  "maxPostings": 100
}
```

Sample output (one posting row, from a real Ashby board):

```json
{
  "recordType": "posting",
  "source": "ashby",
  "company": "Ramp",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "location": "New York, NY (HQ)",
  "secondaryLocations": ["Remote (Canada)", "Remote (US)", "Miami, FL"],
  "department": "Engineering",
  "team": "Backend",
  "employmentType": "FullTime",
  "workplaceType": "Hybrid",
  "postedAt": "2026-04-07T17:12:35.753+00:00",
  "jobUrl": "https://jobs.ashbyhq.com/Ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/Ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application"
}
```

### Inputs

| Field | Description |
|---|---|
| Greenhouse boards (prefilled `["airbnb"]`) | Greenhouse board tokens to watch — the segment after `boards.greenhouse.io/` in a board URL. Empty list = skip Greenhouse. |
| Lever companies (prefilled `[]`) | Lever company slugs to watch — the segment after `jobs.lever.co/`. |
| Ashby boards (prefilled `["Ramp"]`) | Ashby board names to watch — the segment after `jobs.ashbyhq.com/`. Empty list = skip Ashby. |
| Posted within days (required, prefilled 7) | Only postings dated within the last N days. `0` = no date limit. See Limitations for what "date" means per source. |
| Title keywords | Only titles containing at least one keyword (case-insensitive substring). |
| Location keywords | Only postings whose location — including Ashby secondary locations — contains at least one keyword. |
| Departments | Only postings whose department or team matches one of these exactly (case-insensitive). Greenhouse's public payload carries no department, so this filter excludes Greenhouse postings. |
| Only new postings (prefilled on) | Monitor mode: skip postings delivered by earlier runs. Requires the state store below. |
| Seen-postings state store | A key-value store you own where the Actor remembers delivered posting IDs (read/write). If it is missing or fails, the run continues without dedupe and the summary says so — it never crashes. |
| Max postings (prefilled 200) | Cap on posting records delivered (and charged). |

### Output fields

| Field | Meaning |
|---|---|
| `source`, `company` | `greenhouse`, `lever` or `ashby`, and the board name exactly as you configured it. |
| `jobId` | The posting's unique ID within its source. Together, `source:company:jobId` uniquely identifies a posting and is the dedupe key. |
| `title` | Job title as posted. |
| `location`, `secondaryLocations` | Primary location as the board states it; Ashby boards also list secondary locations (other sources omit the field). |
| `department`, `team` | Department and team, when the source provides them (Lever and Ashby; Greenhouse's public payload has neither). |
| `employmentType` | As the source states it: Lever commitment (e.g. `Regular Full Time (Salary)`) or Ashby employment type (e.g. `FullTime`). |
| `workplaceType` | When known: Greenhouse "Workplace Type" metadata, Lever's workplace type, or Ashby's workplace type. |
| `postedAt` | When the posting first went live: Greenhouse first-published date, Lever creation date, Ashby published date. The "posted within N days" filter uses this field. |
| `updatedAt` | Greenhouse only: the posting's last-edited timestamp. Informational; never used for filtering. |
| `jobUrl`, `applyUrl` | The public posting page, and the direct application link when the source provides one. |

Job descriptions are deliberately not included: they are long, they differ wildly in format, and a monitor feed should stay light. The `jobUrl` always takes you to the full posting.

### Scheduling / only-new monitoring usage

This Actor is built to be scheduled. The pattern:

1. Create any key-value store in your Apify account (an empty one is fine) — this is where the Actor remembers delivered postings.
2. Run the Actor once with your boards and filters, `onlyNew` on, and that store selected in the **Seen-postings state store** input. This first run delivers the full in-window set as the **baseline** and records every delivered posting as seen.
3. Schedule the Actor (daily or weekly) with the same input. Each later run fetches the boards, skips anything already in the store — never delivering or charging it twice — and delivers only genuinely new postings. Postings that were filtered out or failed are *not* marked seen, so a later run with different filters can still deliver them.
4. Read the run's SUMMARY row to see fetched / delivered / duplicates-skipped counts per run.

If you turn `onlyNew` on but do not select a store, the Actor degrades gracefully: it delivers all matching postings and states explicitly in the SUMMARY record (and the run status) that dedupe was off — it never crashes and never silently pretends to dedupe.

Practical scheduling notes: a weekly run pairs naturally with `postedWithinDays: 7`; a daily run with `postedWithinDays: 2`. Boards that stop existing (a company migrates ATS) produce one error row per run and affect nothing else.

### Pricing

Pay-per-event only: **$0.01 per posting record delivered**. There is **no start fee**, no charge for the summary or error rows, no charge for duplicates or filtered-out postings, and **empty runs are free** — if nothing matches, you pay nothing.

| Apify plan | Price per posting record |
|---|---|
| FREE / BRONZE | $0.0100 |
| SILVER | $0.0085 |
| GOLD | $0.0070 |
| PLATINUM / DIAMOND | $0.0070 |

For scale: watching the prefilled boards with a 7-day window returns a few dozen postings — well under $1 per run at list price. Cost grows only with the postings you actually receive.

### FAQ

#### How do I find a company's board name?

Open the company's careers page. If jobs are hosted on Greenhouse, the board token is the part after `boards.greenhouse.io/`; for Lever it is the part after `jobs.lever.co/`; for Ashby, after `jobs.ashbyhq.com/`. Most tech companies link their ATS board directly from their careers page.

#### Why did my first only-new run deliver so many postings?

That is the baseline: with a fresh state store, everything in your window counts as "not seen yet". Later scheduled runs deliver only what is genuinely new.

#### A board returned an error row. What happened?

The board name is wrong, the company moved to a different ATS, or the platform hiccuped. The error row carries the HTTP status and a plain-language reason; fix the board name or remove it — the other boards are unaffected.

#### How fresh is the data?

The Actor reads each platform's public API at run time, so a posting appears in your feed on the first scheduled run after the company publishes it. Daily schedules mean day-old hiring signals at the latest.

#### Does this collect personal data?

No. Job postings are published by companies about roles, not people. The output contains no candidate data, no recruiter names or emails, and no descriptions — just the posting's public metadata and links.

### ATS New Postings API

There is no single cross-ATS "job postings API" product; each platform runs its own public board API for embedding postings on customer career pages — Greenhouse's Board API (`boards-api.greenhouse.io`), Lever's Postings API (`api.lever.co`), and Ashby's public job-board API (`api.ashbyhq.com`). This Actor normalizes all three into one record shape, one filter set and one dedupe state, so a schedule can watch companies across platforms without three separate integrations. No API key, account or login is involved on any of the three.

### Alternatives

Single-platform ATS scrapers exist for each of these platforms, and generic job-board scrapers cover individual boards one at a time — typically as one-off snapshots, priced per run or per page, with a different output shape per tool. This Actor is the monitor-shaped option: all three platforms in one feed, delivered on a schedule, only-new dedupe against your own state store, and a flat per-posting price with no start fee.

### Legal and data

**Sources.** All records come from the public job-board APIs that Greenhouse, Lever and Ashby themselves operate so that companies can embed their public postings on career pages and partner sites: `boards-api.greenhouse.io`, `api.lever.co` and `api.ashbyhq.com`. The Actor uses these feeds exactly as offered — unauthenticated, at a modest request rate, reading only publicly listed postings.

**No personal data.** Job postings are company-published role descriptions. They contain no candidate or applicant data, and this Actor additionally strips the payload down to a fixed field whitelist (title, location, department, dates, links) — job description text and any other payload fields are never copied into the output.

**Your responsibilities.** Postings remain each company's content. Respect each board owner's terms and the platforms' terms of use, use the data for legitimate recruiting, lead-generation and market-research purposes, and do not use this Actor to spam applicants or employers.

### Limitations

- **Greenhouse dates use the first-published date.** Companies often bulk-edit every open Greenhouse posting at once, which resets `updated_at`. This Actor filters on `first_published`, so a bulk edit never makes old jobs look new or bills you for them. The last-edit time is kept in `updatedAt` for reference.
- **Greenhouse has no department field** in its public board payload, so the departments filter excludes Greenhouse postings, and Greenhouse records omit `department`/`team`.
- **First only-new run = baseline.** With a fresh state store, the first run delivers the full in-window set (see Scheduling). If you want to start from zero instead, run once with a tiny window (e.g. `postedWithinDays: 1`) to seed the store.
- **Boards are fetched in full.** Each run downloads each configured board's current postings and filters locally; that is what makes only-new dedupe and the filters exact, and it means very large boards (thousands of postings) take proportionally longer — the run stops gracefully at the platform timeout with partial results and a summary, never a crash.
- **Unlisted/draft postings are not visible.** The public APIs return published postings only, which is precisely the public signal this Actor is for.

### Development

```bash
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt pytest
.venv/bin/python -m pytest tests/ -q
```

Tests are network-free: trimmed real payloads from the three platforms (Greenhouse/airbnb, Lever/leverdemo, Ashby/Ramp), whitelist assertions that no unlisted field ever reaches a record, filter/date/dedupe coverage, bad-input soft-fail coverage, and mocked-transport fetch tests.

# Actor input Schema

## `ashbyBoards` (type: `array`):

Ashby job boards to watch, by board name — the segment after jobs.ashbyhq.com/ in the board URL. Example: \["Ramp"]. Empty = do not fetch Ashby. If this input is omitted entirely, the prefill \["Ramp"] is used.

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

Only return postings whose department OR team is one of these (case-insensitive exact match). Example: \["Engineering", "Sales"]. Note: Greenhouse's public board payload carries no department, so this filter excludes Greenhouse postings. Empty = all departments.

## `greenhouseBoards` (type: `array`):

Greenhouse job boards to watch, by board token — the segment after boards.greenhouse.io/ in the board URL. Example: \["airbnb", "stripe"]. Empty = do not fetch Greenhouse. If this input is omitted entirely, the prefill \["airbnb"] is used.

## `leverCompanies` (type: `array`):

Lever companies to watch, by company slug — the segment after jobs.lever.co/ in the careers URL. Example: \["leverdemo"]. Empty = do not fetch Lever.

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

Only return postings whose location (or any Ashby secondary location) contains at least one of these words, case-insensitive. Example: \["Remote", "New York"]. Empty = all locations.

## `maxPostings` (type: `integer`):

Maximum number of posting records to deliver (and charge for). Error rows and the final SUMMARY record are never charged. Example: 200.

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

When true, postings already delivered by an earlier run are skipped (and never charged again). Requires the Seen-postings state store input below; without a store, dedupe is skipped with an explicit note in the SUMMARY record and all matching postings are delivered. Designed for scheduled daily/weekly monitor runs. Note: the first run with a fresh store delivers the full in-window set as the baseline.

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

Only return postings dated within the last N days. Uses the date each posting first went live (Greenhouse first-published, Lever created, Ashby published), so bulk edits of old postings never count as new. Example: 7 for a weekly monitor. 0 = no date limit.

## `stateStore` (type: `string`):

A key-value store this Actor may read and write to remember which postings it has already delivered (used only when Only new postings is on). Pick any store you own — the Actor keeps one small state record in it. If the store is missing or cannot be opened, the run continues without dedupe.

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

Only return postings whose job title contains at least one of these words, case-insensitive. Example: \["Engineer", "Product Manager"]. Empty = all titles.

## Actor input object example

```json
{
  "ashbyBoards": [
    "Ramp"
  ],
  "departments": [
    "Engineering"
  ],
  "greenhouseBoards": [
    "airbnb"
  ],
  "leverCompanies": [
    "leverdemo"
  ],
  "locationKeywords": [
    "Remote"
  ],
  "maxPostings": 200,
  "onlyNew": true,
  "postedWithinDays": 7,
  "titleKeywords": [
    "Engineer"
  ]
}
```

# Actor output Schema

## `results` (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 = {
    "ashbyBoards": [
        "Ramp"
    ],
    "greenhouseBoards": [
        "airbnb"
    ],
    "leverCompanies": [],
    "maxPostings": 200,
    "onlyNew": true,
    "postedWithinDays": 7
};

// Run the Actor and wait for it to finish
const run = await client.actor("magnova/ats-new-postings-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 = {
    "ashbyBoards": ["Ramp"],
    "greenhouseBoards": ["airbnb"],
    "leverCompanies": [],
    "maxPostings": 200,
    "onlyNew": True,
    "postedWithinDays": 7,
}

# Run the Actor and wait for it to finish
run = client.actor("magnova/ats-new-postings-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 '{
  "ashbyBoards": [
    "Ramp"
  ],
  "greenhouseBoards": [
    "airbnb"
  ],
  "leverCompanies": [],
  "maxPostings": 200,
  "onlyNew": true,
  "postedWithinDays": 7
}' |
apify call magnova/ats-new-postings-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,magnova/ats-new-postings-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/tSwLwCSsLYQS8sL5s/builds/bcbp1v8gJatSCxSk7/openapi.json
