# ATS Jobs Scraper: Greenhouse, Lever, Ashby & more (`pinkish_armada/ats-jobs-scraper`) Actor

Scrape and monitor jobs straight from employers' Greenhouse, Lever, Ashby, SmartRecruiters and Workable boards via their public APIs. Auto-detects the ATS. Filters, salary, normalised output, new-jobs-only mode. No LinkedIn.

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

## Pricing

from $2.50 / 1,000 job saveds

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: Greenhouse, Lever, Ashby, SmartRecruiters & Workable jobs API

**Scrape and monitor job postings directly from employers' own applicant tracking systems (ATS).** Give it company names, careers page URLs or job-board links. It auto-detects whether each company uses **Greenhouse, Lever, Ashby, SmartRecruiters or Workable**, pulls every open job from the platform's **official public job-board API**, and returns one clean, normalised dataset: title, company, location, remote/hybrid, department, employment type, **salary**, dates, apply URL, and the full description as text and HTML.

Run it on a schedule with **"New jobs only" monitoring** and you get only the jobs that appeared (or changed, or closed) since the last run. That's ideal for job alerts, hiring-signal sales tools, recruiting pipelines, job boards and labour-market research.

**No LinkedIn, no Indeed, no logins, no proxies.** Data comes straight from the employer's job board, through endpoints the ATS vendors publish for exactly this purpose.

### Why use this job scraper?

- ✅ **5 ATS platforms in one Actor.** Greenhouse jobs, Lever jobs, Ashby jobs, SmartRecruiters jobs and Workable jobs, in one schema.
- 🔎 **Automatic ATS detection.** Type `Stripe`, paste `https://www.notion.so/careers` or `stripe.com`, and the Actor finds the right board (careers pages are scanned for embedded Greenhouse/Lever/Ashby/SmartRecruiters/Workable widgets). No need to know board tokens.
- 🧹 **Normalised output.** The same fields for every platform: `employmentType` mapped to `full-time / part-time / contract / internship / temporary`, `workplaceType` to `remote / hybrid / onsite`, ISO dates, country names and codes, and structured salary (`salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval`).
- 💰 **Salary when published.** Ashby compensation, Lever salary ranges and Greenhouse pay-transparency ranges, plus a careful fallback that reads pay ranges from the description (marked `salarySource: "description"`).
- 🔔 **Job monitoring.** "New only" or "new + changed" mode, with optional "removed" rows for closed jobs. State lives in a named key-value store in your account, so schedules just work.
- 🎯 **Filters:** keywords (title or description, regex supported), excluded words, locations, remote/hybrid/on-site, departments, employment types, posted within N days, max jobs per company.
- 🛡️ **Robust:** retries with backoff, `Retry-After` support, per-platform rate limits, dedupe across inputs, and a per-input run summary that says exactly what was found and what wasn't.
- 💸 **Cheap and predictable:** **$2.50 per 1,000 jobs**. In monitoring mode you only pay for new or changed jobs.

### What can I use it for?

| Use case | How |
|---|---|
| **Job alerts / job monitoring** | Schedule daily or hourly with `monitorMode: "new"`, then send new rows to Slack, email or a webhook via Apify integrations |
| **Hiring signals for B2B sales** | Watch target accounts for roles like "data engineer" or "RevOps"; a new role means a buying signal |
| **Niche job boards & aggregators** | Pull jobs from hundreds of companies' ATS boards into your site, with apply URLs pointing back to the employer |
| **Recruiting & sourcing** | Track competitors' open roles, teams and locations |
| **Labour-market & salary research** | Structured salary ranges, departments and locations across companies |
| **AI agents / RAG** | Clean text descriptions ready for LLMs, embeddings and matching |

### How to use it

1. Click **Try for free**.
2. In **Companies, careers pages or job boards**, add one entry per line:
   - an ATS board URL: `https://job-boards.greenhouse.io/airbnb`, `https://jobs.lever.co/palantir`, `https://jobs.ashbyhq.com/ramp`, `https://jobs.smartrecruiters.com/BoschGroup`, `https://apply.workable.com/huggingface`
   - or `platform:token`: `greenhouse:airbnb`, `lever:palantir`, `ashby:ramp`, `smartrecruiters:BoschGroup`, `workable:huggingface` (`lever-eu:` for Lever's EU region)
   - or a careers page / website: `https://www.notion.so/careers`, `stripe.com`
   - or just a company name: `Figma`, `Spotify`, `Bosch Group`
3. Optionally set filters (keywords, locations, remote, departments, employment types, posted within days).
4. Click **Start**. Download the jobs as JSON, CSV, Excel or XML, or read them via the API.
5. For monitoring: set **Monitoring mode** to *New jobs only* and add a **Schedule** (e.g. daily at 08:00).

#### Input example

```json
{
  "companies": [
    "https://jobs.ashbyhq.com/ramp",
    "greenhouse:airbnb",
    "https://www.notion.so/careers",
    "Palantir",
    "Bosch Group"
  ],
  "keywords": ["engineer", "/data (scientist|analyst)/"],
  "excludeKeywords": ["intern"],
  "locations": ["London", "Remote", "AU"],
  "remote": "any",
  "postedWithinDays": 30,
  "monitorMode": "new",
  "monitorId": "my-daily-alert",
  "descriptionFormat": "text"
}
```

#### Output example

One row per job (description shortened):

```json
{
  "id": "ashby:ramp:465799ed-c58b-4c6a-a290-2e95b52ef0bb",
  "platform": "ashby",
  "boardToken": "ramp",
  "jobId": "465799ed-c58b-4c6a-a290-2e95b52ef0bb",
  "title": "Tech Lead and Manager (TLM), Production Engineering",
  "company": "Ramp",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "San Francisco, CA", "Remote (US)"],
  "city": "New York City",
  "region": "NY",
  "country": "United States",
  "countryCode": "US",
  "isRemote": false,
  "workplaceType": "hybrid",
  "department": "Engineering",
  "team": "Engineering",
  "employmentType": "full-time",
  "employmentTypeRaw": "FullTime",
  "salaryMin": 240000,
  "salaryMax": 330000,
  "salaryCurrency": "USD",
  "salaryInterval": "year",
  "salaryText": "$240K - $330K",
  "salarySource": "ats",
  "postedAt": "2026-09-28T23:10:15.801Z",
  "updatedAt": null,
  "jobUrl": "https://jobs.ashbyhq.com/ramp/465799ed-c58b-4c6a-a290-2e95b52ef0bb",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/465799ed-c58b-4c6a-a290-2e95b52ef0bb/application",
  "requisitionId": null,
  "descriptionText": "ABOUT RAMP\n\nRamp is building the smart infrastructure for finance teams…",
  "descriptionHtml": "<h1><strong>About Ramp</strong></h1><p>Ramp is building…",
  "changeType": "new",
  "firstSeenAt": "2026-10-07T00:00:12.000Z",
  "input": "https://jobs.ashbyhq.com/ramp",
  "scrapedAt": "2026-10-07T00:00:12.000Z"
}
```

The dataset has ready-made views: **Jobs** (overview table), **Jobs with salary**, **Changes (monitoring)** and **All fields**.

#### Run summary (key-value store → `OUTPUT`)

For every input you get the detected platform and board, how it was detected (`input`, `website`, `name-search`, `domain-name-guess`), a `confidence`, the number of jobs on the board / matching your filters / saved, new / changed / removed counts, other candidate boards, and a clear error for anything not found (e.g. *"No Greenhouse/Lever/Ashby/SmartRecruiters/Workable board found… The company may use another ATS (e.g. Workday)"*).

### Output fields

| Field | Description |
|---|---|
| `id` | Stable unique ID `platform:board:jobId` (use it to dedupe across runs) |
| `platform`, `boardToken`, `jobId` | Source ATS, board slug and the platform's job ID |
| `title`, `company` | Job title and company name |
| `location`, `locations` | Primary location and all listed locations |
| `city`, `region`, `country`, `countryCode` | Structured location where the ATS provides it (country also inferred from location text) |
| `isRemote`, `workplaceType` | `true/false/null` and `remote / hybrid / onsite / null` |
| `department`, `team` | Department and team / function |
| `employmentType`, `employmentTypeRaw` | Normalised type and the original value |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryInterval`, `salaryText`, `salarySource` | Pay range when published (`ats`) or found in the description (`description`) |
| `postedAt`, `updatedAt` | First published / last updated (ISO 8601) |
| `jobUrl`, `applyUrl` | Job page and application URL on the employer's board |
| `requisitionId` | Internal requisition / reference number when public |
| `descriptionText`, `descriptionHtml` | Full description as clean text and original HTML |
| `changeType`, `firstSeenAt` | Monitoring only: `new`, `changed` or `removed`, and when this job was first seen |
| `input`, `scrapedAt` | Which input produced the row, and when |

### How much does it cost?

**Pay per event: $2.50 per 1,000 jobs saved** ($0.0025 per job), plus Apify's tiny per-run start fee.

- 100 jobs ≈ **$0.25**; 10,000 jobs ≈ **$25**.
- **Monitoring mode:** you only pay for jobs returned (new or changed). Unchanged jobs are free, and so are "removed" rows.
- Boards that fail, inputs that aren't found, and jobs excluded by your filters are **not charged**.
- Set **Max jobs in total** or the run's **maximum cost** to cap spending. The Actor stops cleanly at the limit and remembers unsaved new jobs for the next monitored run.

### Platforms and data sources

| Platform | Public endpoint used | Notes |
|---|---|---|
| Greenhouse | Job Board API `boards-api.greenhouse.io/v1/boards/{token}/jobs` | Docs: "Job Board data is publicly available, so authentication is not required for any GET endpoints." Pay ranges from the per-job pay-transparency field |
| Lever | Postings API `api.lever.co/v0/postings/{site}` (and `api.eu.lever.co`) | Public postings API documented by Lever on GitHub |
| Ashby | Public Job Posting API `api.ashbyhq.com/posting-api/job-board/{name}` | Includes compensation when the company publishes it |
| SmartRecruiters | Posting API `api.smartrecruiters.com/v1/companies/{id}/postings` | Public, no authentication. Description via the posting-details endpoint |
| Workable | Public published-jobs endpoint `www.workable.com/api/accounts/{subdomain}` | Documented in Workable Help; fetched gently (one request at a time) |

Not supported on purpose: **Workday** and other ATSs without a documented public jobs API, and LinkedIn/Indeed/Glassdoor.

### FAQ

**Is it legal to scrape these job boards?**
This Actor only calls the job-board endpoints that Greenhouse, Lever, Ashby, SmartRecruiters and Workable publish so that employers' jobs can be shown on careers sites and job boards. It doesn't log in, bypass anything, or collect personal data about candidates. When it scans a company website to detect the ATS, it respects robots.txt. Job descriptions remain the employer's content; if you republish them, link back via `jobUrl`/`applyUrl` and follow the laws that apply to you.

**How does company-name detection work?**
It turns the name into likely board slugs (e.g. `Hugging Face` → `huggingface`, `hugging-face`) and asks each platform's public API whether that board exists. When the platform reports a company name (Greenhouse, SmartRecruiters, Workable), it's compared with yours (`confidence: high`). Lever and Ashby don't publish names (`confidence: medium`). For 100% accuracy, use the careers page URL or the board URL.

**A company isn't found. Why?**
It probably uses another ATS (e.g. Workday, iCIMS, Taleo, SuccessFactors) or a custom careers site. The run summary lists what was checked. If you know the board URL, paste it directly.

**How does monitoring work with filters?**
State is tracked per board (every open job, regardless of filters), so a job counts as "new" when it first appears on the board. If you later widen your filters, older jobs aren't re-sent; use a new **Monitor ID** to start fresh.

**Can I monitor hundreds of companies?**
Yes. Add them all (up to 2,000 inputs per run) and schedule the run; boards are fetched in parallel within each platform's rate limits. For very large lists, use board URLs or `platform:token` inputs to skip detection.

**Can I use it from my code, Make, Zapier, n8n or an AI agent?**
Yes. Use the Apify API/SDKs, the official integrations, or the Apify MCP server. Each run returns a dataset you can fetch as JSON/CSV.

### Other tools from the same developer

- [Sitemap URL Extractor](https://apify.com/pinkish_armada/sitemap-url-extractor)
- [Broken Link Checker](https://apify.com/pinkish_armada/broken-link-checker)
- [PDF to Markdown](https://apify.com/pinkish_armada/pdf-to-markdown)

Found a bug or need another ATS? Open an issue on the Actor's **Issues** tab.

# Actor input Schema

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

One per line. Accepts:<br>• <b>ATS board URLs</b>: <code>https://job-boards.greenhouse.io/airbnb</code>, <code>https://jobs.lever.co/palantir</code>, <code>https://jobs.ashbyhq.com/ramp</code>, <code>https://jobs.smartrecruiters.com/BoschGroup</code>, <code>https://apply.workable.com/huggingface</code><br>• <b>platform:token</b>: <code>greenhouse:airbnb</code>, <code>lever:palantir</code>, <code>ashby:ramp</code>, <code>smartrecruiters:BoschGroup</code>, <code>workable:huggingface</code> (<code>lever-eu:</code> for Lever EU)<br>• <b>Careers page or website URL</b>: <code>https://stripe.com/jobs</code>: the page is scanned for an embedded ATS board (robots.txt respected)<br>• <b>Company name</b>: <code>Palantir</code>: probes each platform for a matching board (best effort; check <code>confidence</code> in the run summary).

## `keywords` (type: `array`):

Keep jobs whose title contains ANY of these (case-insensitive). Acronyms and 1–2 letter terms like <code>AI</code>, <code>QA</code>, <code>iOS</code>, <code>SRE</code> match whole words only. Use <code>/regex/</code> for patterns, e.g. <code>/senior|staff/</code>. Leave empty for all jobs.

## `keywordsMatchDescription` (type: `boolean`):

Search the full job description as well as the title (e.g. to find all jobs mentioning <code>Kubernetes</code>).

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

Drop jobs whose title contains ANY of these, e.g. <code>intern</code>, <code>/principal|director/</code>.

## `locations` (type: `array`):

Keep jobs whose location (any listed location, city, region or country) contains ANY of these, e.g. <code>London</code>, <code>Australia</code>, <code>NY</code>. Two-letter country codes such as <code>AU</code> also match the country code. <code>Remote</code> matches remote jobs.

## `remote` (type: `string`):

Filter by remote / hybrid / on-site. Greenhouse doesn't publish a workplace field, so for Greenhouse it is inferred from the location text and custom fields (unknown counts as on-site).

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

Keep jobs whose department or team contains ANY of these, e.g. <code>Engineering</code>, <code>Sales</code>.

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

Keep only these types. Jobs whose platform doesn't state a type (common on Greenhouse) are kept.

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

Keep only jobs first published in the last N days. 0 = any date.

## `monitorMode` (type: `string`):

<b>Off</b>: return every matching job on each run.<br><b>New jobs only</b>: return only jobs that weren't on the board at the previous run (ideal for schedules and alerts).<br><b>New + changed</b>: also return jobs whose title, location, department, type, salary, apply URL or description changed.<br>State is kept per board in a named key-value store in your account, so it survives between runs and schedules.

## `monitorId` (type: `string`):

Separate monitors (e.g. one per client or task) by giving each a different ID. Change it to start from scratch.

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

Name of the key-value store (in your account) that remembers which jobs were already seen.

## `firstRunEmitsAll` (type: `boolean`):

On the first monitored run of a board, return all matching jobs (on) or just record them silently as a baseline (off) so that only future new jobs are returned.

## `reportRemovedJobs` (type: `boolean`):

In monitoring mode, also add a row with <code>changeType: "removed"</code> for each job that disappeared from the board since the last run. Removed-job rows are free.

## `descriptionFormat` (type: `string`):

Include the description as plain text, HTML, both, or not at all (smaller output). Descriptions are always read internally so salary ranges written in the text can still be extracted.

## `fetchSalaryDetails` (type: `boolean`):

Greenhouse publishes pay-transparency ranges on each job's detail endpoint. When on, one extra lightweight request is made per saved Greenhouse job to get structured salary min/max/currency.

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

Newest jobs first. 0 = no limit.

## `maxJobs` (type: `integer`):

Stop after saving this many jobs. 0 = no limit. You can also cap spending with the run's maximum cost setting.

## `platforms` (type: `array`):

Which ATS platforms to probe when an input is a company name or a website without an embedded board. Explicit board URLs and platform:token inputs are always fetched.

## `nameMatchMode` (type: `string`):

When a company name or website matches boards on more than one platform: keep the best match (name match + most jobs) or fetch all of them.

## `detectFromWebsites` (type: `boolean`):

For website / careers page URLs, fetch the page (and up to 5 careers-looking pages on the same site) to find an embedded or linked ATS board. robots.txt is respected. If off or nothing is found, the domain name is tried as a board slug.

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

Boards fetched in parallel. Per-platform rate limits are always respected (e.g. Workable is fetched one request at a time).

## Actor input object example

```json
{
  "companies": [
    "https://jobs.ashbyhq.com/ramp",
    "greenhouse:airbnb",
    "Palantir"
  ],
  "keywords": [
    "engineer",
    "data"
  ],
  "keywordsMatchDescription": false,
  "remote": "any",
  "postedWithinDays": 0,
  "monitorMode": "off",
  "monitorId": "default",
  "stateStoreName": "ats-jobs-monitor",
  "firstRunEmitsAll": true,
  "reportRemovedJobs": false,
  "descriptionFormat": "both",
  "fetchSalaryDetails": true,
  "maxJobsPerCompany": 0,
  "maxJobs": 0,
  "platforms": [
    "greenhouse",
    "lever",
    "ashby",
    "smartrecruiters",
    "workable"
  ],
  "nameMatchMode": "best",
  "detectFromWebsites": true,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Normalised job postings (title, company, location, remote, department, type, salary, dates, URLs, description).

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

Per input: detected platform and board token, confidence, jobs on board / matched / saved, new / changed / removed counts and any errors.

# 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": [
        "https://jobs.ashbyhq.com/ramp",
        "greenhouse:airbnb",
        "Palantir"
    ],
    "keywords": [
        "engineer",
        "data"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("pinkish_armada/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": [
        "https://jobs.ashbyhq.com/ramp",
        "greenhouse:airbnb",
        "Palantir",
    ],
    "keywords": [
        "engineer",
        "data",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("pinkish_armada/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": [
    "https://jobs.ashbyhq.com/ramp",
    "greenhouse:airbnb",
    "Palantir"
  ],
  "keywords": [
    "engineer",
    "data"
  ]
}' |
apify call pinkish_armada/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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