# ATS Job Scraper - Greenhouse, Lever & Ashby Job Monitor (`feedharbor/ats-hiring-signal-scraper`) Actor

Scrape Greenhouse, Lever & Ashby job boards for your companies and get only new job postings — a cross-ATS hiring-signal feed for sales, recruiting & intel.

- **URL**: https://apify.com/feedharbor/ats-hiring-signal-scraper.md
- **Developed by:** [FeedHarbor](https://apify.com/feedharbor) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 job results

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## ATS Job Scraper — Greenhouse, Lever & Ashby New-Requisition Monitor

Scrape jobs from Greenhouse, Lever, and Ashby job boards for any list of companies, and detect **newly-posted requisitions** — the companies that *just started hiring*. Give it your target accounts, run it on a schedule, and get a clean, deduplicated, normalized feed of new, changed, and removed job openings across all three ATS platforms at once.

Unlike a one-off job dump, this actor remembers what it has seen before. Every run compares against the last one and tells you exactly what is **new this week** — the hiring signal that sales teams, recruiters, and market analysts actually pay for.

### What this actor does

- **Scrape Greenhouse job boards** by board token (e.g. `greenhouse:gitlab`)
- **Scrape Lever postings** by company (e.g. `lever:leverdemo`)
- **Scrape Ashby job boards** by org (e.g. `ashby:openai`)
- **Detect new job postings** across a list of companies since your last run
- **Monitor when companies start hiring** for specific roles (sales, engineering, marketing…)
- **Track removed / filled requisitions** so you know when a role closes
- **Normalize job data** from three different ATS platforms into one clean schema
- **Parse seniority** from job titles (intern → C-level) for decision-maker targeting
- **Normalize salary / compensation** into numeric min/max where the board exposes it
- **Deduplicate jobs** across runs with a stable, cross-ATS job ID

### Who it's for

**Sales & RevOps teams** running hiring-intent prospecting. A company posting a "Marketo Administrator" or "VP of Sales" role is a timed buying signal. Monitor your target account list and get alerted the moment a relevant requisition appears.

**Recruiting & staffing agencies** who need a live who's-hiring feed. Watch the companies you place into and know within a day when they open a new seat.

**Market & competitive intelligence** analysts tracking headcount growth by team as a proxy for company expansion, funding, and strategy.

**Founders & investors** monitoring a portfolio or a competitive set for hiring momentum.

### How it works

1. Provide a list of companies as `greenhouse:token`, `lever:company`, `ashby:org`, or full board URLs (the ATS is auto-detected).
2. The actor fetches every board through its official public API — no login, no proxies required.
3. Each posting is normalized into a single unified schema and assigned a stable `job_uid`.
4. The run is compared against previous runs stored under a `trackerName`, computing `change_type` (`new`, `updated`, `unchanged`, `removed`) and `first_seen_at`.
5. With **Monitor mode** on, you receive only the rows that changed — your new-requisition signal.

Run it on an Apify schedule (say, daily) with the same `trackerName` to build a continuous hiring-signal feed for your accounts.

### Input

| Field | What it does |
|---|---|
| `companies` | Target boards: `greenhouse:token`, `lever:company`, `ashby:org`, or a board URL |
| `presetList` | Use a curated set (AI labs, dev tools, fintech) instead of your own list |
| `monitorMode` | Emit only new / updated / removed rows since the last run |
| `trackerName` | State bucket to diff against — one per monitored account set |
| `titleKeywords` / `excludeKeywords` | Keep or drop jobs by title |
| `seniorityFilter` | Keep only e.g. `director`, `vp`, `c_level` |
| `remoteOnly`, `locationKeywords`, `employmentTypes`, `postedSince` | Standard filters |
| `includeDescription`, `includeCompensation` | Add full text / pay data |
| `outputProfile` | `full`, `compact`, or `signals_only` |

Minimal example:

```json
{
  "companies": ["greenhouse:gitlab", "lever:leverdemo", "ashby:openai"],
  "monitorMode": true,
  "trackerName": "my-target-accounts",
  "seniorityFilter": ["director", "vp", "c_level"]
}
```

### Output

Each job is a normalized record. Key fields:

```json
{
  "job_uid": "b1c2…",
  "ats": "greenhouse",
  "company_name": "Gitlab",
  "title": "Senior Backend Engineer",
  "seniority": "senior",
  "department": "Engineering",
  "location_primary": "Remote, US",
  "remote": true,
  "workplace_type": "remote",
  "salary_min": 120000,
  "salary_max": 160000,
  "currency": "USD",
  "posted_at": "2026-07-30T09:00:00.000Z",
  "first_seen_at": "2026-08-04T00:00:00.000Z",
  "change_type": "new",
  "is_new_requisition": true,
  "days_open": 5,
  "url": "https://boards.greenhouse.io/gitlab/jobs/12345"
}
```

The `change_type`, `is_new_requisition`, and `first_seen_at` fields are the hiring-signal core — they only exist because the actor accumulates state across runs.

### Pricing

Pay-per-result. A full scrape is billed per job returned; **new & changed rows** (Monitor mode) are billed at the signal rate, because that is the output worth paying for. Start with a small free allowance so you can validate the data before committing. See the pricing tab for current rates.

### FAQ

**Do I need API keys or proxies?**
No. Greenhouse, Lever, and Ashby all expose public job-board JSON APIs. No login, no proxies.

**How do I find a company's board token?**
It's the slug in their careers URL — `boards.greenhouse.io/**gitlab**`, `jobs.lever.co/**company**`, `jobs.ashbyhq.com/**org**`. Paste the full URL and the actor detects the ATS.

**How does "new this week" work on the first run?**
The first run has no history, so everything is `new`. From the second run onward (same `trackerName`), only genuinely new/changed roles are flagged. For instant history, run it a few times before you rely on the signal, or keep a daily schedule.

**Does it handle companies switching or renaming their ATS?**
Yes — failed or moved boards are reported in the run summary, and removed requisitions are surfaced as `change_type: removed`.

**Is scraping these boards allowed?**
These are public job-board APIs designed to be indexed, and the actor only reads public company job data — not personal contact information.

***

#### For developers

The scraping, normalization, and change-detection logic lives in a platform-agnostic core (`src/core`) with the Apify actor as a thin adapter (`src/adapters/apify.js`). State (the first-seen ledger) is stored in your own Postgres when `DATABASE_URL` is set, or Apify's key-value store as a fallback. Run the same core locally with `node cli.js greenhouse:gitlab --monitor`. Tests: `node --test test/smoke.test.js`.

# Actor input Schema

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

Target boards. Use "greenhouse:token", "lever:company", "ashby:org", or a full board URL (ATS auto-detected). Example: greenhouse:gitlab

## `presetList` (type: `string`):

Use a curated target list instead of (or in addition to) your own companies.

## `atsFilter` (type: `array`):

Only fetch boards on these ATS platforms.

## `monitorMode` (type: `boolean`):

Emit only rows whose change\_type is new, updated, or removed since the last run with the same tracker. This is the hiring-signal / who-just-started-hiring output.

## `trackerName` (type: `string`):

State bucket used to diff this run against previous runs. Use one tracker per monitored account set.

## `detectRemoved` (type: `boolean`):

Emit a change\_type=removed row when a previously-seen job disappears (a role was filled or closed).

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

Keep only jobs whose title contains one of these.

## `excludeKeywords` (type: `array`):

Drop jobs whose title contains any of these.

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

Keep only jobs whose location contains one of these.

## `seniorityFilter` (type: `array`):

Keep only these seniority levels (intern, junior, mid, senior, staff, principal, lead, manager, director, vp, c\_level).

## `employmentTypes` (type: `array`):

Keep only these employment types, e.g. full\_time, part\_time, contract, internship.

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

Keep only roles flagged as remote.

## `postedSince` (type: `string`):

ISO date (2026-07-01) or relative ("7d", "30d"). Only applies where the source exposes a post date.

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

Fetch and include the full job description text (larger output, slower).

## `includeCompensation` (type: `boolean`):

Include compensation fields where the board exposes them.

## `parseSeniority` (type: `boolean`):

Parse a seniority level from each job title.

## `normalizeSalary` (type: `boolean`):

Normalize compensation into numeric min/max fields where possible.

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

Maximum number of jobs to return per company board.

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

How many company boards to fetch in parallel.

## `outputProfile` (type: `string`):

How much detail to include per row: full, compact, or signals\_only.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:gitlab",
    "lever:leverdemo",
    "ashby:openai"
  ],
  "atsFilter": [],
  "monitorMode": false,
  "trackerName": "default",
  "detectRemoved": true,
  "titleKeywords": [],
  "excludeKeywords": [],
  "locationKeywords": [],
  "seniorityFilter": [],
  "employmentTypes": [],
  "remoteOnly": false,
  "includeDescription": false,
  "includeCompensation": true,
  "parseSeniority": true,
  "normalizeSalary": true,
  "maxJobsPerCompany": 1000,
  "maxConcurrency": 5,
  "outputProfile": "full"
}
```

# Actor output Schema

## `results` (type: `string`):

Normalized job requisitions in the default dataset.

# 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": [
        "greenhouse:gitlab",
        "lever:leverdemo",
        "ashby:openai"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("feedharbor/ats-hiring-signal-scraper").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": [
        "greenhouse:gitlab",
        "lever:leverdemo",
        "ashby:openai",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("feedharbor/ats-hiring-signal-scraper").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": [
    "greenhouse:gitlab",
    "lever:leverdemo",
    "ashby:openai"
  ]
}' |
apify call feedharbor/ats-hiring-signal-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,feedharbor/ats-hiring-signal-scraper"
        }
    }
}

```

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/0x1KaeZ8JPLciVUL7/builds/qiq8oaFrDJsP58xAK/openapi.json
