# ATS Job Board API - Greenhouse, Lever, Ashby & Personio (`theprojectdesk/ats-job-board-api`) Actor

Give it your own list of companies, get every open role across four applicant-tracking systems in one normalised schema. Uses each vendor's official public job-board API - no login, no proxy, no scraping.

- **URL**: https://apify.com/theprojectdesk/ats-job-board-api.md
- **Developed by:** [Project Desk](https://apify.com/theprojectdesk) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 results

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## ATS Job Board API — Greenhouse, Lever, Ashby & Personio

Give it your own list of companies. Get back every open role across four applicant-tracking
systems in one schema, deduplicated where a company posted the same role to two of them.

No login. No proxy. No scraping. Each of these vendors publishes a **public job-board API**
so third parties can embed a customer's careers page — this Actor consumes exactly those
documented endpoints.

***

### Why this exists

Most job Actors sell you a hosted feed of somebody else's target list. If you want *your*
forty companies polled every morning, that isn't what you're buying.

This is the other shape: you supply the companies, it returns their roles. Useful for
recruiters tracking a client's competitors, sales teams reading hiring as a buying signal,
job seekers watching a shortlist, and anyone building a niche job board.

***

### Input

One entry per company, as a careers URL or the short `ats:token` form. Both work, mixed
freely:

```
https://boards.greenhouse.io/gitlab
https://jobs.lever.co/leverdemo
https://jobs.ashbyhq.com/notion
acme.jobs.personio.de
greenhouse:stripe
lever:spotify
```

**Where the token comes from.** It is the company's name in its board URL — the `gitlab` in
`boards.greenhouse.io/gitlab`. If a company's careers page is a JavaScript app that doesn't
show its board URL, open a single job posting; the address bar will show the real board.

| Option | Default | What it does |
|---|---|---|
| `boards` | — | The companies. Required. |
| `includeDescription` | `true` | Full job text. Off gives a fast list of titles and links. |
| `deduplicate` | `true` | Merge a role cross-posted to two boards, keeping the fuller copy. |
| `remoteOnly` | `false` | Keep only remote roles. |
| `titleFilter` | — | Case-insensitive substring on the title. |
| `maxJobs` | `0` | Cap the rows. 0 is no cap. If the cap bites, the log says by how much. |
| `maxConcurrency` | `8` | Boards fetched at once. |

***

### Output

One row per role, the same seventeen fields whatever the source:

| Field | Notes |
|---|---|
| `ats` | `greenhouse`, `lever`, `ashby`, `personio` |
| `company`, `board_token`, `job_id` | `job_id` is the vendor's own and is stable across runs — the right key for diffing new and closed roles |
| `title`, `location`, `all_locations` | Location exactly as the company wrote it, never re-mapped |
| `remote` | One boolean, derived — the four vendors each say it differently |
| `department`, `team` | |
| `employment_type`, `workplace_type` | Vendor wording preserved rather than normalised into a guess |
| `apply_url` | The public posting on the company's own board |
| `posted_at`, `updated_at` | ISO 8601 where the board provides it, empty where it doesn't. Never invented |
| `description`, `description_chars` | Plain text, HTML stripped. The char count is a cheap way to spot stub postings |

***

### Two things worth knowing before you buy

**Deduplication is deliberately conservative.** It merges a role only when it appears on
**two different boards**. Two roles on the *same* board that share a title and location are
kept as two rows — a company hiring three Support Engineers in London is not a bug to be
cleaned up, and an earlier, cleverer version of this merged "AI Engineer, Bangalore" into
"Senior AI Engineer, US". Under-merging shows you one extra row; over-merging silently
destroys a real vacancy. This errs toward the harmless mistake.

**SmartRecruiters and Workable are not supported, on purpose.** Both publish a public
postings endpoint, and both currently return HTTP 200 with a valid but permanently empty
response for every company tested (SmartRecruiters: eight companies, all zero results;
Workable: nine accounts, all empty). Rather than ship adapters that always return nothing,
they are left out until one demonstrably returns a job.

***

### Coverage

Verified live on 27 August 2026, plain unauthenticated requests:

| ATS | Endpoint | Test board | Result |
|---|---|---|---|
| Greenhouse | `boards-api.greenhouse.io/v1/boards/{token}/jobs?content=true` | `gitlab` | 218 jobs |
| Lever | `api.lever.co/v0/postings/{token}?mode=json` | `leverdemo` | 384 jobs |
| Ashby | `api.ashbyhq.com/posting-api/job-board/{token}` | `notion` | 135 jobs |
| Personio | `{token}.jobs.personio.de/xml` | `demo` | XML, live |

741 rows in one run across the four, 712 of them carrying a real description.

Ashby roles marked `isListed: false` are skipped — the company has taken those off its
public board, and republishing them would be wrong.

***

### Run it locally

No Apify account, no Docker:

```
python tools/try_local.py greenhouse:gitlab lever:leverdemo https://jobs.ashbyhq.com/notion
```

Writes `tools/out/local-run.json` and prints a summary. Same code path as the Actor — only
the Apify wiring is replaced.

Tests, stdlib only:

```
python tests/test_ats.py
```

79 assertions, including a regression block guarding the dedupe behaviour described above,
and fixtures trimmed from real vendor responses so a shape change surfaces here rather than
in production.

***

### Pricing

Pay per result. You are charged per job row pushed, and nothing else — no platform fee, no
charge for a board that returns empty. Turning `includeDescription` off does not reduce the
row count and so does not reduce the price; it just makes runs faster and the output smaller.

# Actor input Schema

## `boards` (type: `array`):

One entry per company. Paste a careers URL (https://jobs.lever.co/leverdemo, https://boards.greenhouse.io/gitlab, https://jobs.ashbyhq.com/notion, acme.jobs.personio.de) or use the short form `ats:token` such as `greenhouse:gitlab`. Anything unreadable is reported in the log rather than dropped in silence.

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

The description is the bulk of each row. Turn it off for a fast list of titles, locations and apply links.

## `deduplicate` (type: `boolean`):

When the same company posts one role to two different systems, keep the copy with the fuller description. Never merges two roles from the SAME board - those are two real vacancies.

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

Keep only roles whose location or workplace type says remote, anywhere, or work-from-home.

## `titleFilter` (type: `string`):

Case-insensitive substring match on the job title. `engineer` keeps Senior Engineer and Engineering Manager. Leave empty for everything.

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

Stop after this many rows. 0 means no limit. If the cap is hit, the log says how many rows were left behind.

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

How many boards to fetch at once. These are vendor APIs built for this, so the default is comfortable.

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

Not needed. These are public APIs with no rate-limiting worth defeating; the option is here only for users whose network requires an egress proxy.

## Actor input object example

```json
{
  "boards": [
    "greenhouse:gitlab",
    "lever:leverdemo",
    "https://jobs.ashbyhq.com/notion"
  ],
  "includeDescription": true,
  "deduplicate": true,
  "remoteOnly": false,
  "titleFilter": "",
  "maxJobs": 0,
  "maxConcurrency": 8,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Every role as JSON.

## `jobsCsv` (type: `string`):

The same rows as a spreadsheet.

# 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 = {
    "boards": [
        "greenhouse:gitlab",
        "lever:leverdemo",
        "https://jobs.ashbyhq.com/notion"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("theprojectdesk/ats-job-board-api").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 = { "boards": [
        "greenhouse:gitlab",
        "lever:leverdemo",
        "https://jobs.ashbyhq.com/notion",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("theprojectdesk/ats-job-board-api").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 '{
  "boards": [
    "greenhouse:gitlab",
    "lever:leverdemo",
    "https://jobs.ashbyhq.com/notion"
  ]
}' |
apify call theprojectdesk/ats-job-board-api --silent --output-dataset

```

## MCP server setup

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

```

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/tc9fwR8PhcprfwD8V/builds/r0ot0V8oY4ghgStEw/openapi.json
