# ATS Jobs Aggregator — Greenhouse, Lever & Ashby in One Dataset (`daj8/ats-jobs-aggregator`) Actor

Aggregate job postings from Greenhouse, Lever and Ashby ATS job boards into ONE normalized, deduplicated dataset. No login, no API key, no proxies needed — all three ATS expose public boards.

- **URL**: https://apify.com/daj8/ats-jobs-aggregator.md
- **Developed by:** [Donglin Li](https://apify.com/daj8) (community)
- **Categories:** Jobs, E-commerce, Developer tools
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 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?

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 Aggregator — Greenhouse, Lever & Ashby Jobs Scraper & API in One Dataset

> **Keywords**: greenhouse jobs scraper · lever jobs API · ashby job board · ATS job listings · job postings API · hiring data · salary data · incremental job monitoring

Get job postings from **Greenhouse, Lever and Ashby** ATS job boards as **one clean, deduplicated dataset** — no login, no API key, no proxy setup.

If you track hiring signals, build lead lists, or monitor competitors' headcount, you already know the pain: every company uses a different ATS, every API returns a different shape, and you end up writing three scrapers and a merge script before you get a single usable table.

This Actor does that for you.

### What you get

One row per job posting, normalized across all three ATS platforms:

| Field | Example |
|---|---|
| `ats` | `greenhouse` |
| `company` | `Anthropic` |
| `title` | `Account Executive, AI Native` |
| `location` | `New York City, NY; San Francisco, CA` |
| `department` | `Sales` |
| `is_remote` | `true` |
| `published_at` | `2026-10-19T11:17:24-04:00` |
| `updated_at` | `2026-11-08T09:12:00-05:00` |
| `url` | `https://job-boards.greenhouse.io/anthropic/jobs/5456619008` |
| `salary` | `$180,000 - $240,000` *(when the ATS exposes it)* |

Download as **JSON, CSV, or Excel** straight from the dataset tab.

### Sample output

```json
{
  "ats": "greenhouse",
  "company": "Anthropic",
  "job_id": "5456619008",
  "title": "3P Revenue Accounting & Reporting",
  "location": "San Francisco, CA | Seattle, WA",
  "department": "Finance",
  "is_remote": null,
  "published_at": "2026-07-14T18:22:11-04:00",
  "url": "https://job-boards.greenhouse.io/anthropic/jobs/5456619008",
  "salary": "$150,000 - $200,000"
}
```

### How to use it

1. **`companies`** — the company slugs from their careers page URL:
   - `boards.greenhouse.io/stripe` → `stripe`
   - `jobs.lever.co/spotify` → `spotify`
   - `jobs.ashbyhq.com/notion` → `notion`
2. **`ats`** — pick which platforms to query (default: all three)
3. **`sinceDays`** — optional. Set to `1` for a daily scheduled run and **only pay for new or changed postings**.

That last one matters: schedule this Actor every morning with `sinceDays: 1` and you get a cheap daily diff of every company you track, instead of re-pulling everything.

### Why this instead of three separate scrapers

- **Cross-ATS deduplication built in.** Companies migrate between ATS platforms and some run two at once. Duplicate postings are collapsed by company + title + location, so your counts stay honest.
- **Normalized schema.** Greenhouse returns `id` as an integer and location as `location.name`; Lever returns a UUID and a nested `categories` object; Ashby returns yet another shape. You get one schema.
- **No authentication.** All three ATS expose public job boards. Nothing to configure, nothing to rotate.
- **Failures are visible.** Per-company/per-ATS errors are reported rather than silently dropping rows, so you know when a slug is wrong.

### Why teams pick this over full-dataset vendors

- **Real salary numbers**: we regex-extract published ranges from job description bodies — 37.8% coverage vs **0%** on the largest competitor's AI-enhanced fields (measured same-day).
- **Incremental monitoring**: `sinceDays` + cross-ATS fingerprint dedup means you can schedule daily runs and get only *new or changed* postings — "new-job alerts for my tracked companies", not a full re-download.
- **Honest coverage numbers**: we publish measured field coverage instead of claiming completeness.

### Typical use cases

- **Hiring signal / growth tracking** — count open roles per company per week to spot expansion or a freeze
- **Lead generation** — hiring for a specific stack often means budget; filter by department and stack keywords
- **Competitive research** — see which teams a competitor is staffing up
- **Job board aggregation** — feed your own board with fresh, deduplicated postings

### Pricing

Pay per job posting returned. Duplicates and out-of-range postings (when using `sinceDays`) are not billed.

### Field coverage (measured on 3,453 postings from 9 companies, Nov 2026 — refreshed daily)

We publish real numbers rather than claiming "complete data":

| Field | Filled | Notes |
|---|---|---|
| `ats`, `company`, `job_id`, `title`, `location`, `url` | **100%** | always present |
| `department` | **100%** | from ATS taxonomy, falls back to company metadata |
| `published_at`, `updated_at` | **99.9%** | drives incremental `sinceDays` mode |
| `is_remote` | **~49%** | inferred from ATS field, title, or location text |
| `salary` | **~38%** | extracted from job description text where the employer publishes a range (e.g. Anthropic: 77% of postings) |
| `employment_type` | **96.5%** | normalized to a fixed enum (Full-time / Internship / Contract / Part-time / Fellowship / Apprenticeship) via two-stage inference |

**Employment type** is normalized rather than passed through, because each ATS uses different labels (`FullTime`, `Full-time`, `Permanent`, `Intern`, `Internship`). Two-stage inference: special types (Intern / Contract / Part-time) are detected from title and description text first; standard roles (engineer, manager, analyst, director…) default to `Full-time`. You get one consistent enum you can filter on.

Salary is genuinely hard: most ATS boards simply don't have a salary field, so this Actor scans the job description body for phrases like "salary range is $X–$Y" (with `$`, `USD`, `k`, `per year/hour` variants). Where a company publishes pay ranges, coverage is much higher — e.g. **Anthropic: 77% of postings carry a salary range**.

If a field matters to your workflow and you need higher coverage, open an issue and tell us which companies you track.

### Notes and limits

- Some companies use more than one ATS but not all three — an ATS returning 404 for a slug is expected, not an error (18 of 27 company/ATS combinations in our test).
- Very large boards (e.g. Anthropic with full descriptions, ~10 MB) are read in streamed chunks; the Actor degrades gracefully rather than failing outright.
- Respect each platform's terms and your local regulations when using this data.

### Support

Something broken or a company slug missing? Open an issue on the Actor page — response time is tracked publicly, and legitimate data-source changes get fixed fast.

# Actor input Schema

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

Company slugs from their careers page URL. Find them from boards.greenhouse.io/<slug>, jobs.lever.co/<slug> or jobs.ashbyhq.com/<slug>.

## `ats` (type: `array`):

Which ATS job boards to query. The same company may exist on more than one; duplicates are removed automatically.

## `sinceDays` (type: `integer`):

Incremental mode: return only postings updated in the last N days. Leave empty for everything. Schedule daily runs with 1 to pay only for new or changed postings.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "anthropic",
    "notion"
  ],
  "ats": [
    "greenhouse",
    "lever",
    "ashby"
  ]
}
```

# Actor output Schema

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

All job postings from the requested companies across Greenhouse, Lever and Ashby, normalized to one schema and deduplicated.

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

Counts, per-ATS breakdown and any per-company errors encountered during the run.

# 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": [
        "stripe",
        "anthropic",
        "notion"
    ],
    "ats": [
        "greenhouse",
        "lever",
        "ashby"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("daj8/ats-jobs-aggregator").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": [
        "stripe",
        "anthropic",
        "notion",
    ],
    "ats": [
        "greenhouse",
        "lever",
        "ashby",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("daj8/ats-jobs-aggregator").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": [
    "stripe",
    "anthropic",
    "notion"
  ],
  "ats": [
    "greenhouse",
    "lever",
    "ashby"
  ]
}' |
apify call daj8/ats-jobs-aggregator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,daj8/ats-jobs-aggregator"
        }
    }
}
```

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/AFornbdnDMLqZWfXm/builds/AbHVauYkyww4PKWDY/openapi.json
