# ATS Jobs Scraper: Greenhouse, Lever, Ashby (`succinct_glider/ats-job-boards`) Actor

Scrape job postings from Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor. Enter company domains, the ATS is found for you. Get titles, locations, salaries and departments, track new and closed jobs, export to CSV, JSON or Excel.

- **URL**: https://apify.com/succinct\_glider/ats-job-boards.md
- **Developed by:** [Alexandre Bobichon](https://apify.com/succinct_glider) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 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.

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 job postings from Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor** with one Actor.
Give it a list of **company domains** and get back **every job those companies have published**, in one
consistent format — titles, locations, departments, salaries when published, and **new, changed and closed
jobs** since your last run.

Try it with the prefilled input: three companies, three different ATS, results in a few seconds.

### What does this ATS jobs scraper do?

Most job scrapers ask you for an ATS board slug: `greenhouse:aircallioinc`, `lever:contentsquare`. Finding
those by hand for a list of a hundred companies is an afternoon of work, because **the slug is often nothing
like the company name** — `aircall.io` is `aircallioinc` on Greenhouse, `epidemicsound.com` is
`epidemic-sound` on Ashby.

This Actor takes `aircall.io` and works the rest out for you. It reads the company's careers page, finds the
job board behind it, and falls back to probing the six supported ATS APIs. Then it returns every published
job in a single schema, whichever ATS it came from.

It reads the **official public job board APIs** that these ATS provide for syndication — the same endpoints
that power company careers pages and job aggregators. No browser, no login, no proxy needed.

Run it on a schedule and turn on change tracking to get **new, changed and closed jobs** since your last run.

#### Supported ATS and job board URLs

| ATS        | Job board URLs it reads                                   | Example input                         |
| ---------- | --------------------------------------------------------- | ------------------------------------- |
| Greenhouse | `boards.greenhouse.io/{slug}`, `job-boards.greenhouse.io` | `greenhouse:stripe`                   |
| Lever      | `jobs.lever.co/{slug}`                                    | `https://jobs.lever.co/contentsquare` |
| Ashby      | `jobs.ashbyhq.com/{slug}`                                 | `ashby:linear`                        |
| Workable   | `apply.workable.com/{slug}`                               | `workable:blueground`                 |
| Personio   | `{slug}.jobs.personio.de`                                 | `personio:urbansportsclub`            |
| Teamtailor | `{slug}.teamtailor.com` and custom careers domains        | `teamtailor:payfit`                   |

Or just the company domain — `stripe.com`, `linear.app` — and the Actor finds the board.

### Why scrape jobs from company career pages?

- **Hiring-intent signals for B2B sales.** A company that starts hiring five sales engineers is a company
  with a budget. Track your target accounts and get the new roles the day they appear.
- **Portfolio monitoring.** Funds and accelerators watch headcount plans across their portfolio without
  asking anyone for a spreadsheet.
- **Recruiting and talent mapping.** See who is hiring for a role, in which locations, at what salary when
  the employer publishes one.
- **Building a job board.** One normalised feed from six ATS, with stable IDs to deduplicate against.
- **Competitive intelligence.** Roles closing can say as much as roles opening.

### How to scrape Greenhouse, Lever and Ashby jobs

1. Put your companies in the **Companies** field, one per line. A domain (`stripe.com`) is enough.
2. Leave **Max jobs** at its default for a first look, then raise it.
3. Click **Start** and check the Output tab.
4. Once you are happy with the result, open **Schedules**, run it daily, and switch on **Track changes** so
   each run tells you what moved.

#### Three ways to name a company

| Form           | Example                               | When to use it                                            |
| -------------- | ------------------------------------- | --------------------------------------------------------- |
| Company domain | `stripe.com`                          | The normal case. The Actor finds the ATS.                 |
| Job board URL  | `https://jobs.lever.co/contentsquare` | You already have the board link.                          |
| ATS and slug   | `workable:blueground`                 | Fastest and exact. Use it when a domain does not resolve. |

### How well does domain resolution work?

Measured on a deliberately mixed benchmark of 50 companies: **40 resolved, 80 %.** The rate depends a lot on
the kind of company:

| Kind of company    | Resolved |
| ------------------ | -------- |
| European scale-ups | 10/10    |
| US tech            | 9/10     |
| UK and Ireland     | 6/6      |
| DACH               | 6/8      |
| Nordics            | 4/6      |
| Large enterprises  | 5/10     |

The resolver reads the company's careers page (including `careers.` and `jobs.` subdomains), then tries
the known ATS APIs with the domain name and a few common variants of it: `datadoghq.com` is found as
`greenhouse:datadog`, `starlingbank.com` as `workable:starling-bank`.

**Large companies that run their own careers site are the weak spot.** Shopify, Atlassian, Klarna, Revolut
and Bolt could not be resolved from their domain, because nothing on their careers page points at a
supported ATS, and several use an ATS this Actor does not read. They are not a failure of the run: the summary lists them as `unresolved`, and you can add
them with the `ats:slug` form once you know the slug.

Resolutions are cached for 30 days in your state store, so a scheduled run pays this cost only once per
company.

### Input

| Field                   | Type    | Default                | What it does                                                                      |
| ----------------------- | ------- | ---------------------- | --------------------------------------------------------------------------------- |
| `companies`             | array   | —                      | One company per line: a domain, a board URL or `ats:slug`.                        |
| `maxCompanies`          | integer | 50                     | Companies processed in one run.                                                   |
| `maxItems`              | integer | 200                    | Jobs saved in one run. The crawl stops when it is reached.                        |
| `maxJobsPerCompany`     | integer | 0                      | Cap per company so one large board cannot use the whole budget. 0 means no limit. |
| `titleKeywords`         | array   | —                      | Keep only jobs whose title contains one of these.                                 |
| `locationKeywords`      | array   | —                      | Keep only jobs whose location contains one of these.                              |
| `remoteOnly`            | boolean | false                  | Keep only jobs the ATS flags as remote.                                           |
| `publishedAfter`        | string  | —                      | Keep only jobs published on or after this date.                                   |
| `includeDescription`    | boolean | false                  | Add the job description. See the FAQ.                                             |
| `descriptionFormat`     | select  | text                   | Plain text, HTML, or both.                                                        |
| `includeSourceSpecific` | boolean | false                  | Add a `sourceSpecific` object with each ATS's extra fields.                       |
| `trackChanges`          | boolean | false                  | Compare with the previous run.                                                    |
| `onlyChanges`           | boolean | false                  | Save only new, changed and closed jobs.                                           |
| `stateStoreName`        | string  | `ats-job-boards-state` | Key-value store holding the state. One name per list you track.                   |
| `resolveDomains`        | boolean | true                   | Turn off to accept only board URLs and `ats:slug`.                                |

**Filters are applied before saving**, so a job you filtered out is never charged.

```json
{
    "companies": ["stripe.com", "aircall.io", "https://jobs.lever.co/contentsquare"],
    "maxItems": 500,
    "titleKeywords": ["engineer", "developer"],
    "remoteOnly": true,
    "trackChanges": true
}
```

### Output

One item per job. Download as JSON, CSV, Excel or XML, or read it from the API.

```json
{
    "jobId": "186545F8C1",
    "globalId": "workable:blueground:186545F8C1",
    "title": "Client Experience Coordinator (English & Greek Speaker)",
    "url": "https://apply.workable.com/j/186545F8C1",
    "applyUrl": "https://apply.workable.com/j/186545F8C1/apply",
    "companyName": "Blueground",
    "companySlug": "blueground",
    "ats": "workable",
    "department": "Shared Services",
    "employmentType": "full_time",
    "seniority": "Associate",
    "isRemote": false,
    "location": "Athens, Attica, Greece",
    "city": "Athens",
    "country": "Greece",
    "countryCode": "GR",
    "publishedAt": "2026-02-12T00:00:00.000Z",
    "boardUrl": "https://apply.workable.com/blueground",
    "resolvedFrom": "input-slug",
    "scrapedAt": "2026-09-16T11:30:25.864Z"
}
```

#### Fields

| Field                                                                              | Description                                                                                                                              |
| ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `jobId`, `globalId`                                                                | The ATS job ID, and `ats:company:job` — stable across runs, use it to deduplicate.                                                       |
| `title`, `url`, `applyUrl`                                                         | The job, its public page and its application form.                                                                                       |
| `companyName`, `companySlug`, `companyDomain`, `ats`, `boardUrl`                   | Which company, on which ATS.                                                                                                             |
| `resolvedFrom`                                                                     | How the board was found: `input-slug`, `input-url`, `careers-page`, `api-probe` or `cache`.                                              |
| `department`, `team`, `seniority`                                                  | As published. Not every ATS provides them.                                                                                               |
| `employmentType`                                                                   | `full_time`, `part_time`, `contract`, `temporary`, `internship`, `volunteer` or `other`. `employmentTypeRaw` keeps the original wording. |
| `workplaceType`, `isRemote`                                                        | `remote`, `hybrid` or `onsite`, plus a plain boolean.                                                                                    |
| `location`, `city`, `region`, `country`, `countryCode`, `locations`                | A readable label, structured parts when the ATS gives them, and every location of a multi-site job.                                      |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryRaw`            | Only when the employer publishes a range. `salaryRaw` keeps it exactly as written.                                                       |
| `publishedAt`, `updatedAt`                                                         | ISO 8601.                                                                                                                                |
| `description`, `descriptionHtml`                                                   | Off by default.                                                                                                                          |
| `sourceSpecific`                                                                   | Fields peculiar to one ATS, when you ask for them.                                                                                       |
| `changeStatus`, `changedFields`, `previousValues`, `previousScrapedAt`, `closedAt` | Change tracking.                                                                                                                         |
| `scrapedAt`                                                                        | When this run read the board.                                                                                                            |

A field the ATS does not publish is `null`, never missing. Field names and types do not change outside a
major version.

### Tracking changes

Turn on **Track changes** and every job gets a `changeStatus`:

- `new` — not on the board last run.
- `changed` — `changedFields` lists what moved and `previousValues` holds the old values.
- `unchanged` — still there, identical.
- `closed` — it was on the board last run and is gone now. Returned as an item, rebuilt from the saved
  state, so you can act on it even though the ATS no longer serves it.

Add **Only save new, changed and closed jobs** to skip the `unchanged` ones, so you are not charged to be
told nothing happened.

Two things worth knowing:

- **The first run saves everything**, because nothing is known yet.
- **A run that stops early picks up where it stopped.** If `maxItems`, `maxJobsPerCompany` or your cost limit
  cuts a board in half, the jobs it never reached are not reported as closed, and with `onlyChanges` the next
  run moves on to them instead of charging you again for the ones already saved.

Use a different `stateStoreName` for each list you track independently.

### Use it from the API, integrations or an AI agent

Every run is available through the [Apify API](https://docs.apify.com/api/v2), so this Actor works as a
**job postings API** for your own code. Run it and get the jobs back in a single call:

```bash
curl -X POST "https://api.apify.com/v2/acts/succinct_glider~ats-job-boards/run-sync-get-dataset-items?token=<YOUR_TOKEN>"   -H "Content-Type: application/json"   -d '{"companies": ["stripe.com", "linear.app"], "maxItems": 100}'
```

- **Schedule it** in Apify and send new jobs to Google Sheets, Slack, a webhook or your CRM through the
  built-in integrations, Make, Zapier or n8n.
- **Give it to an AI agent**: through the [Apify MCP server](https://mcp.apify.com), agents can call this
  Actor to look up who a company is hiring.

### Pricing

**$0.0015 per job saved. No start fee**, so a run that finds nothing costs nothing.

You only pay for jobs that are actually saved: filtered-out jobs and, with `onlyChanges`, unchanged jobs are
not charged. Monitoring 50 companies daily with `onlyChanges` typically costs a few cents a day, because
most days only a handful of jobs move.

Set a **maximum cost per run** in the run options and the Actor stops cleanly when it is reached.

### Tips

- **Use `onlyChanges` on a schedule.** It is the cheapest way to monitor a large list.
- **Set `maxJobsPerCompany`** when your list mixes small companies with one that has hundreds of openings.
- **Leave descriptions off** unless you need them. They make runs slower and heavier, and change nothing
  about what you pay.
- **Use `ats:slug` for companies that do not resolve.** Open their careers page, look at the URL of the job
  board, and take the slug from it.
- **Use `titleKeywords` and `locationKeywords`** rather than filtering afterwards: filtered jobs are free.

### FAQ

**Where does the data come from?**
The official public job board APIs of each ATS — the endpoints they publish so that job postings can be
syndicated to careers pages and aggregators. Greenhouse documents that "Job Board data is publicly
available, so authentication is not required for any GET endpoints"; Lever states that all postings in the
`published` state are publicly viewable; Ashby documents a public job posting API. No account, no key, no
scraping of pages behind a login.

**Is this legal?**
The Actor reads public job postings that employers publish for redistribution, one company at a time, at
about one request per second. It collects **no personal data**: no recruiter names, no emails, no phone
numbers. You are responsible for how you use the output, in particular the job descriptions if you enable
them — they are the employer's own content, and Workable and Teamtailor signal that their data should not be
used to train models.

**Which ATS are supported?**
Greenhouse, Lever, Ashby, Workable, Personio and Teamtailor. Recruitee is planned. Workday is not supported:
its job endpoint is undocumented and internal.

**A company came back with no jobs. Is it broken?**
Check the run summary. `empty` means the board was read and the company has nothing published right now.
`not_found` means there is no board at that slug, and `unresolved` means the domain could not be matched to
an ATS. These are situations, not failures, and the run succeeds.

**One ATS was down. Did I lose the whole run?**
No. Each company is read independently. A failure is recorded against that company and the others still
produce results. The run only fails if no board at all could be read, or if the data looks structurally
wrong.

**My company has two job boards.**
It happens after migrating ATS, when the old board is left up. The Actor keeps the one with the most recent
posting, reports the company as `ambiguous` in the summary, and lists the other candidates. Use `ats:slug`
to force a specific one.

### Support

Found a bug, or an ATS you would like supported? Open an issue on the **Issues** tab and I will look at it.
Include the input you used and the company that misbehaved.

# Changelog

This Actor's version history is a separate document: https://apify.com/succinct\_glider/ats-job-boards/changelog.md

# Actor input Schema

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

One company per line. Three accepted forms: a company domain (stripe.com) which is resolved to its ATS automatically, a job board URL (https://jobs.lever.co/contentsquare), or an explicit ATS and slug (workable:blueground). Supported ATS: greenhouse, lever, ashby, workable, personio, teamtailor.

## `maxCompanies` (type: `integer`):

Maximum number of companies processed in one run. Extra entries are ignored and listed in the run summary.

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

Maximum number of job postings saved in this run. The crawl stops as soon as it is reached.

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

Limit per company, so one large board cannot use the whole budget. 0 means no limit.

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

Keep only jobs whose title contains one of these texts (case-insensitive). Filtering happens before saving, so filtered-out jobs are never charged.

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

Keep only jobs whose location contains one of these texts (case-insensitive), e.g. Berlin, France, Remote.

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

Keep only jobs flagged as remote by the ATS. Jobs with no remote information are excluded.

## `publishedAfter` (type: `string`):

Keep only jobs published on or after this date (ISO 8601, e.g. 2026-01-01). Jobs with no publication date are kept.

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

Job descriptions are the employer's own content and can contain personal data. Off by default; enabling it also makes runs slower and heavier.

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

Used only when descriptions are included.

## `includeSourceSpecific` (type: `boolean`):

Add a sourceSpecific object with the extra fields of each ATS (Greenhouse offices and departments, Workable industry and function, Personio occupation category...).

## `trackChanges` (type: `boolean`):

Compare each board with the previous tracked run and add changeStatus (new, changed, unchanged, closed), changedFields and previousValues. Closed jobs are returned as items. Ideal with a schedule. The state is kept in a key-value store of your Apify account.

## `onlyChanges` (type: `boolean`):

Skip unchanged jobs: they are not saved, so you are not charged for them. Enables change tracking automatically. The very first run saves everything, because nothing is known yet.

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

Key-value store that keeps the previous state and the resolved domains. Use a different name for each independent list you track. Letters, digits and hyphens only.

## `resolveDomains` (type: `boolean`):

Find the ATS and board slug of a plain domain by reading its careers page and probing the known job board APIs. Disable it to accept only board URLs and explicit slugs.

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

Optional, not needed by default. All sources are public JSON or XML APIs with no anti-bot protection.

## Actor input object example

```json
{
  "companies": [
    "linear.app",
    "https://jobs.lever.co/contentsquare",
    "workable:blueground"
  ],
  "maxCompanies": 50,
  "maxItems": 200,
  "maxJobsPerCompany": 0,
  "remoteOnly": false,
  "includeDescription": false,
  "descriptionFormat": "text",
  "includeSourceSpecific": false,
  "trackChanges": false,
  "onlyChanges": false,
  "stateStoreName": "ats-job-boards-state",
  "resolveDomains": true,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

No description

## `summary` (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": [
        "linear.app",
        "https://jobs.lever.co/contentsquare",
        "workable:blueground"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("succinct_glider/ats-job-boards").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": [
        "linear.app",
        "https://jobs.lever.co/contentsquare",
        "workable:blueground",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("succinct_glider/ats-job-boards").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": [
    "linear.app",
    "https://jobs.lever.co/contentsquare",
    "workable:blueground"
  ]
}' |
apify call succinct_glider/ats-job-boards --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,succinct_glider/ats-job-boards"
        }
    }
}
```

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/h5NblP1ho09I502Na/builds/eNRnUvdXDRPzuba6H/openapi.json
