# ATS Jobs Scraper - Greenhouse, Lever, Ashby (`s-r/ats-jobs-scraper`) Actor

Read company job boards straight from the three applicant-tracking systems that publish openly: Greenhouse, Lever and Ashby. Returns title, location, department, employment type, remote flag, full description and, on Ashby, the published pay range. No API key needed.

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

## Pricing

Pay per event

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 Scraper

Read a company's open positions straight from the applicant-tracking system it
actually hires through. Greenhouse, Lever and Ashby all publish their job
boards openly, so this reads structured data rather than parsing a page.

Give it a company name or a pasted board URL. No API key, no login.

### Why this is different from scraping a careers page

The three systems each expose a documented public job-board endpoint. That has
two consequences worth knowing before you compare this with a page scraper:

**It does not break when a site is redesigned.** There is no markup to parse,
so a careers-page facelift changes nothing here.

**It returns everything at once.** Stripe's board came back with **619 open
positions in a single request**. A page scraper would walk that in twenty
paginated fetches.

### You do not need to know which system a company uses

Type `stripe` and all three are tried until one answers. If you already know,
pin it with `greenhouse:stripe`, `lever:matchgroup` or `ashby:Ramp`, or just
paste the board URL you were looking at.

A company that is on none of the three is reported as exactly that, rather than
as an empty result you would have to interpret.

### What each job gives you

- `title`, `url` and `apply_url`
- `company`, `board` and `ats` — which system published it
- `location`, plus `all_locations` when a posting lists several, and `country`
- `department` and `team`
- `employment_type` (full time, contract), `workplace_type` (onsite, hybrid,
  remote) and `is_remote`
- `published_at` and `updated_at`
- `description` as plain text, plus `description_chars`
- `salary` — see below

`description_chars` is more useful than it looks: a posting with 200 characters
is a stub, and one with 4,000 is a real spec. Sorting by it separates the two
without reading any of them.

### Three payload details this gets right

**Greenhouse escapes its own HTML.** Its description field arrives as
`&lt;h2&gt;&lt;strong&gt;Who we are` rather than as markup. A parser that
strips tags without unescaping first leaves every entity code in place, and the
description reads as soup. It is unescaped once, then stripped.

**Lever timestamps are milliseconds.** `createdAt` is a number like
1787203369315\. Read as seconds that is the year 58,000. Read correctly it is
2026\. A date that wrong still sorts and still renders, which is exactly what
makes it dangerous.

**The three publish three different date formats** — an offset timestamp, a
millisecond integer and a Zulu time with milliseconds. Left alone, sorting a
mixed run by date compares those strings against each other and quietly gets
the order wrong. Everything is converted to one UTC form.

### About salary

**Only Ashby publishes pay on its public board.** Its postings carry a
ready-made range such as `$211.4K – $290.6K • Offers Equity`. Greenhouse and
Lever carry nothing equivalent.

So an empty `salary` means *this system does not publish pay*, not that the job
is unpaid. The run summary reports `withSalary` so you can see how much of a
run carried one rather than assuming.

### Filters

- **Title contains** — `engineer`, `designer`
- **Location contains** — `london`, `remote`, `new york`
- **Remote only** — postings flagged remote by the system or listing a remote
  location
- **Include full descriptions** — on by default. Lever and Ashby always include
  the text; for Greenhouse it means a larger response, so turn it off when you
  only need titles and locations.

### Run summary

Boards requested and read, a count **per system**, jobs returned, how many
carried pay and a description, how many are remote, and the number of distinct
companies and departments.

The per-system breakdown is the useful health check: if a run of twenty
companies reports jobs from only one system, the other names were probably
wrong rather than empty.

### What people use this for

**Recruiting intelligence.** Which teams a competitor is growing, and how fast,
is visible in the department breakdown across repeated runs. `published_at`
turns that into a hiring-velocity series.

**Pay benchmarking.** The Ashby subset is a genuine sample of published ranges
by role and location, which very few sources give you in structured form.

**Job aggregation.** One request per company, everything normalised into one
shape, so a board of a hundred companies is a hundred requests and one table.

**Lead scoring.** A company that just opened fifteen engineering roles is
hiring, and hiring is a buying signal if you sell to engineering teams.

### Scale

Each board is exactly one request regardless of how many openings it holds.
Three companies across all three systems returned **838 jobs in 22 seconds** in
testing, 138 of them with pay and 836 with full descriptions.

Because there is no pagination and no per-job follow-up, a run of fifty
companies is fifty requests.

### Notes

Only currently open positions appear. A role that is filled or withdrawn stops
being published, so running on a schedule and keeping the rows lets you see how
long postings stay open, which is a fair proxy for how hard a role is to fill.

Board names are case-sensitive on Ashby (`Ramp`, not `ramp`) and lower-case on
Greenhouse and Lever. Pasting the URL avoids the question entirely.

# Actor input Schema

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

Companies to read. Paste a job-board URL (boards.greenhouse.io/..., jobs.lever.co/..., jobs.ashbyhq.com/...) or just the company's board name such as 'stripe'. A bare name is tried on all three systems, so you do not need to know which one a company uses. To pin it, write greenhouse:stripe, lever:matchgroup or ashby:Ramp.

## `include_description` (type: `boolean`):

Fetch the full job description text. Lever and Ashby always include it; for Greenhouse it costs a larger response, so turn this off if you only need titles and locations.

## `title_contains` (type: `string`):

Only jobs whose title contains this text, for example 'engineer'.

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

Only jobs whose location contains this text, for example 'london' or 'remote'.

## `remote_only` (type: `boolean`):

Only jobs flagged remote by the system or listing a remote location.

## `jobs_per_board` (type: `integer`):

Upper bound per company. A large employer's board can hold several hundred openings.

## `max_boards` (type: `integer`):

How many companies to read in one run. Each costs one request.

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

How many boards to read at the same time.

## `retries` (type: `integer`):

Retries with backoff before a board is reported as an error. A 404 is an answer, not a failure, and is never retried.

## Actor input object example

```json
{
  "boards": [
    "stripe",
    "https://jobs.ashbyhq.com/Ramp"
  ],
  "include_description": true,
  "title_contains": "engineer",
  "location": "london",
  "remote_only": false,
  "jobs_per_board": 1000,
  "max_boards": 50,
  "concurrency": 5,
  "retries": 3
}
```

# Actor output Schema

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

One row per open position.

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

Counts by system, how many carried pay, and unique companies and departments.

## `errors` (type: `string`):

Boards that could not be read, with a code and a redacted 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",
        "https://jobs.ashbyhq.com/Ramp"
    ],
    "jobs_per_board": 1000,
    "max_boards": 50,
    "concurrency": 5,
    "retries": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("s-r/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 = {
    "boards": [
        "stripe",
        "https://jobs.ashbyhq.com/Ramp",
    ],
    "jobs_per_board": 1000,
    "max_boards": 50,
    "concurrency": 5,
    "retries": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("s-r/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 '{
  "boards": [
    "stripe",
    "https://jobs.ashbyhq.com/Ramp"
  ],
  "jobs_per_board": 1000,
  "max_boards": 50,
  "concurrency": 5,
  "retries": 3
}' |
apify call s-r/ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,s-r/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/2tyOCET9pbtHFkyYq/builds/FDe4LdTM07wO3zHAF/openapi.json
