# ATS Jobs Scraper — Greenhouse, Lever, Ashby & Job Tracker (`devjoi/ats-jobs-scraper`) Actor

Scrape open job postings from company career sites across 6 ATS platforms: Greenhouse, Lever, Ashby, Recruitee, Personio and Breezy. Auto-detects the platform, returns clean normalized JSON, and can output only new postings since the last run. No browser, no login. ~$1.5 per 1,000 results.

- **URL**: https://apify.com/devjoi/ats-jobs-scraper.md
- **Developed by:** [Joinner Medina](https://apify.com/devjoi) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.45 / 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

Scrape open job postings from company career sites across **six ATS platforms** — Greenhouse, Lever, Ashby, Recruitee, Personio and Breezy — with **no browser, no proxies and no login**. Companies publish their openings through public job-board endpoints; this Actor reads them, auto-detects the platform, and turns everything into one clean, normalized dataset you can filter, export and monitor.

Built for recruiters, job boards, lead-generation teams and AI agents that need fresh hiring data, it also works great as a **new-jobs monitor**: run it on a schedule and it will only output postings that appeared since the previous run.

### What does the ATS Jobs Scraper do?

- 🎯 **Auto-detects the ATS platform** from a company slug (`stripe`), a domain (`stripe.com`) or a full career-site URL — no configuration needed. You can also force a platform when you know it.
- 🧩 **Returns one schema for every platform** — 25+ fields per posting (title, locations, workplace type, department, seniority, salary, dates, URLs, descriptions, a stable `global_id` for deduplication), ready for a database, spreadsheet or diff.
- 🔄 **Detects new postings** — with the *Only new jobs* option, each run outputs only jobs you haven't seen before. State persists between runs, so scheduled monitors just work.
- 🔍 **Filters before you pay** — include/exclude title keywords, location keywords and remote-only mode; only matching postings are saved to the dataset.
- 🛡️ **Robust by design** — retries with exponential backoff, per-request timeouts, response size caps, per-company error isolation (one bad board never kills a run) and runtime validation of every record before it reaches your dataset.
- ⚡ **Fast and light** — pure HTTP against single-purpose endpoints: scraping three companies (~960 postings) takes about ten seconds, at a fraction of the cost of browser-based scrapers.

### When should you use this Actor?

- *"I need open job postings from companies that use Greenhouse, Lever or Ashby."* — pass company slugs (`stripe`), domains (`stripe.com`) or career-site URLs and get every live opening back.
- *"I want a daily alert with only the jobs posted since yesterday."* — enable **Only new jobs** and schedule the run; each run returns just the postings that appeared since the previous one.
- *"I'm building a job board or a hiring dataset and need one clean format from many companies."* — every platform is normalized into the same 25+ fields.
- *"I need to filter jobs by title, location or remote before downloading anything."* — filters run before results are saved, so you only pay for what matches.
- *"I want to track hiring trends, salaries or remote-work ratios across companies over time."* — run on a schedule and diff the datasets.

### Why this Actor

- **One schema, six ATS platforms** — Greenhouse, Lever, Ashby, Recruitee, Personio and Breezy, auto-detected from a plain company name or URL. No per-platform configuration.
- **Built-in new-jobs tracking** — persistent state between runs turns it into a monitoring pipeline without any external database.
- **Pay per result** — around **$1.5 per 1,000 job postings**; filtering happens before saving and filtered-out or empty results are not charged.
- **No browser, no proxies, no login** — it reads the public job-board endpoints companies already publish: fast runs at a fraction of the cost of browser-based scrapers.
- **Production-hardened** — retries with exponential backoff, per-request timeouts, response size caps, per-company error isolation and runtime validation of every record.

### Which job platforms are supported?

| Platform | Example career site | Notes |
| --- | --- | --- |
| Greenhouse | `boards.greenhouse.io/stripe` | Description HTML, departments, offices |
| Lever | `jobs.lever.co/spotify` | Workplace type, salary ranges when published |
| Ashby | `jobs.ashbyhq.com/ramp` | Structured compensation included |
| Recruitee | `bunq.recruitee.com` | Full offers API |
| Personio | `personio.jobs.personio.de` | XML board feed |
| Breezy HR | `breezy.breezy.hr` | Full positions feed |

More platforms are on the roadmap (SmartRecruiters, Workable). Need a specific one? Open an issue on the Actor's page.

### How to scrape jobs from company career sites

1. Open the Actor in Apify Console and paste your companies into the **Companies** field — one slug, domain or career-site URL per line. The Actor figures out the rest.
2. *(Optional)* Narrow the output with title, location or remote filters so the dataset contains only what you need.
3. *(Optional)* Enable **Only new jobs** and save the input as a [task](https://docs.apify.com/platform/actors/running/tasks) with a schedule: each run will output only the postings that are new since the last run.
4. Run the Actor, then export the dataset in JSON, CSV, Excel or XML — or pull it through the Apify API.

### How much does it cost to scrape job postings?

The Actor uses **pay-per-result pricing**: you only pay for the job postings saved to the dataset — no monthly fee and no charge for empty results. The exact price per result is always shown in the **Pricing** tab of this page; as a rule of thumb, **1,000 job postings cost around $1.5**, and the Apify free plan includes a monthly usage credit that covers small runs and tests.

Since filtering happens before results are saved, narrow inputs (a few companies, specific keywords) translate directly into lower costs. For a daily new-jobs monitor with change detection, most runs return only a handful of fresh postings.

### Input example

```json
{
  "companies": ["stripe", "jobs.lever.co/spotify", "ramp", "bunq.recruitee.com"],
  "titleKeywords": ["engineer"],
  "excludeTitleKeywords": ["intern"],
  "remoteOnly": false,
  "onlyNewJobs": true,
  "includeDescriptions": true,
  "maxJobsPerCompany": 1000,
  "concurrency": 4
}
```

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `companies` | array of strings | — | Companies to scrape. Slug, domain or career-site URL. **Required.** Max 200 per run. |
| `ats` | string | auto | Force a platform (`greenhouse`, `lever`, `ashby`, `recruitee`, `personio`, `breezy`). |
| `titleKeywords` | array of strings | `[]` | Only jobs whose title contains one of these keywords (case-insensitive). |
| `excludeTitleKeywords` | array of strings | `[]` | Skip jobs whose title contains any of these keywords. |
| `locationKeywords` | array of strings | `[]` | Only jobs whose location contains one of these keywords. |
| `remoteOnly` | boolean | `false` | Output only jobs detected as remote. |
| `onlyNewJobs` | boolean | `false` | Output only jobs not seen in previous runs (change detection). |
| `includeDescriptions` | boolean | `true` | Full descriptions (text + HTML where available). Disable for lighter, faster output. |
| `maxJobsPerCompany` | integer | `1000` | Safety cap per company. |
| `concurrency` | integer | `4` | Companies processed in parallel (1–10). |

### Output example

One dataset record per job posting (trimmed example):

```json
{
  "ats": "greenhouse",
  "company": "stripe",
  "company_name": "Stripe",
  "title": "Abuse Investigator",
  "location": "Dublin",
  "locations": ["Ireland Locations", "Dublin"],
  "remote": false,
  "remote_type": "unknown",
  "department": "8611 Security Analytics",
  "seniority": "unknown",
  "salary_text": null,
  "salary_min": null,
  "salary_max": null,
  "salary_currency": null,
  "salary_interval": null,
  "posted_at": "2026-09-03T17:30:34.000Z",
  "url": "https://stripe.com/jobs/search?gh_jid=8172487",
  "job_id": "8172487",
  "description_text": "…",
  "global_id": "greenhouse:stripe:8172487",
  "scraped_at": "2026-10-08T00:00:04.342Z"
}
```

| Field | Notes |
| --- | --- |
| `global_id` | Stable unique id: `{ats}:{company}:{job_id}`. Use it as your dedupe key across runs and platforms. |
| `remote_type` | `remote` / `hybrid` / `onsite` / `unknown` (derived from provider fields, then location, then description). |
| `seniority` | Heuristic: `intern` / `entry` / `senior` / `lead` / `manager` / `executive` / `unknown`. |
| `salary_*` | Best effort: structured provider compensation when available, otherwise parsed from the salary text; left `null` when nothing reliable is found. |
| `posted_at` / `updated_at` | ISO-8601 UTC when the provider exposes them. |

### Use cases

- **Job boards & aggregators** — build a normalized feed of company-direct postings (no third-party job-board noise).
- **Recruiting & sourcing** — find fresh openings at target companies filtered by title, location and seniority.
- **Job alerts & monitoring** — schedule daily runs with *Only new jobs* and pipe new postings to Slack, email or your CRM.
- **Market research** — track hiring trends, salary data and remote-work ratios across companies over time.
- **AI agents** — feed fresh, structured hiring data into agent workflows via the Apify API or MCP server.

### Integrations and automation

- **API** — start runs and pull datasets via the [Apify API](https://docs.apify.com/api/v2) or the official [JavaScript](https://docs.apify.com/api/client/js) / [Python](https://docs.apify.com/api/client/python) clients:

```bash
curl -X POST "https://api.apify.com/v2/acts/devjoi~ats-jobs-scraper/runs?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"companies": ["stripe", "ramp"], "maxJobsPerCompany": 50}'
```

- **Schedules** — run hourly, daily or weekly; combined with *Only new jobs* that's a complete monitoring pipeline.
- **No-code** — connect through Zapier, Make, n8n or the Apify integrations catalog.
- **AI agents** — the Actor is described by full input/output schemas, so LLM agents can discover and chain it (Apify MCP server supported).
- **Exports** — JSON, CSV, Excel, XML and direct API access; data retention is handled by the platform.

### Limitations and honest scope

- Only the six supported ATS platforms are covered. Companies on other platforms are reported as unresolvable in the run summary and skipped. SmartRecruiters and Workable are on the roadmap.
- Salary fields are best-effort: structured provider data when published, otherwise parsed from salary text — left `null` when nothing reliable is found. `seniority` is derived heuristically from the job title.
- The Actor reads only what each company publishes publicly on its job board. No login walls, no applicant or personal data.
- One run processes up to 200 companies; use several runs or schedules for larger lists.

### FAQ

#### Do I need proxies, a browser or a login?

No. The Actor talks to the public job-board endpoints that companies already expose (the same feeds that power their career pages). No browser, no proxies and no accounts — which keeps runs fast and cheap.

#### How do I get only new job postings?

Enable **Only new jobs**. The Actor remembers every posting it has seen (in a persistent state store) and each run outputs only postings that appear for the first time. Combine it with a schedule for automated new-jobs alerts.

#### Can I filter by location, title or remote?

Yes — use `titleKeywords`, `excludeTitleKeywords`, `locationKeywords` and `remoteOnly`. Filtering happens before results are saved, so you don't pay for postings you don't need.

#### Which companies can I scrape?

Any company using Greenhouse, Lever, Ashby, Recruitee, Personio or Breezy for their careers site — that includes thousands of tech and non-tech companies. If you pass a company that can't be resolved, the run continues with the rest and reports the failure in the summary.

#### The run reports a company couldn't be resolved. What should I check?

Most of the time it's the slug: use the exact career-site address (e.g. `jobs.lever.co/acme` → `acme`). If the company does use a supported platform, try forcing it with the **Force ATS platform** option.

#### How fresh is the data?

Completely fresh: every run queries the companies' live career sites at that moment. Nothing is cached between runs except the change-detection state (a list of already-seen posting ids).

#### Is it legal to scrape these career pages?

The Actor only reads job-board endpoints that companies publish for public consumption, and it identifies itself normally. As with any scraping, you are responsible for how you use the data and for complying with the sites' terms — keep request volumes reasonable (defaults are already conservative).

#### Something went wrong — where do I look?

Open the run in Apify Console and check the **SUMMARY** record in the key-value store: it lists per-company results, error messages and statistics. Most issues are typos in company slugs. If a board is temporarily down, the run retries automatically and reports the company as failed without affecting the others.

### Development

```bash
npm install
npm run local            # runs against ./storage (INPUT.json is created on first run)
npm run local -- --fresh # clean slate (also clears change-detection state)
npm test                 # offline suite: unit + real-payload fixtures + e2e
npm run test:live        # live integration tests against real career sites
```

Local runs emulate the platform storage layout (`storage/datasets/default/`, `storage/key_value_stores/default/`). The offline test suite runs against captured real API payloads in `tests/fixtures/`, and the opt-in live suite hits real endpoints — so upstream API changes are caught early.

# Changelog

This Actor's version history is a separate document: https://apify.com/devjoi/ats-jobs-scraper/changelog.md

# Actor input Schema

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

Companies to scrape open jobs from. Each item can be a slug ("stripe"), a domain ("stripe.com") or a full career-site URL. The ATS platform is detected automatically. Maximum 200 companies per run.

## `ats` (type: `string`):

Optional: force a specific ATS platform for every company (skips auto-detection). Leave empty for automatic detection.

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

Only output jobs whose title contains at least one of these keywords (case-insensitive). Empty = no filter.

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

Skip jobs whose title contains any of these keywords (case-insensitive).

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

Only output jobs whose location contains at least one of these keywords (case-insensitive). Empty = no filter.

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

Only output jobs detected as remote.

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

Output only jobs that were not seen in previous runs of this actor (change detection). Ideal for job alerts and scheduled monitoring.

## `includeDescriptions` (type: `boolean`):

Include full job description (plain text + HTML where available). Disable for a lighter, faster output.

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

Safety cap on the number of jobs pulled per company.

## `concurrency` (type: `integer`):

How many companies are processed in parallel (1-10).

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "shopify",
    "ramp",
    "bunq.recruitee.com"
  ],
  "ats": "",
  "titleKeywords": [
    "engineer",
    "designer"
  ],
  "excludeTitleKeywords": [
    "intern"
  ],
  "locationKeywords": [
    "london",
    "berlin"
  ],
  "remoteOnly": false,
  "onlyNewJobs": false,
  "includeDescriptions": true,
  "maxJobsPerCompany": 1000,
  "concurrency": 4
}
```

# Actor output Schema

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

All scraped job postings as dataset items (JSON, CSV, Excel or XML via the API).

# 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",
        "spotify",
        "ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("devjoi/ats-jobs-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": [
        "stripe",
        "spotify",
        "ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("devjoi/ats-jobs-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": [
    "stripe",
    "spotify",
    "ramp"
  ]
}' |
apify call devjoi/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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