# ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workable, Recruitee (`adastra-connect/ats-jobs-scraper`) Actor

Scrape jobs from Greenhouse, Lever, Ashby, Workable and Recruitee boards via official public APIs. Get titles, locations, salaries and apply links, filter roles, and monitor new job postings.

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

## Pricing

from $1.10 / 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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, Workable and Recruitee job boards

Scrape open jobs from **Greenhouse, Lever, Ashby, Workable and Recruitee** company career boards in one run. ATS Jobs Scraper reads each platform's official public job board API and returns one clean, consistent dataset: job title, company, department, location, remote status, employment type, salary range, posting date, description and apply link. No login, browser or proxy, so runs are fast and cheap.

Turn on monitoring and the Actor returns **only jobs that are new since the last run**, which makes it a simple job alert feed for recruiting, sales prospecting, market research or your own job search.

### Why use ATS Jobs Scraper?

- **Five ATS platforms, one schema.** Mix Greenhouse, Lever, Ashby, Workable and Recruitee boards in the same input and get identical fields for every job.
- **Official public APIs only.** The same endpoints company career pages use. No HTML parsing that breaks when a page redesign ships.
- **New-job monitoring built in.** Schedule it daily and pay only for jobs you have not seen before.
- **Structured salaries.** Uses structured compensation where the ATS provides it (for example Ashby), with conservative text extraction as a fallback.
- **Filters before billing.** Keyword, exclusion, location, remote, department and date filters run before output, and you pay only for rows you receive.
- **Fault tolerant.** One broken or moved board is reported in the run summary and never fails the whole run.

### Use cases

- **Recruiting and talent intelligence:** track hiring at competitor or target companies by team and location.
- **Sales and lead signals:** spot companies hiring for roles that indicate a need for your product (for example "data engineer" or "security").
- **Job boards and newsletters:** feed a niche job board or weekly digest with fresh postings from a curated company list.
- **Market research:** count openings by department, location or remote policy over time.
- **Personal job alerts:** watch a shortlist of companies and get only new matching roles.

### What data can you get

| Fields | Data |
| --- | --- |
| `id` | Stable `ats:companySlug:nativeId` identifier. A URL hash is used if an API has no native ID. |
| `ats`, `companySlug`, `companyName` | Source and company. The board slug is used when the API does not provide a company display name. |
| `title`, `department`, `team` | Job title and available organizational categories. |
| `location`, `locations`, `country` | Primary location, all available location strings, and country when supplied. |
| `isRemote`, `workplaceType` | Remote flag and `onsite`, `hybrid`, `remote`, or null. Unknown remote flags are null. |
| `employmentType`, `employmentTypeRaw` | `full_time`, `part_time`, `contract`, `temporary`, `internship`, `other`, or null, plus the source value. |
| `salaryMin`, `salaryMax`, `salaryCurrency` | Structured compensation when available, otherwise conservative extraction from job text. Unknown values are null. |
| `salaryInterval`, `salaryText` | `year`, `month`, `week`, `day`, `hour`, or null, and an available compensation summary or matched snippet. |
| `url`, `applyUrl` | Public job page and application link when supplied. |
| `publishedAt`, `updatedAt` | ISO 8601 dates, or null. A created date is used when a published date is unavailable. |
| `descriptionText` | Plain text with paragraph breaks, included by default. |
| `descriptionHtml` | Optional safe paragraph HTML rebuilt from the cleaned description. |
| `isNew`, `scrapedAt` | New-job flag in monitoring mode, otherwise null, and the collection timestamp. |

The dataset's **overview** table shows the company, job title, location, remote status, employment type, salary, posting date, URL and ATS. Export JSON, CSV, Excel or other formats supported by Apify datasets.

### How to use

1. Open the Actor input and enter companies as ATS shorthand, job board URLs or bare slugs.
2. Add optional keyword, location, department or remote filters.
3. Set the maximum number of results and start the Actor.
4. Open the dataset to review or export jobs. Open `RUN_SUMMARY` in the default key-value store to inspect target counts and errors.
5. To monitor jobs, enable `onlyNewJobs`, keep the same `monitorKey`, and schedule repeated runs.

Empty programmatic input `{}` uses `greenhouse:stripe` and a maximum of 1,000 results. The input editor also offers a multi-company example.

#### Company formats

- `greenhouse:stripe`, `lever:spotify`, `ashby:ramp`, `workable:huggingface`, `recruitee:your-company`
- `https://boards.greenhouse.io/stripe`, `https://job-boards.greenhouse.io/stripe` or `https://job-boards.eu.greenhouse.io/your-company`
- `https://jobs.lever.co/spotify` or `https://jobs.eu.lever.co/your-company`
- `https://jobs.ashbyhq.com/ramp`
- `https://apply.workable.com/huggingface`
- `https://your-company.recruitee.com`
- `notion`: try Greenhouse, Lever, Ashby, Workable and Recruitee in that order, taking the first board with at least one listed job. The selected source and probes appear in `RUN_SUMMARY`. Probes do not emit or bill rows.

Lever tries the global API first and tries its EU API on HTTP 404. A supplied EU board URL follows this same lookup rule. Greenhouse URLs, including EU board URLs, use the specified public `boards-api.greenhouse.io` endpoint.

#### Input example

```json
{
  "companies": [
    "greenhouse:stripe",
    "lever:spotify",
    "ashby:ramp",
    "workable:huggingface",
    "notion"
  ],
  "keywords": ["engineer"],
  "excludeKeywords": ["intern"],
  "locations": ["Toronto", "Canada", "Remote"],
  "remoteOnly": false,
  "searchInDescription": false,
  "maxJobsPerCompany": 15,
  "maxResults": 60,
  "requestDelayMs": 500
}
```

#### Input reference

| Input | Default | Behavior |
| --- | --- | --- |
| `companies` | `["greenhouse:stripe"]` | Required array of non-empty company strings. Invalid individual targets are isolated. |
| `keywords`, `excludeKeywords` | `[]` | Case-insensitive substring matching. ANY keyword includes a job; ANY exclusion removes it. |
| `searchInDescription` | `false` | Apply keyword inclusion and exclusion to title and description together. |
| `locations` | `[]` | Match ANY substring in ANY location. Explicit remote status also matches `Remote`. |
| `remoteOnly` | `false` | Keep only explicitly remote jobs. Hybrid roles are excluded. |
| `departments` | `[]` | Match ANY substring in department or team. |
| `postedWithinDays` | `0` | Published/created date window. Zero disables it; undated jobs are kept. |
| `includeDescription` | `true` | Include plain text description. Filtering and salary extraction work even when this is false. |
| `includeDescriptionHtml` | `false` | Include sanitized paragraph HTML. Original attributes, links and layout are removed. |
| `onlyNewJobs` | `false` | Output jobs absent from the previous full snapshot for this monitor. |
| `monitorKey` | `"default"` | Separate independent monitoring baselines. |
| `maxJobsPerCompany` | `0` | Per-company output limit after filters and new-job detection; zero is unlimited. |
| `maxResults` | `100` | Positive integer; hard cap on billable output rows. |
| `requestDelayMs` | `500` | Minimum per-host request spacing. Lever always uses at least 1,000 ms. Robots crawl delays can raise it. |

Filter groups are combined with AND. Title matching is a substring search, so `engineer` also matches `engineering`. Limits apply after filtering. Duplicate entries pointing to the same ATS and slug do not produce duplicate rows.

#### Output example

Illustrative job data, with the same schema as a real result:

```json
{
  "id": "lever:example:bc8214ab-9810-4050-89ba-123456789abc",
  "ats": "lever",
  "companySlug": "example",
  "companyName": "example",
  "title": "Senior Backend Engineer",
  "department": "Engineering",
  "team": "Platform",
  "location": "Toronto, Canada",
  "locations": ["Toronto, Canada", "Remote Canada"],
  "country": null,
  "isRemote": true,
  "workplaceType": "remote",
  "employmentType": "full_time",
  "employmentTypeRaw": "Full-time",
  "salaryMin": 120000,
  "salaryMax": 150000,
  "salaryCurrency": "CAD",
  "salaryInterval": "year",
  "salaryText": null,
  "url": "https://jobs.lever.co/example/bc8214ab-9810-4050-89ba-123456789abc",
  "applyUrl": "https://jobs.lever.co/example/bc8214ab-9810-4050-89ba-123456789abc/apply",
  "publishedAt": "2026-10-01T10:00:00.000Z",
  "updatedAt": null,
  "descriptionText": "Build reliable services.\n\nWork with the platform team.",
  "isNew": true,
  "scrapedAt": "2026-10-20T12:00:00.000Z"
}
```

### How much does it cost to scrape ATS jobs?

This Actor uses **pay per event** pricing: **$1.50 per 1,000 jobs** returned (Apify Store discount tiers pay less). The price on the Store page is authoritative. Platform compute is included, so you pay for the jobs you receive plus Apify's small per-run start fee, nothing else.

| Example run | Jobs returned | Approximate cost |
| --- | --- | --- |
| 5 companies, filtered to engineering roles | 50 | about $0.08 |
| 20 companies, all roles | 1,000 | about $1.50 |
| Daily monitoring of 20 companies, about 30 new jobs per day | 900 per month | about $1.35 per month |

What you are charged for: each job row written to the dataset is one `job` event. Auto-detection probes, failed boards, filtered-out jobs and previously seen jobs in monitoring mode are free. Use `maxResults` to cap a run, and set a maximum cost per run in Apify; the Actor stops cleanly when that limit is reached.

### Incremental runs / monitoring

Set `onlyNewJobs: true` and choose a stable `monitorKey`. State is stored in the **named key-value store `ats-jobs-scraper-state`**, under a SHA-256-derived monitor key, with separate ID sets for each ATS and company slug. Ordinary runs with monitoring disabled neither read nor update this state.

The first monitoring run returns all current matching jobs, subject to the output limits. You can make this first run cheap by setting a small `maxResults`. The Actor still fetches full snapshots and saves every current ID, including jobs hidden by filters or output limits. Later runs therefore return only jobs introduced after that baseline. Changing filters does not make an existing job new.

After a successful full fetch, IDs absent from that snapshot are removed. A closed job that later reappears is new again. Changed descriptions or titles with the same ID are not new. Failed or incomplete fetches do not replace the company's baseline. Boards not included in a run retain their stored baseline.

A spending-limit stop ends further work. Successfully processed companies may have their baselines updated; unprocessed companies keep their earlier state. Run the same monitor serially to avoid concurrent writes. Use another `monitorKey`, or delete that monitor's named-store record, to start over.

### Scheduling and integrations

Use Apify schedules for periodic checks and webhooks to trigger a downstream process when a run finishes. Start runs and download datasets through the Apify API. Connect results to Make, Zapier or n8n for notifications, spreadsheets and recruiting workflows. AI agents can call the Actor through Apify's MCP integration and consume the normalized job records.

### Is it legal to scrape job postings?

This Actor uses only official public GET endpoints intended for company career pages. It does not log in, send cookies, collect applicant profiles, or submit applications. It identifies itself, checks the API host's robots.txt before fetching, and obeys disallowed paths and crawl delays. Following RFC 9309, robots.txt redirects are followed (for example from a Recruitee subdomain to the company's own careers domain), a robots.txt that is unavailable with a 4xx status means no restrictions, and a server error or HTML response causes the target to be skipped. SmartRecruiters is intentionally unsupported because its API robots policy disallows generic crawlers.

The output is limited to job fields. It excludes recruiter, hiring-manager and applicant objects and arbitrary metadata, and redacts email addresses, phone numbers and detected individual names in job copy. Safe description HTML is rebuilt from redacted text. Automated name detection has limitations; review descriptions before redistribution. You are responsible for your use of the data, applicable terms and legal requirements.

Public API references: [Greenhouse Job Board API](https://developers.greenhouse.io/job-board.html), [Lever Postings API](https://github.com/lever/postings-api), [Ashby Job Postings API](https://developers.ashbyhq.com/docs/public-job-posting-api). Workable uses its public widget endpoint and Recruitee uses its public careers offers endpoint, as listed in the source adapters.

### Limitations

- Only the five listed ATS platforms and their public jobs are supported. No private jobs or authenticated APIs are used.
- Companies can change ATS providers, close boards or change API formats. Auto-detection takes the first non-empty board and cannot guarantee it belongs to the company you intended.
- An explicit board with zero jobs is a successful fetch. Auto-detection requires at least one job.
- API access depends on each provider's current robots policy and public API. If a provider changes either, affected targets are skipped and reported in `RUN_SUMMARY` rather than failing the whole run.
- Ashby's public API does not return a company display name, so `companyName` is the board slug for Ashby jobs.
- Workable's widget often provides job summaries without descriptions, salaries or update dates. No detail-page scraping is performed to fill these gaps.
- Company names, dates, country, workplace type and compensation may be absent. Null values are deliberate. Lever and Ashby commonly use the slug as the company display name.
- Salary extraction is conservative. Structured salary fields take priority. Multiple money ranges or conflicting currencies leave numeric fields null. A bare dollar sign does not identify a currency. No currency conversion or annualization is performed.
- Full list snapshots are fetched before filters or output limits. Lever uses paginated requests; the other specified endpoints return full lists. Normal runs can stop at the overall output cap; monitoring continues through all targets unless the spending limit is reached.
- Only complete valid snapshots update monitoring state. The state is an ID baseline, not a complete job archive or a guarantee of downstream delivery.
- Requests have a 20-second timeout and up to four retries with exponential backoff and jitter for transient errors. Retry-After is honored. Targets run sequentially, with one request at a time per host.

### FAQ

**Do I need an API key or company account?** No. Only public job board endpoints are used.

**Can I use a company homepage?** Use its supported ATS board URL, `ats:slug`, or the board slug. Arbitrary company websites are not crawled.

**Why did a company return no results?** Check filters and `RUN_SUMMARY`. The board might be empty, incorrect, migrated, blocked by robots, or temporarily unavailable.

**Will the second monitoring run be empty?** Yes, if no new job IDs appeared on successfully fetched boards. The first run establishes the baseline even when output limits hide some jobs.

**Can I get raw HTML?** The optional HTML field contains safe paragraphs rebuilt from redacted text. It omits original markup, attributes and embedded content.

**How do I find a company's board?** Open the company's careers page and click any job. The job URL shows the ATS and slug, for example `boards.greenhouse.io/stripe` or `jobs.lever.co/spotify`. You can paste that URL directly, or just enter the slug and let auto-detection find it.

**How fast is it?** A typical board of a few hundred jobs takes a few seconds. Lever boards are paged and rate-limited politely, so very large Lever boards take longer.

**Can I get jobs from Workday, SmartRecruiters, iCIMS or LinkedIn?** Not in this Actor. It only uses documented public job board APIs, and SmartRecruiters' API robots policy disallows generic crawlers.

**Can one bad company fail the entire run?** Per-target failures are isolated and recorded. The run fails when every attempted target fails. Invalid top-level input also fails clearly.

### Support

Open an issue in the Actor's **Issues** tab. Include the company entry, relevant input and run link or `RUN_SUMMARY` error. Do not include credentials or personal information.

### Disclaimer

Greenhouse, Lever, Ashby, Workable and Recruitee are trademarks of their respective owners. This Actor is an independent tool and is not affiliated with, sponsored by or endorsed by any of them. Nothing in this README is legal advice.

# Actor input Schema

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

Use ats:slug, a supported job board URL, or a bare slug for auto-detection. Empty Actor input defaults to Stripe.

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

Keep jobs matching ANY keyword. Case-insensitive substring matching.

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

Reject jobs matching ANY excluded keyword.

## `searchInDescription` (type: `boolean`):

Apply keyword inclusion and exclusion to descriptions as well as titles.

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

Keep jobs matching ANY location substring, such as Toronto, Canada or Remote.

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

Keep jobs explicitly identified as remote. Hybrid jobs are excluded.

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

Keep jobs matching ANY department or team substring.

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

0 disables the date filter. Jobs without a published or created date are kept.

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

Include redacted, whitespace-normalized plain text descriptions.

## `includeDescriptionHtml` (type: `boolean`):

Include sanitized paragraph HTML reconstructed from redacted description text.

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

Compare the full snapshot with the last successful snapshot for this monitorKey. The first run returns all matching jobs up to output limits. A small maxResults makes the first run cheap.

## `monitorKey` (type: `string`):

Separate independent monitors. State is stored in the named ats-jobs-scraper-state key-value store.

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

0 means unlimited. Applied after filters and new-job detection.

## `maxResults` (type: `integer`):

Hard cap on billable output rows. Defaults to 100; raise it for full boards. Monitoring still fetches complete snapshots for all targets.

## `requestDelayMs` (type: `integer`):

Minimum spacing per host. Lever always uses at least 1000 ms. Robots crawl delays can increase this value.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:stripe",
    "ashby:ramp"
  ],
  "searchInDescription": false,
  "remoteOnly": false,
  "postedWithinDays": 0,
  "includeDescription": true,
  "includeDescriptionHtml": false,
  "onlyNewJobs": false,
  "monitorKey": "default",
  "maxJobsPerCompany": 20,
  "maxResults": 50,
  "requestDelayMs": 500
}
```

# Actor output Schema

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

All rows in the default dataset.

## `overview` (type: `string`):

Dataset table view "overview".

## `runSummary` (type: `string`):

Per-target status, counts and errors (RUN_SUMMARY record).

# 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:stripe",
        "ashby:ramp"
    ],
    "maxJobsPerCompany": 20,
    "maxResults": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("adastra-connect/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": [
        "greenhouse:stripe",
        "ashby:ramp",
    ],
    "maxJobsPerCompany": 20,
    "maxResults": 50,
}

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

```

## MCP server setup

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