# ATS Jobs Scraper (`bigibyte/ats-jobs-scraper`) Actor

Unified job board scraper for 6 major ATS platforms: Greenhouse, Lever, Ashby, Workable, SmartRecruiters, and Recruitee. Extract open roles, structured salary ranges, remote status, locations, and hiring signals in a single unified schema without API keys.

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

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-usage

## 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, SmartRecruiters and Recruitee

<p align="center">
  <img src="https://imgur.com/lLrYZyM.png" alt="USA Automotive Leads Hub" style="max-width:100%; border-radius:14px; width:680px;" />
</p>

Paste a careers URL, an `ats:slug` pair, or a bare company domain. This actor collects every open job from Greenhouse, Lever, Ashby, Workable, SmartRecruiters, and Recruitee, and saves them in one schema: title, location, remote, department, salary, posted date, and description.

Salary shows up when the company actually publishes it.

### What it does

One career page scraper for six ATS platforms. You can mix boards in a single run. A Lever link, a Greenhouse slug, an Ashby org, a Workable account, a Recruitee subdomain, and a SmartRecruiters company id all land in the same dataset.

Failed boards do not stop the run. A missing or wrong slug becomes an error row (`NOT_FOUND`, or `NO_JOBS_OR_WRONG_ID` when SmartRecruiters returns zero jobs). Each company is also written to a `COMPANIES` summary as `{ats, slug, jobCount, status}`, which is the hiring-signal rollup.

### Use cases

- Job boards and aggregators that need public postings from several ATS platforms in one place.
- Recruiter sourcing: filter by title, location, department, and a posted-after date, then export the shortlist.
- B2B sales teams working from hiring signals. See which companies are opening roles, and in which teams, before you reach out.
- Salary and market research, for the roles where a company lists pay.
- Job alerts: schedule the actor and send new rows to Sheets, Slack, or a webhook.

### Input

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `startUrls` | array | Ashby Ramp and Lever Spotify | Careers URLs, `ats:slug` pairs, or company domains. Required. |
| `atsHint` | string | `auto` | Force one ATS, or leave auto-detect on. |
| `keywords` | array | empty | Case-insensitive match on the job title. Any keyword qualifies. |
| `locations` | array | empty | Case-insensitive match against location text. |
| `remoteOnly` | boolean | `false` | Keep only jobs the ATS marks remote. |
| `departments` | array | empty | Case-insensitive match against the department. |
| `postedAfter` | string | empty | Keep jobs posted on or after this date. |
| `includeDescription` | boolean | `true` | Include the full job description in each row. |
| `descriptionFormat` | string | `text` | `text`, `markdown`, or `html`. |
| `maxItems` | integer | `500` | Total jobs saved this run. Hard cap 5,000. |
| `maxJobsPerCompany` | integer | `1000` | Cap per company. |
| `maxConcurrency` | integer | `5` | Companies processed at once. Hard cap 5. |
| `timeoutSecs` | integer | `300` | Run time limit. Jobs already saved are kept. |
| `requestTimeoutSecs` | integer | `30` | Time limit for each board. Raise it for very large boards. |
| `proxyConfiguration` | object | No proxy | Optional proxy settings. Off by default. |

#### Example input

```json
{
  "startUrls": [
    "https://jobs.ashbyhq.com/ramp",
    "https://jobs.lever.co/spotify",
    "greenhouse:airbnb",
    "https://apply.workable.com/huggingface/",
    "https://bunq.recruitee.com",
    "https://careers.smartrecruiters.com/BoschGroup",
    "stripe.com"
  ],
  "keywords": ["engineer"],
  "remoteOnly": false,
  "includeDescription": true,
  "descriptionFormat": "text",
  "maxItems": 500,
  "maxJobsPerCompany": 1000,
  "maxConcurrency": 5,
  "proxyConfiguration": { "useApifyProxy": false }
}
```

`stripe.com` is best effort. A direct `greenhouse:stripe` form is the most reliable when you already know the slug.

### Output

Each job is one dataset row. The title is stored twice: `title` for the brief's field name, and `itemTitle` for the dataset view (Apify reserves the name `title` in the schema). Both values match.

```json
{
  "ats": "ashby",
  "atsSlug": "ramp",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "itemTitle": "Security Engineer, Cloud",
  "company": "ramp",
  "location": "New York, NY (HQ)",
  "locations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
  "country": "USA",
  "remote": true,
  "workplaceType": "Hybrid",
  "department": "Engineering",
  "team": "Backend",
  "employmentType": "FullTime",
  "salaryMin": 211400,
  "salaryMax": 290600,
  "salaryCurrency": "USD",
  "salaryInterval": "1 YEAR",
  "salaryText": "$211.4K - $290.6K",
  "salarySource": "structured",
  "postedAt": "2026-04-07T17:12:35.753+00:00",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application"
}
```

A bad input looks like this and does not fail the run:

```json
{"sourceInput": "https://jobs.lever.co/plaid", "ats": "lever", "error": "NOT_FOUND"}
```

Views: `jobs` (company, title, location, remote, department, salary, posted date, URL), `salaries`, and `errors`.

Dataset exports and Apify integrations cover Google Sheets, Airtable, Make, n8n, and webhooks. Point a webhook at the default dataset when a scheduled run should feed a job alert.

The `COMPANIES` summary holds one record per board: `ats`, `slug`, `jobCount`, and `status` (`OK`, `EMPTY`, `FILTERED`, `NOT_FOUND`, or `NO_JOBS_OR_WRONG_ID`).

### Supported ATS

| ATS | What to paste | Slug |
|-----|----------------|------|
| Greenhouse | `https://job-boards.greenhouse.io/airbnb` or `https://boards.greenhouse.io/airbnb` | `airbnb` |
| Lever | `https://jobs.lever.co/spotify` | `spotify` |
| Ashby | `https://jobs.ashbyhq.com/ramp` | `ramp` |
| Workable | `https://apply.workable.com/huggingface/` | `huggingface` |
| SmartRecruiters | `https://careers.smartrecruiters.com/BoschGroup` | `BoschGroup` (case-sensitive) |
| Recruitee | `https://bunq.recruitee.com` | `bunq` |

If you are looking for a greenhouse jobs scraper, a lever jobs scraper, an ashby jobs scraper, a workable jobs scraper, a smartrecruiters scraper, or a recruitee scraper, the slug is the first path segment or the subdomain on the careers host.

SmartRecruiters company ids are case-sensitive. A wrong id returns zero jobs and is recorded as `NO_JOBS_OR_WRONG_ID`. For SmartRecruiters, descriptions are optional and controlled by `includeDescription`.

### Limits

Public postings only. No candidate data, and nothing private.

The free cap is 5,000 jobs per run (`maxItems`). `maxJobsPerCompany` defaults to 1,000, which matters on very large boards like Bosch Group on SmartRecruiters (thousands of postings).

Salary data is only as good as what the company published. Many boards leave it empty.

Concurrency stays at or below 5.

### FAQ

**How do I find a company's slug?**
Open the careers page and read the host. `jobs.lever.co/spotify` means Lever and `spotify`. `jobs.ashbyhq.com/ramp` means Ashby and `ramp`. If the company hides the ATS on its own domain, paste the domain anyway.

**Why is salary empty?**
The company did not publish salary for that role. That is common.

**How many jobs can one run save?**
5,000 total, and by default 1,000 per company. Raise `requestTimeoutSecs` if a very large board is slow. `timeoutSecs` stops the run and keeps rows already saved.

**Why is SmartRecruiters slower when descriptions are on?**
Including descriptions makes a SmartRecruiters run slower. Turn `includeDescription` off if you only need title, location, and date.

**What happens if the slug is wrong?**
You get an error row, not a failed run. Empty boards (a Lever company that moved, a Workable account with zero jobs) are stored as `EMPTY` on the company summary and do not look like a crash.

### Related tools

If you plan to act on these rows, enriching the companies with contact and email data or a quick CSV cleanup are the usual next steps. BigiByte also builds scrapers for other job boards and company data when you need them as a separate job.

Need another data source, a scheduled job monitor, or a custom pipeline? BigiByte builds custom scrapers and APIs: https://bigibyte.com

### Trademark Notice

Greenhouse, Lever, Ashby, Workable, SmartRecruiters, and Recruitee and related names, logos, and trademarks belong to their respective owners. Their use in this documentation is solely to identify the source of the data and does not imply affiliation, sponsorship, endorsement, or partnership.

### Disclaimer

This Actor is an independent tool and is not affiliated with, endorsed by, sponsored by, or otherwise officially associated with Greenhouse, Lever, Ashby, Workable, SmartRecruiters, and Recruitee.

### Is web scraping legal?

Collecting information that is already public is generally legal. Scraping is not the same thing as hacking, and there is no single law that bans it. The limits are the usual ones: do not break into accounts, do not take private or confidential data, and be careful with personal data and copyrighted works. A site's terms can also restrict how you access it. Facts such as a public name, price, or rating are not the same thing as republishing someone's article or photo.

This actor collects information that is already publicly available. It does not access private or protected data.

This is not legal advice. For the longer explanation, see Apify's article [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/).

# Actor input Schema

## `startUrls` (type: `array`):

Careers page URLs, ats:slug pairs, or company domains. Examples: https://jobs.lever.co/spotify, https://job-boards.greenhouse.io/airbnb, https://jobs.ashbyhq.com/ramp, https://apply.workable.com/huggingface/, https://bunq.recruitee.com, https://careers.smartrecruiters.com/BoschGroup, greenhouse:stripe, stripe.com

## `atsHint` (type: `string`):

Leave on auto to detect the ATS from the URL, page HTML, or a short list of slug guesses. Pick a platform to force that ATS.

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

Case-insensitive match on the job title. A job is kept when its title contains any keyword. Leave empty to keep every title.

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

Case-insensitive substring match against the job location and extra locations. A job is kept when any value matches.

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

Keep only jobs the ATS marks as remote.

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

Case-insensitive substring match against the department. A job is kept when any value matches.

## `postedAfter` (type: `string`):

Keep jobs posted on or after this date. Jobs with no posted date are dropped when this is set.

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

Store the job description. SmartRecruiters needs 1 extra request per job, so turn this off to go faster on large boards.

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

How descriptionText is stored: plain text, markdown, or HTML.

## `maxItems` (type: `integer`):

Free version cap: 5,000 jobs per run. The actor stops once this many jobs have been saved.

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

Cap jobs saved for each company. The run also stops at maxItems. Large SmartRecruiters boards are paged until this cap is hit.

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

How many companies to fetch at once. Hard cap is 5, matching the actor limit.

## `timeoutSecs` (type: `integer`):

Maximum run time in seconds. When the clock runs out the actor stops and keeps the jobs already saved.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout in seconds. Raise it for large boards such as a full Lever or Ashby payload.

## `proxyConfiguration` (type: `object`):

Optional. These job board APIs are public, so the default is no proxy. Turn on Apify Proxy only if a board starts refusing the datacenter IP.

## Actor input object example

```json
{
  "startUrls": [
    "https://jobs.ashbyhq.com/ramp",
    "https://jobs.lever.co/spotify"
  ],
  "atsHint": "auto",
  "remoteOnly": false,
  "includeDescription": true,
  "descriptionFormat": "text",
  "maxItems": 500,
  "maxJobsPerCompany": 1000,
  "maxConcurrency": 5,
  "timeoutSecs": 300,
  "requestTimeoutSecs": 30,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `jobs` (type: `string`):

Open jobs in one schema: company, title, location, remote, department, salary, posted date, and URL.

## `salaries` (type: `string`):

The same jobs with the salary columns: minimum, maximum, and currency.

## `errors` (type: `string`):

Inputs that were not a public board, returned no SmartRecruiters match, or failed the request.

## `dataset` (type: `string`):

Every saved row, including jobs and error rows, with all fields.

# 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 = {
    "startUrls": [
        "https://jobs.ashbyhq.com/ramp",
        "https://jobs.lever.co/spotify"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("bigibyte/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 = {
    "startUrls": [
        "https://jobs.ashbyhq.com/ramp",
        "https://jobs.lever.co/spotify",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("bigibyte/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 '{
  "startUrls": [
    "https://jobs.ashbyhq.com/ramp",
    "https://jobs.lever.co/spotify"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call bigibyte/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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