# ATS Jobs Scraper: Workable, SmartRecruiters, Personio & more (`kilndata/ats-jobs-scraper`) Actor

Get every open job from company career sites on Workable, SmartRecruiters, Recruitee, Personio, Teamtailor, BambooHR, Breezy HR, Greenhouse, Lever and Ashby in one normalized format: title, location, remote, type, salary, description, apply link. Auto-detects the ATS. No login, no personal data.

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

## Pricing

from $1.00 / 1,000 job listings

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: Workable, SmartRecruiters, Recruitee, Personio, Teamtailor, BambooHR, Breezy HR, Greenhouse, Lever, Ashby

Give it a company career site, an "ats:slug" pair or just a company name. It finds out which applicant tracking system (ATS) the company uses, reads every open job and returns them in one flat format: title, department, location, remote or on-site, employment type, salary when published, description, job link and apply link.

The same fields come out whether the job lives on Workable or on Ashby, so you do not need ten parsers.

- **Ten systems, one format.** Workable, SmartRecruiters, Recruitee, Personio, Teamtailor, BambooHR, Breezy HR, Greenhouse, Lever and Ashby.
- **Detects the system for you.** Paste `https://www.example.com/careers` and the actor looks for the ATS behind the page. Or paste the ATS address directly, or type `greenhouse:stripe`, or a plain name such as `Hugging Face`.
- **Filters before you pay.** Title keywords, location, remote only, posted in the last N days. Jobs that are filtered out are not charged.
- **Normalized values.** `workplaceType` is `remote`, `hybrid` or `on-site`. `employmentType` is `full-time`, `part-time`, `contract` and so on. The original wording stays in `employmentTypeRaw`.
- **No personal data.** No recruiter names, no personal e-mail addresses, no phone numbers. See the Limitations section.

### Who is it for?

| Use | How |
|---|---|
| Recruiters and sourcers | Watch competitors' openings by role and city, run it weekly on a schedule |
| Job boards and aggregators | Pull open jobs from many employers without one integration per ATS |
| Salary research | Use the **Salaries** view: `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod` for jobs that publish a range |
| Market analysis | Count openings per department, country or company over time; compare remote and on-site share |
| AI agents / MCP | Small, flat JSON, easy to ask: "remote data engineer jobs at these 30 companies" |

### Supported systems

The list below is exactly what the code supports. Anything else is not read.

| System | Input examples | Where the data comes from |
|---|---|---|
| Workable | `https://apply.workable.com/huggingface/` | Public widget API of the career page |
| SmartRecruiters | `https://careers.smartrecruiters.com/Wix2` | The public career site and its job pages (HTML). The SmartRecruiters API is not used, because its robots.txt disallows generic bots |
| Recruitee | `https://bunq.recruitee.com`, custom domains | Public offers API of the career site |
| Personio | `https://company.jobs.personio.de` (also `.com`) | Public XML job feed |
| Teamtailor | `https://tibber.teamtailor.com`, custom domains | Public RSS feed |
| BambooHR | `https://flyio.bamboohr.com/careers` | Public careers JSON; one request per job for the description |
| Breezy HR | `https://company.breezy.hr` | Public JSON list plus the job page for the description |
| Greenhouse | `https://job-boards.greenhouse.io/discord` | Public Job Board API |
| Lever | `https://jobs.lever.co/spotify` (EU site too) | Public Postings API |
| Ashby | `https://jobs.ashbyhq.com/notion` | Public Job Posting API, with compensation |

### How it works

1. Each input line is read. An ATS address or `ats:slug` goes straight to that system.
2. Any other web address is opened and searched for links or embeds of a supported ATS. If nothing is found on the page, `/careers` and `/jobs` on the same site are tried. If still nothing, you get a `not-found` line with the reason.
3. A plain company name is turned into a few likely slugs (`huggingface`, `hugging-face`) and tried on each supported system, in order, until one has jobs. You can limit this with **Systems to try**. A name is a guess, so a URL is more reliable.
4. The jobs are listed, filtered, enriched with details when the system needs one request per job (SmartRecruiters, BambooHR, Breezy HR), cleaned and saved.

Requests go to the same public endpoints the ATS offers to the company's own career page. No login, no browser. At most 3 requests at a time per ATS host, with a short pause between them. Timeouts, HTTP 429 and 5xx are retried with backoff.

### Input

Example:

```json
{
    "companies": ["https://apply.workable.com/huggingface/", "greenhouse:stripe", "Notion"],
    "systems": [],
    "maxJobsPerCompany": 50,
    "titleKeywords": ["engineer"],
    "remoteOnly": true
}
```

| Field | Type | Default | Meaning |
|---|---|---|---|
| `companies` | array | required | Career site URLs, ATS addresses, `ats:slug` pairs or company names, one per line |
| `systems` | array | all | Only for plain names: which systems to try (`workable`, `smartrecruiters`, `recruitee`, `personio`, `teamtailor`, `bamboohr`, `breezy`, `greenhouse`, `lever`, `ashby`) |
| `maxJobsPerCompany` | integer | 0 (no limit) | Stop after this many jobs per company |
| `maxJobs` | integer | 0 (no limit) | Stop the whole run after this many jobs |
| `includeDescription` | boolean | true | Return the description as text and HTML. Turn off for a fast list; this also saves the extra request per job on SmartRecruiters, BambooHR and Breezy HR |
| `titleKeywords` | array | empty | Keep jobs whose title contains any of these words |
| `excludeTitleKeywords` | array | empty | Drop jobs whose title contains any of these words |
| `locationKeywords` | array | empty | Keep jobs whose location, city, region or country contains any of these words |
| `remoteOnly` | boolean | false | Keep only jobs marked remote, or with "remote" in the location |
| `postedWithinDays` | integer | 0 (any) | Keep jobs published in the last N days. Jobs with no date are kept |
| `language` | string | empty | Two-letter code (`en`, `de`) for Personio feeds that publish several languages |
| `keepCompanyEmails` | boolean | false | Keep generic mailboxes such as jobs@ or careers@ in descriptions. Other e-mail addresses are always removed |
| `respectRobotsTxt` | boolean | true | Skip endpoints that robots.txt disallows. Skipped companies are listed and not charged |
| `maxConcurrency` | integer | 10 | Companies processed at the same time (1 to 50) |
| `detailConcurrency` | integer | 4 | Job pages read at the same time per company (1 to 10) |
| `requestTimeoutSecs` | integer | 30 | Wait for one response (5 to 120) |
| `maxRetries` | integer | 2 | Retries per request (0 to 5) |
| `proxyConfiguration` | object | no proxy | Optional. The public endpoints work without one |

### Output

One dataset item per job (description shortened here):

```json
{
    "status": "ok",
    "input": "ashby:notion",
    "ats": "ashby",
    "companySlug": "notion",
    "companyName": "notion",
    "jobId": "1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
    "title": "Software Engineer, Developer Platform",
    "department": "Engineering",
    "location": "San Francisco, California",
    "country": "United States",
    "locations": ["San Francisco, California", "New York, New York"],
    "isRemote": false,
    "workplaceType": "hybrid",
    "employmentType": "full-time",
    "employmentTypeRaw": "FullTime",
    "postedAt": "2026-08-24T14:44:49.699Z",
    "salaryMin": null,
    "salaryMax": null,
    "salaryCurrency": null,
    "salaryPeriod": null,
    "salaryText": null,
    "descriptionText": "Who We Are\nNotion is the collaborative AI workspace where teams and agents think together. ...",
    "jobUrl": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a",
    "applyUrl": "https://jobs.ashbyhq.com/notion/1fc309c8-da20-4ff2-84c7-8b863ece2b0a/application",
    "careersPageUrl": "https://jobs.ashbyhq.com/notion",
    "scrapedAt": "2026-10-05T03:55:20.618Z",
    "error": null
}
```

The full item also has `team`, `city`, `region`, `countryCode`, `experienceLevel`, `descriptionHtml` and `language`; fields a system does not publish are `null`.

A company that returns nothing gets one item with no job data and a `status` of `no-jobs`, `not-found`, `failed`, `skipped` (robots.txt) or `invalid-input`, plus an `error` that says why. These lines are free. At the end of the run, a `SUMMARY` record in the key-value store lists the result per company.

The dataset has two views: **Jobs** and **Salaries**.

### How much does it cost?

You pay per job returned, not for time or for pages read.

| Plan | Per 1,000 jobs |
|---|---|
| Free | $2.00 |
| Starter (Bronze) | $1.50 |
| Scale (Silver) | $1.20 |
| Business (Gold) | $1.00 |

- The event is `job-listing`, charged once per job saved. Duplicates inside one run are saved and charged once.
- A small `Actor Start` fee of $0.00005 applies to each run.
- You are not charged for companies that fail, are not found, return no jobs, are skipped by robots.txt, or for invalid input lines.
- Jobs removed by your filters are not charged.
- Apify platform usage is billed separately by Apify.

**Spending limit.** Set **Max cost per run** in the run options. The actor reads it before starting, saves and charges job by job, and stops when the limit is reached. It never saves more jobs than your limit pays for. `maxJobs` gives you a second, count-based limit.

### Limitations

- **Only these ten systems.** A company on Pinpoint, iCIMS, Workday, Taleo or its own system is not read. You get a `not-found` or `skipped` line.
- **Companies change systems.** A career site that moved from one ATS to another may still show an old link. If a run returns an empty or missing board, check the career page in a browser and paste the current address.
- **Plain names are guesses.** `Notion` works if the company's board uses a slug like `notion`. Companies with unusual slugs need the URL or `ats:slug`.
- **Salary is often empty.** Most postings do not publish pay. Where a system has structured pay, it goes to `salaryMin`, `salaryMax`, `salaryCurrency` and `salaryPeriod` (Recruitee, Lever, Ashby). Some systems give only text, which goes to `salaryText` (BambooHR, Breezy HR). Do not treat an empty salary as zero.
- **Fields vary by system.** `department`, `team`, `experienceLevel`, `postedAt`, `countryCode` and workplace type exist only where the ATS publishes them. In one test run, Personio and Greenhouse jobs had no workplace type, and Teamtailor and Greenhouse jobs had no employment type.
- **SmartRecruiters is slower.** It is read from the public career site pages, one request per job for details, so large boards take longer. Use `includeDescription: false` for a quick list.
- **Descriptions are cleaned.** Text is the company's own. E-mail addresses, phone numbers, recruiter names and "contact person" blocks are removed, so a description can look slightly shorter than on the page.
- **What it does not collect.** No applicants, no candidate data, no recruiter or hiring-manager names, no personal e-mail addresses or phone numbers.
- Career sites can change or close an endpoint at any time.

### Responsible use

The actor reads only data that companies publish for anyone to see, through endpoints the ATS vendors provide to the companies' own career sites. It does not log in, does not read applicant data and does not try to get around blocks. It checks robots.txt (on by default), limits requests per host and backs off on HTTP 429. Whether you may use job data for a given purpose depends on the company's terms and your local law, so check that for your case.

### Integrations

Run it through the Apify API, schedules and webhooks, or the Apify MCP server. Send the dataset to JSON, CSV, Excel, Google Sheets, Make, Zapier or n8n with the standard Apify integrations.

### FAQ

**Do I need to know which ATS a company uses?** No. Paste the company's career page and the actor looks for the system. If it cannot tell, paste the ATS address or use `ats:slug`.

**Can it find the jobs of any company?** Only companies on the ten systems above whose job board is public.

**Does it return closed jobs?** No, only jobs that are open on the career site when the run reads it.

**Why did a company return `skipped`?** Its robots.txt disallows the endpoint and **Respect robots.txt** is on. That line is free.

**How do I get only remote engineering jobs?** Set `remoteOnly` to true and `titleKeywords` to `["engineer"]`.

**Can I get the salary of every job?** Only where the company publishes it. Many do not.

### Feedback

Missing a system, a field or a filter? Found a career site that does not parse? Open a ticket in the **Issues** tab of this actor. Include the input line you used and, if you can, the run link.

This actor is not affiliated with any of the applicant tracking systems or companies it reads.

# Actor input Schema

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

One per line. Accepted: an ATS job board URL ("https://apply.workable.com/huggingface/", "https://bunq.recruitee.com", "https://careers.smartrecruiters.com/Wix2", "https://1komma5grad.jobs.personio.de", "https://jobs.lever.co/palantir"), the company's own careers page ("https://www.example.com/careers" — the ATS is detected automatically), "ats:slug" ("greenhouse:stripe", "bamboohr:flyio", "breezy:plat4mation") or just a company name ("Hugging Face") — then every supported ATS is tried.

## `systems` (type: `array`):

Only used for inputs that are plain names (no URL, no "ats:" prefix). Leave empty to try all. Values: workable, smartrecruiters, recruitee, personio, teamtailor, bamboohr, breezy, greenhouse, lever, ashby.

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

Stop after this many jobs for each company. 0 = no limit.

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

Stop the whole run after this many jobs. 0 = no limit (your max cost per run still applies).

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

Return the full description as plain text and HTML. For SmartRecruiters, BambooHR and Breezy HR this needs one extra request per job; turn it off for a fast, lightweight list (title, location, type, link).

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

Keep only jobs whose title contains at least one of these words (case-insensitive), e.g. "engineer", "sales". Filtered-out jobs are not charged.

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

Drop jobs whose title contains any of these words, e.g. "intern", "senior".

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

Keep only jobs whose location, city, region or country contains one of these words, e.g. "Berlin", "Germany", "US", "Remote".

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

Keep only jobs marked as remote by the ATS (or with "remote" in the location).

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

Keep only jobs published in the last N days. 0 = any date. Jobs without a publication date are kept.

## `language` (type: `string`):

Optional two-letter language code for Personio feeds that publish several languages, e.g. "en" or "de". Other systems return the language the company published.

## `keepCompanyEmails` (type: `boolean`):

Off by default: every e-mail address in a description becomes "\[email removed]". Turn on to keep generic company mailboxes such as jobs@, careers@ or accommodations@. Personal e-mail addresses, phone numbers, recruiter names and "contact person" blocks are always removed.

## `respectRobotsTxt` (type: `boolean`):

Skip endpoints that robots.txt disallows. Skipped companies are listed in the results and not charged.

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

How many companies are processed at the same time. Requests to the same ATS host are additionally limited to 3 at a time.

## `detailConcurrency` (type: `integer`):

For systems that need one request per job (SmartRecruiters, BambooHR, Breezy HR).

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

How long to wait for one HTTP response before retrying.

## `maxRetries` (type: `integer`):

Retries for timeouts, HTTP 429 and 5xx (with backoff).

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

Optional. The public career-site endpoints work without a proxy.

## Actor input object example

```json
{
  "companies": [
    "https://apply.workable.com/huggingface/",
    "recruitee:bunq",
    "https://tibber.teamtailor.com"
  ],
  "systems": [],
  "maxJobsPerCompany": 20,
  "maxJobs": 0,
  "includeDescription": true,
  "titleKeywords": [],
  "excludeTitleKeywords": [],
  "locationKeywords": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "language": "",
  "keepCompanyEmails": false,
  "respectRobotsTxt": true,
  "maxConcurrency": 10,
  "detailConcurrency": 4,
  "requestTimeoutSecs": 30,
  "maxRetries": 2,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

# 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://apply.workable.com/huggingface/",
        "recruitee:bunq",
        "https://tibber.teamtailor.com"
    ],
    "maxJobsPerCompany": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("kilndata/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://apply.workable.com/huggingface/",
        "recruitee:bunq",
        "https://tibber.teamtailor.com",
    ],
    "maxJobsPerCompany": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("kilndata/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://apply.workable.com/huggingface/",
    "recruitee:bunq",
    "https://tibber.teamtailor.com"
  ],
  "maxJobsPerCompany": 20
}' |
apify call kilndata/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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