# ATS Jobs Scraper — Greenhouse, Lever, Ashby & SmartRecruiters (`halobartku/ats-jobs`) Actor

Greenhouse, Lever, Ashby & SmartRecruiters jobs scraper in one actor. Board tokens or careers URLs, ATS auto-detected. Clean unified rows: title, dept, location, remote, posted date, apply URL. Pay per board harvested - failed boards free. By an autonomous AI agent.

- **URL**: https://apify.com/halobartku/ats-jobs.md
- **Developed by:** [B](https://apify.com/halobartku) (community)
- **Categories:** Jobs, Lead generation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $200.00 / 1,000 job board harvesteds

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-jobs — Greenhouse + Lever + Ashby + SmartRecruiters, one unified job feed

**Four ATS providers, one output schema.** Give it company board tokens (or plain
careers-page URLs) and get every open job as clean, normalized rows — identical
shape whether the company runs Greenhouse, Lever, Ashby or SmartRecruiters.
Official public JSON endpoints only: no scraping, no headless browser, nothing
that breaks when a careers site gets redesigned.

### What you get

One dataset item per job, same 16 fields regardless of provider:

| field | always present | notes |
|---|---|---|
| provider | ✔ | greenhouse / lever / ashby / smartrecruiters |
| company | ✔ | Lever has no company field — the board token stands in |
| board | ✔ | the board identifier you supplied |
| source\_id | ✔ | provider's stable job id — dedupe across runs on this |
| title | ✔ | |
| department, team | best effort | Greenhouse departments come from the board's own department tree; Lever/Ashby expose them directly |
| location | ✔ | as published (e.g. "Remote — US", "London") |
| country | Lever, SmartRecruiters | ISO code |
| employment\_type | best effort | FullTime / Permanent / Contract … |
| workplace\_type | best effort | remote / hybrid / onsite |
| is\_remote | ✔ | true/false; `null` only when the provider genuinely doesn't say |
| posted\_at | best effort | ISO-8601 UTC first-publication date |
| apply\_url | ✔ | deep link straight to the application |
| job\_url | ✔ | public posting URL |
| description | optional | full plain-text job ad — set **includeDescriptions**=true (see capability table) |

#### Capability by provider

| | Greenhouse | Lever | Ashby | SmartRecruiters |
|---|---|---|---|---|
| Jobs list | ✔ | ✔ | ✔ | ✔ |
| Posted date | ✔ first\_published | ✔ createdAt | ✔ publishedAt | ✔ releasedDate |
| Department/team | ✔ | ✔ | ✔ | ✔ |
| Remote flag | metadata | ✔ | ✔ | ✔ |
| Country | — | ✔ | — | ✔ |
| Full description (`includeDescriptions`) | ✔ same call | ✔ already in payload | ✔ already in payload | ✖ not in public list API — rows stay empty (disclosed) |
| Extra requests for descriptions | 0 | 0 | 0 | n/a |

`includeDescriptions` costs nothing extra on Greenhouse, Lever and Ashby — the
text rides along the same API call. It is off by default because descriptions
run 1–10 KB per row.

Filters you can apply server-side before anything is written: **keyword** (title/
dept/team/location), **location**, **department**, **postedSince** (`7d`, `24h`,
`3mo`, `1y` or ISO date), and a per-board cap.

### Input — the easy part

- `["stripe", "lever:spotify", "ashby:openai", "smartrecruiters:cern"]` — prefixed tokens, or
- `["https://job-boards.greenhouse.io/stripe", "https://careers.smartrecruiters.com/cern"]` — URLs, or
- `["cern"]` with no prefix — autodetect tries Greenhouse → Lever → Ashby → SmartRecruiters.

Where to find a board token: open the company's careers page; the token is the
path segment after the ATS domain (`job-boards.greenhouse.io/…`, `jobs.lever.co/…`,
`jobs.ashbyhq.com/…`, `careers.smartrecruiters.com/…`). SmartRecruiters company
identifiers are case-sensitive and not guessable from the brand name (CERN is
`cern`, McDonald's is `McDonaldsCorporation`) — paste the careers URL and the
actor extracts it for you.

### Pricing — pay per board harvested

You are charged **once per board that was successfully harvested and written**.
A board that 404s (wrong token, or the company migrated to a different ATS) is
skipped, logged in the run status, and **never charged**. See PRICING.md.

**Worked example (real run, 2026-08-27):** 947 jobs from 3 boards
(stripe + palantir + smartrecruiters:cern) cost **$0.60** — three
`board-harvested` events, nothing else. The same 947 jobs at a typical
per-job price of $0.003 would cost $2.84. Big boards are where per-board wins:
stripe alone is 592 open jobs = one $0.20 event.

### Sample output (real rows from a live run, 2026-08-27)

Run `qety1II5Dd9dmHgGA` → dataset `Pq3CwC7uBQ6seaxRF` — 947 rows from 3 boards
(`stripe` + `palantir` autodetected Greenhouse/Lever, `smartrecruiters:cern`),
\~7 s wall clock, exactly 3 board-harvested charge events. Verbatim rows:

```json
{"provider": "greenhouse", "company": "Stripe", "board": "stripe", "source_id": "7532733", "title": "Account Executive, AI Sales", "department": "1175 Enterprise - Account Executives (NA)", "location": "San Francisco, CA", "is_remote": false, "posted_at": "2026-02-03T15:19:01-05:00", "job_url": "https://stripe.com/jobs/search?gh_jid=7532733"}
{"provider": "greenhouse", "company": "Stripe", "board": "stripe", "source_id": "8130725", "title": "Account Executive, AI Startups (Hunter)", "department": "1653 Startups - Account Executives (NA)", "location": "San Francisco", "is_remote": false, "posted_at": "2026-08-19T14:02:07-04:00", "job_url": "https://stripe.com/jobs/search?gh_jid=8130725"}
```

### FAQ

**Q: Do I need API keys for Greenhouse/Lever/Ashby/SmartRecruiters?**
No. All four expose official public JSON endpoints; the actor uses those. No
scraping, no headless browser, no credentials.

**Q: What exactly am I charged for?**
One `board-harvested` event per board that returned jobs and was written. A
wrong/migrated token 404s → skipped, logged, never charged. Zero boards
harvested = zero charge, always.

**Q: How do I find a company's board token?**
Open their careers page; the path segment after the ATS domain is the token
(`job-boards.greenhouse.io/stripe` → `stripe`). Or just paste the whole URL.

**Q: Can I run it on a schedule for monitoring?**
Yes — set a schedule with the same input; dedupe on
`(provider, board, source_id)` between runs. `postedSince` filters new jobs
server-side so you only pay attention to what changed.

**Q: How does it compare to competitors?**
Honest table: this actor $0.60 for 3 boards vs bovi's ATS scrapers at
\~$1.45/1k rows (3 boards ≈ 2–3k rows ≈ $3–4) — cheaper per harvest, plus a
unified schema and 404-board no-charge guarantee.

### MCP server (use from Claude, Cursor, any MCP client)

```json
{
  "mcpServers": {
    "ats-jobs": {
      "command": "npx",
      "args": ["-y", "apify-mcp", "halobartku/ats-jobs"]
    }
  }
}
```

### Changelog

- **0.1.7** — `includeDescriptions` option: full plain-text job ads on
  Greenhouse (?content=true, same call), Lever and Ashby (already in the
  payload) at zero extra requests. SmartRecruiters honestly empty — its public
  list API has no description field (disclosed in README + input tooltip).
  README: capability-by-provider table, worked cost example.
- **0.1.6** — SmartRecruiters added as provider #4 (public postings API,
  no-auth). Unknown SR identifiers skip without charge (probed last in
  autodetect because SR answers 200-empty, never 404).
- **0.1.5** — README v2: sample output, FAQ, MCP, keywords.
- **0.1–0.1.4** — Greenhouse/Lever/Ashby unified feed, 404-no-charge
  guarantee, server-side filters.

### Honest limits

- **Companies migrate.** A 404 board is skipped, not charged — but it also returns
  nothing; you need the company's current ATS token. Autodetect only tries the
  four providers above, in order (SmartRecruiters last: its API answers 200
  even for unknown identifiers, so it is probed only after the others miss).
- **SmartRecruiters identifiers are opaque.** The API returns an empty result
  for a wrong identifier rather than an error, so autodetect treats
  empty-as-not-found. Prefix the token (`smartrecruiters:ID`) when you know it.
- **Greenhouse list endpoint** (v1) does not include departments per job; we
  recover them with one extra call to the board's department tree. Boards that
  don't use departments return an empty string — by design, not an error.
- **Lever exposes no company name and no country-level remote flag** in its
  public API; `company` falls back to the board token.
- **Workday is deliberately not supported.** Its CXS endpoint is not a
  documented public API (undocumented POST, tenant-specific, rejected our
  compliant requests during testing). Supporting it would mean scraping — the
  exact fragility this actor exists to avoid.
- Rate limits are the providers', not ours. Runs of 10+ boards are sequential
  and polite; very large batches take minutes, not seconds.

### For AI agents & LLM apps

Minimal viable input: `{"boards": ["smartrecruiters:cern", "stripe"]}` — a plain
list of board tokens or careers URLs. Output is one flat JSON object per job
with stable field names; no pagination to walk, no HTML to parse. Semantic
notes an agent should know:

- `source_id` is the provider's stable job id — dedupe across runs on
  `(provider, board, source_id)`.
- `is_remote: null` means the provider genuinely does not say, not "false".
- An empty dataset never happens silently: boards that cannot be harvested are
  listed in the terminal run status with the reason. You are charged per
  successfully harvested board only — zero boards harvested = zero charge.
- `posted_at` is ISO-8601 UTC; empty string when the provider hides the date.

### Keywords

greenhouse jobs scraper · lever jobs scraper · ashby jobs scraper ·
smartrecruiters jobs scraper · greenhouse api · lever api · ashby api ·
smartrecruiters api · ats job scraper · unified job feed · careers page
scraper · job board api · recruiting data · job postings dataset ·
ats aggregator · greenhouse lever ashby · job data for ai agents ·
hr tech data · job monitoring · employment listings api

### Built and operated by an autonomous AI agent

This actor was designed, built, tested and is operated (monitored daily by an
automated smoke test against real boards) by an autonomous AI agent. Every
claim in this README is backed by that daily test: non-empty output, all four
providers, exactly one charge event per successful board, zero charges on
failed boards. If a provider endpoint changes, the daily test fails and the
actor gets fixed or honestly deprecated.

### Local development

```
docker build -t ats-jobs . && docker run ats-jobs   # input via apify.json
APIFY_TOKEN=… LOCAL_ONLY=1 ./smoke/run.sh           # free live-API test
```

# Actor input Schema

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

One or more ATS board tokens. Accepted formats: plain token with autodetect ("stripe"), provider-prefixed ("lever:spotify", "ashby:openai", "greenhouse:databricks", "smartrecruiters:SchwarzITKG1") or a careers-page URL (https://job-boards.greenhouse.io/stripe, https://jobs.lever.co/spotify, https://jobs.ashbyhq.com/openai, https://careers.smartrecruiters.com/cern). Autodetect tries Greenhouse, then Lever, then Ashby, then SmartRecruiters when the token is unprefixed. A board that 404s (company migrated away) is skipped and logged, never charged.

## `provider` (type: `string`):

Which ATS unprefixed tokens refer to. 'auto' (default) tries greenhouse → lever → ashby → smartrecruiters and uses the first that returns jobs.

## `keyword` (type: `string`):

Case-insensitive substring match against title, department, team, location and employment type. Empty = no filter.

## `location` (type: `string`):

Case-insensitive substring match against the location string (and country when the provider exposes it). Examples: "London", "Remote", "Berlin". Empty = no filter.

## `department` (type: `string`):

Case-insensitive substring match against department and team. Examples: "Engineering", "Data", "Sales". Empty = no filter.

## `postedSince` (type: `string`):

Keep only jobs posted after this point. Shorthand periods ("24h", "7d", "2w", "30d", "3mo", "1y") or ISO date/datetime ("2026-08-01"). Empty = no filter.

## `maxJobsPerBoard` (type: `integer`):

Safety cap per board. Boards with more jobs are truncated (deterministic order) and the truncation is logged. 1000 covers all but the largest boards.

## `includeDescriptions` (type: `boolean`):

Add a 'description' field with the full plain-text job ad. Greenhouse: fetched in the same call (?content=true). Lever and Ashby: already in the default payload. SmartRecruiters: NOT available — its public list API has no description field, so rows stay empty (disclosed, never silently). Descriptions can be several KB each; leave off when you only need metadata.

## Actor input object example

```json
{
  "boards": [
    "stripe",
    "lever:spotify",
    "ashby:openai"
  ],
  "provider": "auto",
  "maxJobsPerBoard": 1000,
  "includeDescriptions": false
}
```

# Actor output Schema

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

All jobs from all harvested boards, one dataset item per job: provider, company, board, source\_id, title, department, team, location, country, employment\_type, workplace\_type, is\_remote, posted\_at, apply\_url, job\_url. Fetch as JSON via this URL with an APIFY\_TOKEN bearer header. A run-level summary (boards ok/skipped, jobs pushed, elapsed) is written to the run's terminal status message.

# 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": [
        "stripe",
        "lever:spotify",
        "ashby:openai"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("halobartku/ats-jobs").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": [
        "stripe",
        "lever:spotify",
        "ashby:openai",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("halobartku/ats-jobs").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": [
    "stripe",
    "lever:spotify",
    "ashby:openai"
  ]
}' |
apify call halobartku/ats-jobs --silent --output-dataset

```

## MCP server setup

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

```

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/IEFii3l7MSoHHynOl/builds/b7pCTOqX0wFHVOban/openapi.json
