# ATS Jobs Scraper: Greenhouse, Lever, Ashby, Workable (`pradio/greenhouse-lever-ashby-job`) Actor

Public job-board JSON from Greenhouse, Lever, Ashby, Workable and SmartRecruiters for a list of company slugs. One row per posting: title, location, department, apply URL, full description, and salary when published. Title, location and remote filters; only-new-since-last-run. No login.

- **URL**: https://apify.com/pradio/greenhouse-lever-ashby-job.md
- **Developed by:** [E A](https://apify.com/pradio) (community)
- **Categories:** Jobs, Lead generation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.65 / 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/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: Greenhouse, Lever, Ashby, Workable

Reads the **public job-board JSON** that Greenhouse, Lever, Ashby, Workable and SmartRecruiters publish for a company's careers page, for a **list of companies** in one run. Each posting is one row: title, location, department, apply URL, the full description, and salary when that board publishes it. Filter by title, location or remote before anything is read; ask for only the postings you have not seen since your last run. No login, no browser, no applicant or Harvest APIs. The default company is `ramp` (Ramp's Ashby board).

### Who uses ATS Jobs Scraper

| Who | What they run it for |
|---|---|
| Talent intelligence and aggregators | Pull every open role across a list of companies, whichever of the five vendors each one uses, into one table with one shape. |
| Recruiters and sourcers | Refresh a target list's live postings, keep apply URLs and published pay, and see only what is new since last week. |
| Sales and hiring-signal work | Watch which roles a company opens, in which department and where, and whether pay is posted. |
| Agents and pipelines | Pass a list of slugs and get rectangular rows instead of five vendor JSON dialects. |

### What you can count on

- every row is charged only after it is written to your dataset; a row you cannot see is never billed
- a run that finds nothing returns one NO\_MATCHING\_LISTINGS row that says so, never an empty dataset
- a spending limit stops the run cleanly with a STOPPED\_EARLY row saying how many rows were returned and how many were not
- every run writes a RUN\_SUMMARY record with rowsFetched, rowsPushed, rowsCharged and duplicatesDropped, so a short run and a broken one are told apart
- if a board changes its response, the run fails with the error in the log; it never returns rows full of nulls and calls it success
- no column is always empty: every field in the table below is filled by at least one vendor, and the table says which

#### Measured against the alternative

The most-used alternative on this board was run on 2026-09-10, on its own default board, for 10 postings. Its rows left `salary` empty on 10 of 10, `employment_type` empty on 10 of 10, `remote_type` empty on 9 of 10, and `parse_confidence` empty on 10 of 10. On our 100-row capture of the same day, `salary_minimum` is filled on 97 of 100 rows, `job_type` on 100 of 100, and `is_remote` on 100 of 100. It runs at 1024 MB; this Actor runs at 256 MB. It charges $0.0015 a posting; this Actor charges $0.0012.

### What data does ATS Jobs Scraper return?

One real posting from the default Ramp board (description shortened):

```json
{
  "platform": "ashby",
  "official_url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "posted_date": "2026-04-07T17:12:35.753+00:00",
  "location": "New York, NY (HQ)",
  "is_remote": true,
  "workplace_type": "Hybrid",
  "description": "ABOUT RAMP\n\nRamp is building the smart infrastructure for finance teams …",
  "job_type": "FullTime",
  "department": "Engineering",
  "job_id": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "salary_minimum": 211400,
  "salary_maximum": 290600,
  "salary_currency": "USD",
  "compensation": { "summaryComponents": [ { "compensationType": "Salary", "minValue": 211400, "maxValue": 290600, "currencyCode": "USD" } ] },
  "company_name": "ramp",
  "address": { "postalAddress": { "addressLocality": "New York", "addressRegion": "NY", "addressCountry": "US" } },
  "apply_url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "result": "ROW"
}
```

The capture that froze this example returned **100 postings** on the default input (`company` = `["ramp"]`, `maxItemsPerCompany` = 100); the board listed 146. Ten rows across all five vendors, two per vendor, are the test fixture this Actor is held to.

#### Fields on every posting row

| Field | Meaning | Ashby | Greenhouse | Lever | Workable | SmartRecruiters |
|---|---|---|---|---|---|---|
| `platform` | Which vendor answered | ashby | greenhouse | lever | workable | smartrecruiters |
| `official_url` | Public posting URL (link) | `jobUrl` | `absolute_url` | `hostedUrl` | `url` | `postingUrl` |
| `title` | Job title | `title` | `title` | `text` | `title` | `name` |
| `posted_date` | Published time | `publishedAt` | `first_published` | `createdAt` | `published_on` | `releasedDate` |
| `location` | Workplace text as published | `location` | `location.name` | `categories.location` | city, state, country | `fullLocation` |
| `is_remote` | Vendor's remote flag; null when it has none | `isRemote` | word Remote in location, else null | `workplaceType` | `telecommuting` | `location.remote` |
| `workplace_type` | remote, hybrid or onsite | `workplaceType` | null | `workplaceType` | `workplace` | remote / hybrid flags |
| `description` | Full posting text, plain or HTML as the vendor gives it | `descriptionPlain` | `content` | `descriptionPlain` | description + requirements + benefits | jobDescription + qualifications + additionalInformation |
| `job_type` | Employment type in the vendor's words | `employmentType` | null | `categories.commitment` | `employment_type` | `typeOfEmployment.label` |
| `department` | Department or team | `department` | `departments[0].name` | `categories.department` | `department` | `department.label` |
| `job_id` | The vendor's own posting id | `id` | `id` | `id` | `shortcode` | `id` |
| `salary_minimum` | Published pay floor | compensation `minValue` | `pay_input_ranges` | `salaryRange.min` | null | null |
| `salary_maximum` | Published pay ceiling | compensation `maxValue` | `pay_input_ranges` | `salaryRange.max` | null | null |
| `salary_currency` | ISO code of the published pay | `currencyCode` | `currency_type` | `salaryRange.currency` | null | null |
| `compensation` | The vendor's whole pay object when published | `compensation` | `pay_input_ranges` | `salaryRange` | null | null |
| `company_name` | The slug you passed in `company` | slug | slug | slug | account | company |
| `address` | Structured location when published | `address` | null | null | city, state, country | city, region, country, coordinates |
| `apply_url` | Where a person applies (link) | `applyUrl` | posting URL | `applyUrl` | `application_url` | `applyUrl` |
| `result` | `ROW` on a posting | | | | | |
| `reason` | Status rows only: why there were no postings, or why the run stopped early | | | | | |
| `rowsFetched` | Status rows only: postings received before de-duplication and the cap | | | | | |
| `rowsReturned` | Status rows only: posting rows written to the dataset | | | | | |
| `rowsRemaining` | Status rows only: fetched postings not returned | | | | | |

A null in a cell above means that vendor does not publish the value on its public API; the row still carries the column, as `null`, so every vendor's rows have the same shape. Salary is present whenever the board publishes it: Ashby and Lever do on many postings, Greenhouse on boards that expose pay ranges, Workable and SmartRecruiters never on these endpoints.

### Pricing

Pay-per-event: **$0.0012 per `listing-returned`**, one posting written to the dataset. The platform also charges `apify-actor-start` at $0.00005 per compute unit allocated for the run (256 MB is a quarter of a unit).

Status rows (`NO_MATCHING_LISTINGS`, `STOPPED_EARLY`) and dropped duplicates are **not** charged. Postings removed by a filter are never read in full and never charged.

| Postings written | `listing-returned` | Start (default) | Total (those two) |
|---|---|---|---|
| 100 | $0.12 | $0.00005 | $0.12005 |
| 1,000 | $1.20 | $0.00005 | $1.20005 |
| 10,000 | $12.00 | $0.00005 | $12.00005 |

Platform compute is billed separately by Apify according to memory and duration. A run of 100 Ramp postings finished in about 10 seconds at 256 MB. Workable and SmartRecruiters descriptions cost one request per kept posting; a run of 100 such postings takes about half a minute more.

### How do I use ATS Jobs Scraper?

1. Open the Actor on Apify and press **Start**. The default input reads Ramp's board.
2. In `company`, add one entry per board: a slug (`ramp`), a tagged slug (`greenhouse:stripe`, `lever:spotify`, `workable:nuvei`, `smartrecruiters:Ubisoft2`), or a public board URL.
3. Optionally set `titleKeyword`, `locationKeyword` or `remoteOnly` to keep only the postings you want, `maxItemsPerCompany` (default 100), and `onlyNew` to skip what an earlier run already returned.
4. Download the dataset. `RUN_SUMMARY` in the default key-value store has the counts.

Example input, the one this page's sample row came from:

```json
{
  "company": ["ramp"],
  "maxItemsPerCompany": 100
}
```

Three boards, engineering roles, remote only:

```json
{
  "company": ["ramp", "greenhouse:stripe", "lever:spotify"],
  "maxItemsPerCompany": 50,
  "titleKeyword": "engineer",
  "remoteOnly": true
}
```

Weekly watch of a target list, new postings only:

```json
{
  "company": ["workable:nuvei", "smartrecruiters:Ubisoft2", "ramp"],
  "onlyNew": true
}
```

API (replace the token):

```
https://api.apify.com/v2/acts/Pradio~greenhouse-lever-ashby-job/runs?token=YOUR_TOKEN
```

with body `{"company":["ramp"],"maxItemsPerCompany":20}`.

### Input

| Input | Type | Default | Required | What it does |
|---|---|---|---|---|
| `company` | list of strings | `["ramp"]` | yes | One entry per company board: slug, `vendor:slug`, or public board URL |
| `maxItemsPerCompany` | integer | `100` | no | Most posting rows per company; each board is capped on its own |
| `maxItems` | integer | (none) | no | Overall cap across the run, on top of the per-company cap |
| `titleKeyword` | string | (empty) | no | Keep postings whose title contains this text, case-insensitive |
| `locationKeyword` | string | (empty) | no | Keep postings whose location text or structured address contains this text |
| `remoteOnly` | boolean | `false` | no | Keep only postings the vendor marks remote; an unclassified posting is dropped, not guessed |
| `onlyNew` | boolean | `false` | no | Skip postings an earlier run under your account already returned |
| `includeDescription` | boolean | `true` | no | Read the per-posting description on Workable and SmartRecruiters (one request per kept posting) |

#### company

Accepted forms, one per entry:

- Bare slug: `ramp` — tried on Ashby, Greenhouse, Lever, Workable and SmartRecruiters in that order until one answers.
- Tagged: `ashby:ramp`, `greenhouse:stripe`, `lever:spotify`, `workable:nuvei`, `smartrecruiters:Ubisoft2`. Pin the vendor when two of them know the same slug.
- URLs: `https://jobs.ashbyhq.com/ramp`, `https://boards.greenhouse.io/stripe`, `https://jobs.lever.co/spotify`, `https://apply.workable.com/nuvei/`, `https://jobs.smartrecruiters.com/Ubisoft2/`, and the vendors' API URLs for the same boards.

It does **not** accept applicant Data API URLs, Harvest tokens, or login cookies.

#### maxItemsPerCompany and maxItems

Each company keeps at most `maxItemsPerCompany` postings after the filters, so a board of 300 never starves the next one on the list. `maxItems` is an extra ceiling on the whole run; leave it empty unless you want one.

#### titleKeyword, locationKeyword, remoteOnly

Substring matches, case-insensitive, on the title and on the location text plus the structured address. `remoteOnly` keeps only postings whose vendor marks them remote. All three run before any per-posting description is read, so filtered-out postings cost nothing.

#### onlyNew

Postings this Actor has returned under your account are remembered in a named key-value store, `greenhouse-lever-ashby-job-seen`, by posting URL. A posting is added only after it was actually returned, so a run cut short by a spend cap leaves the rest for next time. Delete that store to forget everything.

#### includeDescription

Ashby, Greenhouse and Lever include the description in the board response. Workable and SmartRecruiters publish it on a per-posting endpoint, one request per kept posting. Set this to `false` for a cheaper title-and-link run on those two vendors; their `description` is then null.

### Output

- **Posting rows** carry `result: "ROW"` and every field in the table above (a value the vendor does not publish is `null`, never omitted). Duplicate `official_url` values are dropped before charge.
- **Zero postings:** one uncharged row with `result: "NO_MATCHING_LISTINGS"` and a `reason`. The dataset is never empty.
- **Spend cap:** one uncharged `STOPPED_EARLY` row with `rowsReturned` and `rowsRemaining`.
- **RUN\_SUMMARY** in the default key-value store: `rowsFetched`, `rowsPushed`, `rowsCharged`, `duplicatesDropped`, `stoppedEarly`, and `skippedSeen` when `onlyNew` is on.

An HTTP error, a JSON parse failure, or a 429 that still fails after retries **fails the run**. That is deliberate: a refused read is not an empty board.

### What can you do with the data?

A researcher compiling every open engineering role across twenty portfolio companies passes their twenty slugs in `company` with `titleKeyword` set to `engineer` and exports title, department, location, `apply_url` and pay where published.

A sourcer watching Stripe's Greenhouse board and Spotify's Lever board sets `locationKeyword` to `London` and `onlyNew` to true, and gets each week only the postings that appeared since the last run.

A pipeline that already stores company slugs maps each one to this Actor, writes rows into a warehouse, and joins on `official_url` or `job_id` as the stable posting key.

An agent asks for one slug and reads rectangular fields instead of learning five vendor JSON dialects.

### Use ATS Jobs Scraper with AI agents

```
claude mcp add --transport http apify "https://mcp.apify.com?tools=Pradio/greenhouse-lever-ashby-job"
```

Paste that line to give an MCP client this Actor as a tool (the tool name is the Store identity `Pradio/greenhouse-lever-ashby-job`).

### Limits

- **Public job-board JSON only.** Ashby posting-api, Greenhouse boards-api, Lever postings, Workable's widget and v2 job endpoints, SmartRecruiters postings. No Harvest, no authenticated Data APIs, no apply POST, no shared ATS accounts, no HTML scrape of careers sites.
- **Five vendors, named.** Recruitee, Personio and Workday are not read: Recruitee's public offers endpoint answered no live board we tried, Personio publishes XML, and Workday has no logged-out JSON board. A slug on one of those returns NO\_MATCHING\_LISTINGS, not a guess.
- **A list of company boards per run**, not a search engine across every company on a vendor.
- **No browser, no proxy.** If a host refuses the GET, the run fails (with backoff on HTTP 429).
- **Salary only when the board publishes it.** Workable and SmartRecruiters never do on these endpoints; many Greenhouse boards omit it; those rows carry null pay fields and a null `compensation`.
- **`remoteOnly` drops the unclassified.** Greenhouse has no remote flag, so a Greenhouse posting passes `remoteOnly` only when its location text says Remote.
- **Not affiliated** with Greenhouse, Lever, Ashby, Workable, SmartRecruiters, or any employer whose board you read. Re-read each vendor's public API docs and site terms before you rely on a high-volume schedule; stop if they object.

Adjacent Actors from the same publisher: [Authentic Jobs Listings Scraper](https://apify.com/pradio/authentic-jobs) for that board's public listings feed, and [Voice Over Jobs Monitor](https://apify.com/pradio/vo-jobs-pipeline) for voice-over castings. This Actor is only the five vendors' public job-board JSON.

Report problems on the Actor's Issues tab on Apify.

### Troubleshooting

**I got fewer rows than `maxItemsPerCompany`.** The board had fewer postings after the filters, or Ashby skipped `isListed: false` items. That is the board running out, not a silent error. Check `RUN_SUMMARY.rowsFetched`.

**I got one row and no jobs.** Read `result`. `NO_MATCHING_LISTINGS` means every vendor tried for that slug answered and there were no postings, or none passed the filters, or `onlyNew` had already returned them all. That one row is the answer; it is not billed.

**The run failed instead of returning empty titles.** A 401/403/5xx or truncated JSON fails the run. Fix the slug, wait out a 429, or stop. Empty-looking success rows are not produced on a refused read.

**A bare slug found the wrong company.** Two vendors can know the same slug. Pin it: `greenhouse:your-slug`, `workable:your-account`.

**SmartRecruiters returned nothing for a bare slug.** SmartRecruiters answers an unknown company with an empty list rather than a 404, so a bare slug moves on; use `smartrecruiters:Company` to read it directly, and note that their company identifiers are case-sensitive (`Ubisoft2`).

### FAQ

#### Can I use integrations with ATS Jobs Scraper?

Yes. Apify integrations can start this Actor and pass any of the inputs above. Chain it by storing the dataset and pointing the next Actor at that dataset URL.

#### Can I use ATS Jobs Scraper with the Apify API?

Yes. Start runs against `Pradio/greenhouse-lever-ashby-job` (`Pradio~greenhouse-lever-ashby-job` in the path). The input JSON is the same as the Console. Dataset items and `RUN_SUMMARY` are the same records as a Console run.

#### Can I use ATS Jobs Scraper through an MCP server?

Yes. Use the snippet in **Use ATS Jobs Scraper with AI agents** so the client exposes this Actor as a tool.

#### Is it legal to scrape the boards?

This Actor only GETs the vendors' **public job-board / posting JSON** (logged-out), the same endpoints their own careers-page embeds read. It does not use Harvest or applicant APIs and it does not collect recruiter emails. You are responsible for each vendor's current public API rules and terms, for your jurisdiction, and for stopping if a host objects. This page is not legal advice. The Actor is **not** an official product of any vendor named here.

### Not affiliated

ATS Jobs Scraper is an independent tool. It is **not affiliated with, endorsed by, or maintained by Greenhouse Software, Lever (Employ Inc.), Ashby, Workable, SmartRecruiters, or any employer** whose public job board you request. Job content belongs to the posting company.

# Actor input Schema

## `company` (type: `array`):

One entry per company board: a slug (ramp), a tagged slug (greenhouse:stripe, lever:spotify, workable:nuvei, smartrecruiters:Ubisoft2), or a public job-board URL. A bare slug is tried on Ashby, Greenhouse, Lever, Workable and SmartRecruiters in that order until one answers.

## `maxItemsPerCompany` (type: `integer`):

Most posting rows returned for each company in the list. Each board is capped on its own, so a big board never starves the next one.

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

An overall cap across the whole run, on top of the per-company cap. Leave empty for no overall cap.

## `titleKeyword` (type: `string`):

Keep only postings whose title contains this text (case-insensitive). Applied before any description is read, so filtered-out postings cost nothing.

## `locationKeyword` (type: `string`):

Keep only postings whose location text or structured address contains this text (case-insensitive), e.g. London, Remote, US.

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

Keep only postings the vendor marks remote. A posting the vendor does not classify is dropped, not guessed.

## `onlyNew` (type: `boolean`):

Skip postings an earlier run of this Actor under your account already returned. The memory is a named key-value store (greenhouse-lever-ashby-job-seen) that only rows actually returned are added to.

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

Workable and SmartRecruiters publish descriptions on a separate per-posting endpoint; reading it is one extra request per kept posting. Turn off for a cheaper title-and-link run on those two vendors. Ashby, Greenhouse and Lever include the description in the board response either way.

## Actor input object example

```json
{
  "company": [
    "ramp"
  ],
  "maxItemsPerCompany": 100,
  "remoteOnly": false,
  "onlyNew": false,
  "includeDescription": true
}
```

# Actor output Schema

## `rows` (type: `string`):

JSON items for this run: one object per job posting plus any status row.

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

RUN\_SUMMARY record: rowsFetched, rowsPushed, rowsCharged, duplicatesDropped, stoppedEarly.

# 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 = {
    "company": [
        "ramp"
    ],
    "maxItemsPerCompany": 100
};

// Run the Actor and wait for it to finish
const run = await client.actor("pradio/greenhouse-lever-ashby-job").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 = {
    "company": ["ramp"],
    "maxItemsPerCompany": 100,
}

# Run the Actor and wait for it to finish
run = client.actor("pradio/greenhouse-lever-ashby-job").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 '{
  "company": [
    "ramp"
  ],
  "maxItemsPerCompany": 100
}' |
apify call pradio/greenhouse-lever-ashby-job --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,pradio/greenhouse-lever-ashby-job"
        }
    }
}

```

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/o7QbmokIrFz01AU7i/builds/WfkonZLNM5veZLQyA/openapi.json
