# ATS Jobs Scraper & Hiring Monitor - Greenhouse, Lever, Ashby (`ivan-petrus-g/company-hiring-monitor`) Actor

Track which companies are hiring. Enter company domains or careers URLs; the ATS is auto-detected (Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee, Personio, Teamtailor, BambooHR). Get only NEW jobs since the last run, closed jobs, and a hiring summary per company.

- **URL**: https://apify.com/ivan-petrus-g/company-hiring-monitor.md
- **Developed by:** [Ivan Petrus](https://apify.com/ivan-petrus-g) (community)
- **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

## ATS Jobs Scraper & Hiring Monitor: Greenhouse, Lever, Ashby, Workday & More

Track **who is hiring, for what, and where**. Enter company domains or careers-page URLs. The Actor finds the company's job board, reads it through the board's **official public job-board API**, and returns:

- **New jobs since the last run** (monitor mode, default) or all open jobs.
- **Closed jobs**: jobs that disappeared since the last run. These rows are free.
- **A hiring summary per company**: open roles, change vs last run, trend, top departments and locations, remote and salary counts, new departments and new locations. These rows are free.

Supported job systems (ATS): **Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Recruitee, Personio, Teamtailor, BambooHR**.

Typical uses:

- Sales and RevOps teams use hiring as a buying signal ("Acme just opened 5 data-engineering roles in Berlin").
- Recruiters and agencies watch target companies.
- Investors and analysts track headcount momentum.
- Job boards and newsletters aggregate fresh roles from specific companies.

### Sample output

A job row and the free summary row from a real Apify cloud run (Oct 2026, `greenhouse:n26`), shortened:

```json
[
  {
    "type": "job",
    "company": "N26",
    "title": "AFC Operations Team Lead - French Team",
    "department": "AFC Operations",
    "location": "Madrid",
    "employmentType": "Full-time",
    "postedAt": "2026-09-17T08:31:23Z",
    "url": "https://n26.com/en-eu/careers/positions/8201489?gh_jid=8201489",
    "ats": "Greenhouse",
    "isNew": false
  },
  {
    "type": "summary",
    "company": "N26",
    "ats": "Greenhouse",
    "openJobs": 44,
    "detectionMethod": "explicit",
    "topDepartments": [{"name": "Group Internal Audit", "count": 4}, {"name": "Risk", "count": 3}, …],
    "topLocations": [{"name": "Berlin", "count": 21}, {"name": "Madrid", "count": 8}, …]
  }
]
```

### How it works

1. **Detection.** For each input the Actor works out which ATS the company uses:
   - A job-board URL or `ats:token` is used directly (`greenhouse:airbnb`, `https://jobs.lever.co/palantir`, `https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`).
   - For a domain or careers URL, it scans the company's careers pages (`/careers`, `/jobs`, `careers.` and `jobs.` subdomains, the homepage, plus a few "Careers" links) for ATS links and embeds.
   - If nothing is found, it tries the domain name as the board token on each ATS (for example, stripe.com becomes greenhouse:stripe). It picks a guessed board only if that board has open jobs. Such matches are marked `detectionMethod: "slug-guess"`, with a confidence value.
2. **Fetching.** Jobs come from the public endpoints that each ATS publishes so companies can embed job boards on their own sites. There is no login, no scraping of personal data, and normally no proxy.
3. **Diffing.** The open jobs of each company are stored in a named key-value store (`stateStoreName`). The next run compares against them and reports new and closed jobs.

### Input example

```json
{
  "companies": ["huggingface.co", "stripe.com", "https://jobs.lever.co/palantir", "greenhouse:airbnb",
                "workday:nvidia.wd5/NVIDIAExternalCareerSite"],
  "onlyNewJobs": true,
  "firstRunBehavior": "baselineOnly",
  "titleKeywords": ["engineer", "data"],
  "locationKeywords": ["Germany", "Berlin", "Remote"]
}
```

You can also pass explicit boards as JSON: `[{"ats": "greenhouse", "boardToken": "airbnb", "companyName": "Airbnb"}]`.

**Scheduling:** create a task with your company list and schedule it daily or weekly. Each run outputs only what changed.

### Output

**Job row** (`type: "job"`):

```json
{
  "type": "job", "company": "Channable", "ats": "Recruitee", "jobId": "2622523",
  "title": "Python Software Engineer - AI team", "department": "Engineering", "team": null,
  "location": "Utrecht, Utrecht, Netherlands", "locations": ["Utrecht, Utrecht, Netherlands"], "country": "NL",
  "remote": false, "workplaceType": "hybrid", "employmentType": "Full-time",
  "salaryMin": 5000, "salaryMax": 7000, "salaryCurrency": "EUR", "salaryPeriod": "month", "salaryText": "5,000–7,000 EUR/month",
  "postedAt": "2026-06-02T10:10:41Z", "updatedAt": "2026-10-08T01:54:04Z",
  "url": "https://jobs.channable.com/o/python-software-engineer-ai-team-1", "isNew": true,
  "firstSeenAt": "2026-10-08T07:46:17Z"
}
```

**Summary row** (`type: "summary"`, free), one per company:

- `openJobs`, `previousOpenJobs`, `openJobsChange`, `openJobsChangePct`, `hiringTrend` (growing / stable / shrinking).
- `newJobs`, `closedJobs`, `newJobTitles`, `closedJobTitles`.
- `topDepartments`, `topLocations`, `newDepartments`, `newLocations`.
- `remoteJobs`, `hybridJobs`, `jobsWithSalary`, `employmentTypes`, `newestPostedAt`.
- `boardUrl`, `detectionMethod`, `detectionConfidence`, `listComplete`, `notes`.

**Closed-job row** (`type: "closedJob"`, free): title, department, location, URL, `firstSeenAt` and `closedDetectedAt`.

**Error row** (`type: "error"`, free): companies whose job board could not be detected or read. The row says what was scanned and guessed, plus a hint on how to fix it. Companies that fail are not charged.

The dataset has table views for Jobs, Hiring summary and Closed jobs. `RUN_SUMMARY` in the default key-value store holds run statistics.

### Data availability per ATS (honest notes)

| ATS | Department | Location / remote | Salary | Posted date | Description option |
|---|---|---|---|---|---|
| Greenhouse | yes | yes / from custom fields | when the company publishes pay ranges | first published + updated | yes |
| Lever | yes (department/team) | yes / workplace type | when published (rare) | created | yes |
| Ashby | yes | yes / remote flag | often (US companies) | published | yes |
| Workday | **no per job** (summary uses Workday's category counts) | text (`"3 Locations"` for multi-site) | no | approximate (from "Posted 3 Days Ago"; "30+ days" gives null) | no |
| SmartRecruiters | sometimes | yes / remote + hybrid | no | released | no |
| Workable | yes | yes / remote flag | no | published | yes |
| Recruitee | yes | yes / remote + hybrid | when published | published + updated | yes |
| Personio | yes | office | no | created | yes |
| Teamtailor | yes | yes / remote status | no | published | yes |
| BambooHR | yes | yes | no | **no** | no |

### Limits and caveats

- **Detection is not 100%.**
  - Companies with an in-house careers system, or with other ATSs (iCIMS, Taleo, SuccessFactors, Jobvite and others), are reported as errors.
  - When a careers site loads its jobs with JavaScript from an unknown source, detection falls back to the guessed token.
  - A guessed token can belong to a different company with the same name, so check `boardUrl` on `slug-guess` rows. You can always force a board with `ats:token`.
- **Big boards.**
  - Workday lists at most about 2,000 postings per career site.
  - SmartRecruiters and Workday are read 100 or 20 jobs per request.
  - When `maxJobsPerCompany` (default 2,000) or Workday's cap cuts the list, `listComplete` is false and closed-job detection is switched off for that company. New-job detection still works on the jobs that were read.
- **First run.** In monitor mode the first run outputs all current jobs (or nothing with `firstRunBehavior: "baselineOnly"`). `isNew` is false on the first run.
- **Remote flag.** It comes from each ATS's own field, or from "remote" in the location or title. It is `null` when unknown.
- The data is company-level job postings published by the companies themselves. No candidate or employee data is collected.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Actor start | $0.003 per run |
| Company checked (board found and read) | $0.003 per company |
| Job row output | $0.001 per job ($1 per 1,000) |

Summary, closed-job and error rows are free.

Example: monitoring 50 companies daily, with about 2 new jobs per company per day, costs about $0.003 + 50 × $0.003 + 100 × $0.001 = **$0.25 per day**. Use `firstRunBehavior: "baselineOnly"` to avoid paying for the existing backlog on the first run. `maxJobsOutputPerCompany` (default 500) caps job rows per company per run.

### Daily alerts to Slack or email

1. Fill in the input and click **Save as a new task** (one task per client or competitor set is a good pattern).
2. In **Schedules**, create a schedule (for example every day at 08:00 in your time zone) and add the task.
3. In the task's **Integrations** tab, add the **Slack** or **Gmail** integration to get a message when a run finishes,
   or a **webhook** on "Run succeeded" that passes the run to Zapier, Make, n8n or your own endpoint. Those tools can read the rows from
   `https://api.apify.com/v2/datasets/{defaultDatasetId}/items` and format them however you like.

Because monitor mode outputs **only jobs that are new since the previous run (plus free closed-job and summary rows)**, every scheduled run's dataset *is* your alert list. An empty dataset (apart from free summary rows) means nothing changed.
Turn on Apify's run-failure notifications too, so you hear about a failed run instead of silence.

### Related actors

Part of a small **competitor-intelligence suite** by the same developer. Same conventions everywhere: pay per event, failed items are never charged, and the monitors return only what changed since the last run.

- [Google Trends Scraper & API](https://apify.com/ivan-petrus-g/google-trends-api): interest over time, by region/city, top queries and Trending now for any country.
- [Google Ads Transparency Scraper & New Ads Monitor](https://apify.com/ivan-petrus-g/google-ads-transparency-monitor): competitors' Google Search, Display and YouTube ads, with only-new-ads alerts.
- [LinkedIn Ad Library Scraper & New Ads Monitor](https://apify.com/ivan-petrus-g/linkedin-ad-library-monitor): competitors' LinkedIn ads without login, incl. EU impressions and targeting.
- [Bing Ads Library Scraper - Microsoft Ads Monitor (EU)](https://apify.com/ivan-petrus-g/microsoft-ads-library-monitor): Bing ads from Microsoft's official Ad Library (EU/EEA), with impressions by country.
- [App Store & Google Play Scraper](https://apify.com/ivan-petrus-g/app-store-monitor): ratings, installs, versions, chart and keyword ranks of iOS and Android apps, with change rows.

### FAQ

**Can I monitor a company that is not detected?** Paste its job-board URL or `ats:token`. If the ATS is not supported, open an issue with the careers URL.

**Can I get only engineering jobs in Germany?** Yes. Use `titleKeywords` / `departmentKeywords` and `locationKeywords`. The summary still covers all jobs.

**Do I need a proxy?** No. The job-board APIs are public. A proxy option exists in case a careers website blocks page scanning.

# Actor input Schema

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

One company per line. Accepted: a company domain (<code>stripe.com</code>), a careers page URL (<code>https://www.figma.com/careers</code>), a job-board URL (<code>https://jobs.lever.co/palantir</code>, <code>https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite</code>), <code>ats:token</code> (<code>greenhouse:airbnb</code>, <code>ashby:notion</code>, <code>workday:nvidia.wd5/NVIDIAExternalCareerSite</code>) or a plain company name (token is guessed). The job system is detected automatically.

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

Alternative to auto-detection: a JSON list of boards, e.g. <code>\[{"ats": "greenhouse", "boardToken": "airbnb", "companyName": "Airbnb"}, {"ats": "workday", "boardToken": "nvidia", "workdayHost": "wd5", "workdaySite": "NVIDIAExternalCareerSite"}]</code>. ats = greenhouse, lever, ashby, workday, smartrecruiters, workable, recruitee, personio, teamtailor or bamboohr.

## `onlyNewJobs` (type: `boolean`):

On: each run outputs only jobs that were not there in the previous run of the same company (state is kept per company in a named key-value store), plus closed jobs. Off: output all current open jobs every run (closed/new detection still runs).

## `firstRunBehavior` (type: `string`):

What to output the first time a company is checked in monitor mode: all current open jobs, or nothing (just remember them, so you only get truly new jobs from the next run on - cheaper).

## `includeClosedJobs` (type: `boolean`):

Add a row for every job that disappeared from the company's board since the last run. Free - not charged.

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

Only output jobs whose title contains at least one of these words (case-insensitive), e.g. engineer, sales. Filters apply to job and closed-job rows; the summary always covers all jobs.

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

Skip jobs whose title contains any of these words, e.g. intern, senior.

## `departmentKeywords` (type: `array`):

Only jobs whose department or team contains one of these words, e.g. marketing.

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

Only jobs whose location (any of its locations or country) contains one of these words, e.g. Berlin, Germany, Remote.

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

Only output jobs marked as remote by the ATS (or with 'remote' in the location/title).

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

Add the plain-text job description (max 6,000 characters) where the public board provides it in the list (Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Teamtailor). Not available for Workday, SmartRecruiters, BambooHR.

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

Upper limit of jobs read from one board (0 = all). Big boards (Workday, SmartRecruiters) are paginated; when the limit cuts the list, closed-job detection is switched off for that company.

## `maxJobsOutputPerCompany` (type: `integer`):

Cost guard: at most this many (charged) job rows per company per run (0 = no limit). Jobs above the limit are still remembered as seen.

## `includeSummary` (type: `boolean`):

One free row per company: open jobs, new/closed since last run, change vs last run, top departments and locations, remote and salary counts, new departments/locations.

## `guessBoardTokens` (type: `boolean`):

If no job-board link is found on the company's careers pages, try the domain name as board token on each supported ATS (e.g. stripe.com -> greenhouse:stripe). Guesses are marked with detectionMethod = slug-guess.

## `stateStoreName` (type: `string`):

Named key-value store that remembers the jobs per company between runs. Use different names for independent monitors.

## `maxConcurrency` (type: `integer`):

How many companies are checked at the same time.

## `proxyConfiguration` (type: `object`):

Not needed for the official job-board APIs. Enable only if a careers website blocks requests.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "https://jobs.ashbyhq.com/notion",
    "greenhouse:airbnb"
  ],
  "onlyNewJobs": true,
  "firstRunBehavior": "outputAll",
  "includeClosedJobs": true,
  "remoteOnly": false,
  "includeDescription": false,
  "maxJobsPerCompany": 2000,
  "maxJobsOutputPerCompany": 5,
  "includeSummary": true,
  "guessBoardTokens": true,
  "stateStoreName": "company-hiring-monitor-state",
  "maxConcurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

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

No description

## `closed` (type: `string`):

No description

## `runSummary` (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": [
        "huggingface.co",
        "greenhouse:airbnb"
    ],
    "maxJobsOutputPerCompany": 5,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("ivan-petrus-g/company-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": [
        "huggingface.co",
        "greenhouse:airbnb",
    ],
    "maxJobsOutputPerCompany": 5,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("ivan-petrus-g/company-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": [
    "huggingface.co",
    "greenhouse:airbnb"
  ],
  "maxJobsOutputPerCompany": 5,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call ivan-petrus-g/company-hiring-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ivan-petrus-g/company-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/qX6Dp7wMzw3khDYEg/builds/h8TSXPnPolAWNrMvQ/openapi.json
