# ATS Jobs Scraper: Company Job Boards by Domain (`glitchbound/ats-jobs-scraper`) Actor

Give it a company domain and it finds which hiring system that company runs, then reads the board: Greenhouse, Lever, Ashby, Workable, Recruitee, Rippling, Workday, Personio and SmartRecruiters. Titles, locations, departments, salary where published. No API key, no proxy, no board token to look up.

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

## Pricing

from $1.50 / 1,000 job postings

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/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 Jobs Scraper: Company Job Boards by Domain

**Give it a domain. That is all.**

```json
{ "companies": ["stripe.com", "figma.com", "openai.com"] }
```

No API key. No proxy. No board tokens to look up. No blocked requests.

### Why this returns data when job scrapers return 403

Indeed, Glassdoor, ZipRecruiter and LinkedIn Jobs do not originate job data.
They aggregate it from company career pages, and those pages run on a handful
of applicant tracking systems that each publish a **free, keyless, public JSON
API**, because companies want their openings indexed.

So the aggregators are behind Cloudflare while the actual source is wide open.
Measured from a residential address on 2026-08-01:

| Aggregator | | Source |
|---|---|---|
| `indeed.com/jobs` | **403**, zero bytes | `greenhouse` / stripe → **548 jobs** |
| `glassdoor.com` | **403**, zero bytes | `ashby` / openai → **753 jobs** |
| `ziprecruiter.com` | **403**, zero bytes | `lever` / palantir → **302 jobs** |
| `upwork.com` | **403**, zero bytes | `rippling` / rippling → **768 jobs** |

Going to the source is also **fresher**. An aggregator shows you its last crawl.
This shows you the board.

### You do not need to know the board token

Every other ATS Actor asks you for one. You cannot use the Greenhouse API
without knowing that Figma is `figma` and Databricks is `databricks`, and there
is no directory to look that up in.

This Actor derives candidates from the domain and **verifies each one against
the live API**. That works because a board that does not exist answers `404`
while a board with nothing open answers `200` with an empty list, so a `200` is
proof rather than a maybe.

Measured on 30 companies:

| Method | Resolved |
|---|---|
| Reading the careers page for an ATS link | 50% |
| **Deriving the token and verifying it** | **90%** |

Reading the page fails on `stripe.com` because its careers page is a JavaScript
app that names no board. Deriving and verifying finds Stripe's 548 open roles.

### One shape, seven platforms

`greenhouse`, `lever`, `ashby`, `workable`, `recruitee`, `rippling`,
`smartrecruiters`. Each names the same five things differently. Every row comes
back the same:

| Field | |
|---|---|
| `title`, `location`, `url` | |
| `departments`, `offices` | |
| `company`, `atsPlatform`, `boardToken`, `boardUrl` | which system, which board |
| `postedAt`, `updatedAt` | |
| `description` | opt in, see below |
| `employmentType`, `isRemote`, `compensation` | where the platform provides it |

### Filters that do not cost you anything

```json
{
  "companies": ["stripe.com"],
  "titleKeywords": ["security"],
  "remoteOnly": true
}
```

Filtered rows are **not charged**. Keywords are matched as plain text, so `c++`
and `node.js` mean exactly that rather than being read as regular expressions.

### Two limits worth knowing before you run it

**`includeDescription` is off by default, and that is not laziness.** It is
expensive at the source: Greenhouse returns 111 KB for a board without
descriptions and 1.97 MB with them, an 18x difference for the same jobs. Turn it
on when you want the text.

**Rippling's board API returns no description at all**, at any setting. Five
fields per job and nothing more. That is the source's limit, and rows from it
simply carry no `description` rather than an empty one.

### Where a wrong answer would be easy, this says so instead

A wrong board token gets an explanation, not an empty dataset:

```
greenhouse has no board 'zzznotarealzzz'. The API answers 404 for a board that
does not exist and 200 for one that exists but has nothing open, so this is
genuinely the wrong token, not an empty board.
```

**SmartRecruiters is the one place this is not possible, and it is labelled.**
It answers `200 {"totalFound":0,"content":[]}` both for a company id that does
not exist and for one with no open roles, and `/v1/companies/<id>` returns 404
even for Visa, whose postings endpoint returns real jobs. There is no way to
tell the two apart, so an empty SmartRecruiters result says exactly that rather
than reporting a confident zero. Token guessing is disabled for that platform
for the same reason.

### Who this is for

- **Recruiters and sourcers**: watch a list of target companies without a seat
  in anyone's tool
- **Job seekers**: the roles, from the company, before the aggregator crawls
- **Sales and BD**: hiring is the clearest public signal of where budget went
- **Market research**: headcount direction by team, by company, over time
- **LLM pipelines**: clean JSON, no HTML parsing, no key to rotate

### Notes

- Sources are public APIs that companies publish deliberately so their openings
  get indexed. Nothing here is behind a login, a paywall or a bot wall.
- An empty board is a real answer and is reported as one: the company is on that
  platform and is not hiring right now.
- Three of the thirty companies tested run an ATS this Actor does not yet cover.
  Pass `boards` directly if you know the platform and token.
- Errors are per company. One unresolvable domain does not end the run, and
  error rows are not charged.

### The nine systems

| System | Found from a domain? | Notable |
|---|---|---|
| Greenhouse | yes | the biggest startup boards, `databricks` 803 roles |
| Lever | yes | answers with a bare JSON array, unlike every other one |
| Ashby | yes | the only one with a salary band, behind a parameter |
| Workable | yes | European mid-market, many small boards |
| Recruitee | yes | per-subdomain, so the domain usually is the token |
| Rippling | yes | `rippling` alone is 768 roles, and publishes no descriptions |
| **Workday** | **no** | the biggest boards anywhere: NVIDIA 2,000, Salesforce 1,478. Takes its careers URL, because the address carries three unknowns a domain cannot supply |
| **Personio** | **no** | structured salary: min, max, currency and period as separate fields. A bogus subdomain answers 429, so a guess cannot be confirmed |
| SmartRecruiters | **no** | answers the same empty response for an unknown company and for one with nothing open |

The three marked "no" are not missing features. On the other six a bogus board
answers 404, which is what makes a derived token provable. On these three it
does not, so they ask for an exact board rather than inventing an answer.

### Watching, rather than reading

If you are running this on a schedule to see what is new, **Job Change Monitor**
is the Actor for that. It reports only the roles that appeared and the roles
that disappeared since its last run, so a daily check on Stripe returns the
four new openings rather than the 548 that were already there.

A search of the Apify Store on 2026-08-01 found no other Actor that does this
for jobs. Its first run records a baseline and is not charged.

# Actor input Schema

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

Company domains or names, one per line, e.g. stripe.com. The Actor works out which applicant tracking system each one runs and reads its board. You do not need to know that Figma's Greenhouse token is "figma": it derives candidates from the domain and verifies them against the live API, so a match is confirmed rather than guessed. Discovery covers the six systems where a bogus board answers 404, which is what makes a match provable. Workday, Personio and SmartRecruiters cannot be found from a domain and take an exact board instead; the Actor says so rather than returning a confident zero.

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

Skip discovery and read a board directly, as platform:token, one per line: greenhouse:stripe, ashby:openai, lever:palantir, recruitee:bunq, workable:zego, rippling:rippling, personio:alasco. Workday takes its careers URL instead of a token, because a Workday address carries three things a domain cannot supply: workday:https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite. A bare value is treated as a Greenhouse token.

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

Keep only roles whose title contains any of these, one per line, e.g. security or staff engineer. Matched as plain text, case-insensitive, so c++ and node.js mean exactly that. Filtered rows are not charged.

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

Keep only roles whose location contains any of these, one per line, e.g. London or Israel. Filtered rows are not charged.

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

Keep only roles flagged remote by the ATS, or whose title or location says remote or anywhere.

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

Fetch the description text as well. Off by default because it is expensive at the source rather than here: Greenhouse returns 111 KB for a board without it and 1.97 MB with, an 18x difference for the same jobs. Rippling's board API returns no description at any setting.

## `maxResultsPerCompany` (type: `integer`):

Ceiling on roles taken from any single board.

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

A hard ceiling on rows for the entire run, across every company. The per-company limit above caps each one separately, so fifty companies at a thousand each is still fifty thousand rows; this caps the total. The run stops cleanly when it is reached and nothing beyond that point is fetched or charged. Leave empty for no ceiling.

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

Proxy settings. Every source here is a public API a company publishes on purpose so its openings get indexed, and none of them block datacenter addresses, so the default is fine.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "figma.com"
  ],
  "remoteOnly": false,
  "includeDescription": false,
  "maxResultsPerCompany": 1000,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

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

Every matching open role, one per row, in the run's default dataset.

# 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": [
        "stripe.com",
        "figma.com"
    ],
    "maxResultsPerCompany": 1000
};

// Run the Actor and wait for it to finish
const run = await client.actor("glitchbound/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": [
        "stripe.com",
        "figma.com",
    ],
    "maxResultsPerCompany": 1000,
}

# Run the Actor and wait for it to finish
run = client.actor("glitchbound/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": [
    "stripe.com",
    "figma.com"
  ],
  "maxResultsPerCompany": 1000
}' |
apify call glitchbound/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

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