# ATS Job Boards — Greenhouse, Lever, Ashby, SmartRecruiters (`canonrow/ats-job-board-scraper`) Actor

Give it company domains. It finds which ATS each company uses, pulls every open role from the ATS's own public job-board endpoint, and returns them all in one normalised schema.

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

## Pricing

from $4.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.

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 Board Scraper — Greenhouse, Lever, Ashby, SmartRecruiters

Give it a list of **company domains**. It works out which applicant tracking system each company uses, pulls every open role straight from that ATS's own public job-board endpoint, and hands you all of it in **one normalised schema**.

No login. No headless browser. No proxies. No rate-limit roulette — these endpoints are the same ones the companies use to publish their own careers pages.

### Why you'd use this

Job data on the big aggregators is stale, deduplicated badly, and missing the direct apply link. Company ATS boards are the **source** those aggregators copy from — fresher, complete, and with a real `applyUrl` for every row.

The awkward part has always been that you need to know a company uses Greenhouse under the token `stripe`, or Ashby under `ramp`. **This Actor works that out for you**: it reads the company's own careers page for a board link, and if the page renders its board in JavaScript, it falls back to asking each ATS directly whether a board exists under that company name.

```
ramp.com    → ashby:ramp        138 roles
linear.app  → ashby:Linear       32 roles
vanta.com   → ashby:vanta        (found by probe — careers page is JS-rendered)
```

### Input

```json
{
  "companies": ["ramp.com", "linear.app", "vanta.com"],
  "boards": ["greenhouse:stripe", "jobs.lever.co/palantir", "careers.smartrecruiters.com/Visa"],
  "titleFilter": "engineer",
  "onlyRemote": false,
  "postedWithinDays": 0,
  "includeDescription": false
}
```

- **`companies`** — plain domains. ATS and board token are detected automatically.
- **`boards`** — if you already know the board, pass the URL or the shorthand `greenhouse:stripe`. Both inputs can be used together; duplicates are collapsed.
- **`titleFilter`** — case-insensitive regular expression, e.g. `engineer|developer|scientist`.
- **`onlyRemote`** — uses the ATS's own remote flag where it exists (`workplaceType` on Lever, `isRemote` on Ashby) rather than guessing from the location string.
- **`postedWithinDays`** — keeps only roles published or updated inside the window.
- **`includeDescription`** — adds the full job description as plain text. Slower, and on SmartRecruiters it costs one extra request per role, so it is off by default.

### Output

Every row is the same shape, whichever ATS it came from:

```json
{
  "ats": "lever",
  "company": "palantir",
  "jobId": "655f9937-a4ce-4e7d-80e2-a6659af07329",
  "title": "Software Engineer",
  "department": "Engineering",
  "team": "Engineering",
  "employmentType": "Full-time",
  "location": "London, United Kingdom",
  "locations": ["London, United Kingdom"],
  "isRemote": false,
  "postedAt": "2026-07-02T10:14:00.000Z",
  "applyUrl": "https://jobs.lever.co/palantir/655f9937-.../apply",
  "compensation": null,
  "descriptionText": null,
  "scrapedAt": "2026-08-19T16:44:02.113Z"
}
```

A `SUMMARY` record in the key-value store lists how many roles each board returned, plus every company it could **not** resolve and why — so a partial run is never silently reported as a complete one.

### Supported systems

| ATS | Detected from | Notes |
|---|---|---|
| Greenhouse | `boards.greenhouse.io/{token}`, embed URLs | full board in one request |
| Lever | `jobs.lever.co/{slug}` | includes `workplaceType` and salary range when the company publishes it |
| Ashby | `jobs.ashbyhq.com/{slug}` | includes compensation summary and secondary locations |
| SmartRecruiters | `careers.smartrecruiters.com/{id}` | paginated automatically |

### Pricing

Pay per job listing delivered — after filtering. A company whose board returns nothing costs you nothing.

### Honest limits

- Companies on ATS platforms not listed above (Workday, Taleo, iCIMS, BambooHR) will not be found. They are reported in `SUMMARY.failures` rather than silently dropped.
- Discovery reads the careers page and then probes by company name. A company whose board token bears no relation to its domain (an acquired brand, say) needs its board URL passed in `boards`.
- `postedWithinDays` drops roles whose ATS publishes no date at all, because there is no honest way to tell whether they fall inside the window.

# Actor input Schema

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

Plain company domains, e.g. ramp.com. The Actor opens each company's own careers page to work out which applicant tracking system it runs and under which board token — you don't need to know any of that in advance.

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

Pass a board directly if you already know it: boards.greenhouse.io/stripe, jobs.lever.co/palantir, jobs.ashbyhq.com/ramp, careers.smartrecruiters.com/Visa — or the shorthand greenhouse:stripe. Can be combined with the field above; duplicates are collapsed.

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

Fetch each role's full description as plain text. Slower, and on SmartRecruiters it costs one extra request per role, so it is off by default.

## `onlyRemote` (type: `boolean`):

Keep only roles marked as remote. Uses the ATS's own remote flag where it exists (workplaceType on Lever, isRemote on Ashby) rather than guessing from the location text.

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

Case-insensitive regular expression, e.g. engineer|developer|scientist. Leave empty to keep every role.

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

Keep only roles published or updated inside this window. Roles whose ATS publishes no date at all are dropped when this is set, because there is no honest way to tell whether they fall inside it.

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

Cap how many roles are returned per board. Useful for keeping costs predictable while you try the Actor out.

## `concurrency` (type: `integer`):

How many boards to fetch at once. Higher is faster but less polite to the ATS vendors.

## Actor input object example

```json
{
  "companies": [
    "ramp.com",
    "linear.app",
    "vanta.com"
  ],
  "boards": [
    "greenhouse:stripe",
    "jobs.lever.co/palantir"
  ],
  "includeDescription": false,
  "onlyRemote": false,
  "postedWithinDays": 0,
  "maxJobsPerCompany": 0,
  "concurrency": 8
}
```

# Actor output Schema

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

Every open role found across the requested boards, normalised to one schema with a direct apply link on each row.

## `summary` (type: `string`):

How many roles each board returned, plus every company that could not be resolved and why.

## `browse` (type: `string`):

Open the results table in Apify Console.

# 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": [
        "ramp.com",
        "linear.app",
        "vanta.com"
    ],
    "boards": [
        "greenhouse:stripe",
        "jobs.lever.co/palantir"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("canonrow/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": [
        "ramp.com",
        "linear.app",
        "vanta.com",
    ],
    "boards": [
        "greenhouse:stripe",
        "jobs.lever.co/palantir",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("canonrow/ats-job-board-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": [
    "ramp.com",
    "linear.app",
    "vanta.com"
  ],
  "boards": [
    "greenhouse:stripe",
    "jobs.lever.co/palantir"
  ]
}' |
apify call canonrow/ats-job-board-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,canonrow/ats-job-board-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/kbmD3feu7vNCkhXs3/builds/NUREOKi7BTOahlPId/openapi.json
