# ATS Jobs API - Greenhouse, Lever, Ashby, Workable | $1.50/1K (`northwestsouth/ats-job-board-scraper`) Actor

Give it company names, not board tokens. Finds each company's job board across 6 applicant tracking systems and returns every opening in one schema - normalised country, remote flag, salary. Pay per job; companies it cannot find are free.

- **URL**: https://apify.com/northwestsouth/ats-job-board-scraper.md
- **Developed by:** [NorthWestSouth Data](https://apify.com/northwestsouth) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 job returneds

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/platform/actors/running/actors-in-store#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 Scraper — company names in, jobs out

Every other job-board actor in this store wants a **board token**: `greenhouse:gitlab`, `lever:spotify`, `ashby:ramp`. That is backwards. You know the company. You do not know which HR software it happens to have bought.

Give this one **company names or domains**. It works out where their job board lives, reads it, and returns every opening in one schema.

```json
{ "companies": ["Celonis", "SumUp", "raisin.com", "Delivery Hero"] }
```

```
Celonis        → greenhouse:celonis          251 jobs
SumUp          → greenhouse:sumup            370 jobs
raisin.com     → greenhouse:raisin            35 jobs
Delivery Hero  → smartrecruiters:deliveryhero 1056 jobs
```

No LinkedIn. No Indeed. No proxies, no browsers, no anti-bot arms race — only the public feeds these platforms publish so companies can embed their own openings on their own careers pages.

### Systems covered

**Found automatically:** Greenhouse · Lever · Ashby · SmartRecruiters · Recruitee · Workable

**On request only:** Personio — see the honest note below.

### How well the discovery actually works

Measured against **50 real DACH companies**, names only, no tokens:

- **29 of 50 (58%) resolved to a live board.** Greenhouse 18, Ashby 4, SmartRecruiters 3, Recruitee 2, Lever 1, Workable 1.
- The other 21 **cost nothing** and are listed with a reason in a separate dataset.

The 21 misses are companies whose board is on a system not covered here (Workday, SuccessFactors, softgarden, d.vinci), or whose board name differs from their brand name. For those, pass the board directly — `greenhouse:acme` always works and skips discovery entirely.

**A board with zero open positions counts as not found.** That is deliberate, and it is the single most important design decision in this actor. SmartRecruiters answers `200 OK` with an empty list for *any* identifier, including invented ones. Workable is full of dormant trial accounts — `zalando` and `flixbus` both resolve there to a real but permanently empty board, while those companies actually hire somewhere else. Accepting an empty board would hand you a confident answer that yields no jobs, stop the search before the real board was found, and cache the wrong pairing for every later run. So empty means absent. The cost is that a company genuinely between postings reads as "not found" — free, honest, and overridable by naming the board yourself.

### What you get per job

| Field | Example |
|---|---|
| `ats`, `company` | `greenhouse`, `celonis` |
| `title` | AI & Management Consulting Intern (Value Engineering – DACH Market) |
| `url`, `apply_url` | Direct links to the posting and the application form |
| `location`, `city`, `region`, `country` | `Munich, Germany` → Munich / **DE** |
| `remote`, `hybrid` | From the platform's own flag where it exists, from the location text otherwise |
| `department`, `team` | Engineering / Platform |
| `employment_type` | Full-time · Part-time · Internship · Working student · Apprenticeship … |
| `posted_at`, `updated_at` | ISO 8601, whatever the source format was |
| `salary_min`, `salary_max`, `salary_currency` | Where the platform publishes structured pay (Ashby, Recruitee) |
| `description_html`, `description_text` | Optional — off by default, because it multiplies the output size |
| `job_uid` | Stable identity for deduplication across runs |

Locations are normalised, so `Berlin`, `München`, `Wien`, `Zürich` and `Remote Germany` all resolve to a real ISO country code — you can filter on `country` instead of writing string matches. Salary is read **only** from structured fields; guessing a number out of prose produces wrong numbers, so it is not attempted.

### Fair billing, by design

- **You pay per job returned.** Nothing else.
- **A company that cannot be located is free.** It goes to the `ATS-COMPANIES-NOT-FOUND` dataset with the reason.
- **A board that is found but unreadable is free.**
- **`maxJobs` is a hard ceiling.** Set it to 500 and you get at most 500 — the cap is claimed before each write, not after, so concurrent workers cannot overshoot it between them.
- **`onlyNewJobs` charges you once per posting, ever.** Jobs returned by an earlier run are skipped. A daily schedule then pays only for genuinely new openings, which is what a job alert should cost.

### The board directory grows as you use it

Every company resolved is remembered in a key-value store. The first run pays a handful of lookup requests; every run after that goes straight to the board. Re-running the same company list is fast and cheap, and the cache is yours.

### Personio: the honest version

Personio is the standard applicant tracking system in the German Mittelstand, so it belongs here. It mostly does not work, and pretending otherwise would waste your money.

Tested against **15 real Personio boards: 3 answered, 10 returned a bot-protection challenge** (a Vercel JavaScript checkpoint that replies `429` to anything without a browser), 2 did not exist. Getting past that would mean a headless browser and residential proxies — slower, far more expensive per job, and an explicit attempt to defeat a block the platform put there on purpose. This actor does not do that.

So Personio is **never guessed**. Name it and it will be tried: `personio:orderbird`, or by selecting it in *atsPlatforms*. If the checkpoint blocks it, you are told exactly that — not "company not found" — and you are not charged.

### Useful setups

**Daily alert for German engineering roles**

```json
{ "companies": ["Celonis", "SumUp", "N26", "HelloFresh", "Enpal"],
  "countries": ["DE"], "titleKeywords": ["engineer", "entwickler"],
  "onlyNewJobs": true }
```

Schedule it. You pay only for postings that appeared since yesterday.

**Which of my target accounts are hiring right now?**

```json
{ "companies": ["forto.com", "sennder.com", "raisin.com"], "maxJobsPerCompany": 5 }
```

**Everything a company has open, with full text**

```json
{ "companies": ["greenhouse:gitlab"], "includeDescription": true }
```

### Why this data is fair to use

Each endpoint is the platform's own public job-board feed — no key, no login, no session. Their purpose is to let a company's openings be republished, which is exactly what happens here. `robots.txt` on these API hosts permits it. One request per company per run, and cached discovery keeps repeat runs to a single call.

Job postings are company data, not personal data. Recruiter names are not collected.

### Limits worth knowing before you run it

- **Static feeds only.** A company running its own JavaScript careers site with no ATS feed cannot be read, and is reported as a miss rather than guessed at.
- **Descriptions cost extra time.** SmartRecruiters needs one additional request per job for them, capped at 200 per run.
- **Coverage is six systems.** Workday and SuccessFactors are the big absentees; they need per-tenant handling and are not here yet. Say so in an issue if you need them.

### Support

A company that should resolve but doesn't, or a field parsed wrongly? [Open an issue](https://console.apify.com/actors) — both count as bugs and get fixed.

# Actor input Schema

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

Company names ("GetYourGuide"), domains ("raisin.com") or an exact board if you know it ("greenhouse:gitlab", "personio:acme"). Names and domains are resolved automatically — you do not need to know which HR system a company uses. Companies that cannot be located cost nothing.

## `atsPlatforms` (type: `array`):

Leave empty to try all seven. Restricting the list makes discovery faster and cheaper when you already know the platform.

## `countries` (type: `array`):

e.g. DE, AT, CH. Locations are normalised, so "Berlin", "München" and "Remote Germany" all resolve to DE. Leave empty for every country.

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

Case-insensitive, e.g. engineer, entwickler, sales. Leave empty for all titles.

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

e.g. intern, praktikum, working student.

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

Matched against both department and team, e.g. engineering, marketing.

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

Uses the platform's own remote flag where it exists, and the location text otherwise.

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

Leave empty for all. Jobs with no publication date are excluded when this is set.

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

For monitoring. Jobs already returned by an earlier run of this actor are skipped and NOT charged again, so a daily schedule only pays for genuinely new postings.

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

Adds description\_html and description\_text. Slower, and much larger output. SmartRecruiters needs one extra request per job for this, capped at 200.

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

Leave empty for all openings.

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

A hard ceiling on the run, and therefore on the cost.

## `discoverViaCareersPage` (type: `boolean`):

ON by default. When token discovery finds nothing and you supplied a domain, the careers page is checked for a board link. Reads static HTML only.

## Actor input object example

```json
{
  "companies": [
    "GetYourGuide",
    "raisin.com",
    "Staffbase",
    "forto",
    "greenhouse:gitlab"
  ],
  "atsPlatforms": [],
  "countries": [],
  "titleKeywords": [],
  "excludeKeywords": [],
  "departments": [],
  "remoteOnly": false,
  "onlyNewJobs": false,
  "includeDescription": false,
  "discoverViaCareersPage": true
}
```

# 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": [
        "GetYourGuide",
        "raisin.com",
        "Staffbase",
        "forto",
        "greenhouse:gitlab"
    ],
    "atsPlatforms": [],
    "countries": [],
    "titleKeywords": [],
    "excludeKeywords": [],
    "departments": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("northwestsouth/ats-job-board-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": [
        "GetYourGuide",
        "raisin.com",
        "Staffbase",
        "forto",
        "greenhouse:gitlab",
    ],
    "atsPlatforms": [],
    "countries": [],
    "titleKeywords": [],
    "excludeKeywords": [],
    "departments": [],
}

# Run the Actor and wait for it to finish
run = client.actor("northwestsouth/ats-job-board-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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": [
    "GetYourGuide",
    "raisin.com",
    "Staffbase",
    "forto",
    "greenhouse:gitlab"
  ],
  "atsPlatforms": [],
  "countries": [],
  "titleKeywords": [],
  "excludeKeywords": [],
  "departments": []
}' |
apify call northwestsouth/ats-job-board-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=northwestsouth/ats-job-board-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/zTG7v7PxVitBkCinz/builds/JohrdUACsFEn1Gu1y/openapi.json
