# ATS Job Scraper: Greenhouse, Lever, Ashby, Workable +2 (`techymaki/ats-jobs`) Actor

Scrape job postings from Greenhouse, Lever, Ashby, Workable, SmartRecruiters and Recruitee job boards in one schema. Paste any careers page and the ATS is auto-detected. $1 per 1,000 jobs.

- **URL**: https://apify.com/techymaki/ats-jobs.md
- **Developed by:** [TechyMaki Labs](https://apify.com/techymaki) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 jobs

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?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ATS Jobs: Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee

Pull every open job from a company's applicant tracking system (ATS) in one clean, identical format. Give it a board URL, an `ats:slug` pair such as `greenhouse:airbnb`, or just the company's own careers page. The actor detects which ATS the page uses.

Use it as a **Greenhouse jobs scraper, Lever jobs scraper, Ashby jobs scraper, Workable jobs scraper, SmartRecruiters scraper or Recruitee scraper**, or as one ATS scraper for a whole list of company careers pages. $1 per 1,000 jobs.

It calls each ATS's public JSON endpoint. It does not scrape HTML job pages, so it is fast and rarely breaks. It does not log in, use proxies, or bypass anything.

### What you get

One item per job, the same fields for all six ATS. A field the ATS does not provide is `null`, never guessed.

```json
{
  "ats": "ashby",
  "company": "ramp",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "team": "Backend",
  "location": "New York, NY (HQ)",
  "remote": true,
  "employmentType": "FullTime",
  "postedAt": "2026-04-07T17:12:35.753Z",
  "updatedAt": null,
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
  "salary": { "min": 211400, "max": 290600, "currency": "USD", "interval": "year" },
  "scrapedAt": "2026-10-26T13:01:48.619Z"
}
```

That item is real output (see `samples/sample-output.json` for 47 more). `descriptionText` is added when you turn on `includeDescription`.

### Supported ATS

Checked against live endpoints on 2026-10-26.

| ATS | Board input | Salary | Posted date | Updated date | Apply URL | Remote flag | Team |
|---|---|---|---|---|---|---|---|
| Greenhouse | `boards.greenhouse.io/x`, `job-boards.greenhouse.io/x` | Yes, when the company enables pay transparency (min/max/currency, no interval) | Yes | Yes | No (null) | Only if the location text says "Remote" | No |
| Lever | `jobs.lever.co/x` | No | Yes | No | Yes | Yes | Yes |
| Ashby | `jobs.ashbyhq.com/x` | Yes, when the company publishes it (min/max/currency/interval) | Yes | No | Yes | Yes | Yes |
| Workable | `apply.workable.com/x` | No | Yes (date only) | No | Yes | Yes | No |
| SmartRecruiters | `careers.smartrecruiters.com/x`, `jobs.smartrecruiters.com/x` | No | Yes | No | Only with `includeDescription` | Yes | No |
| Recruitee | `x.recruitee.com` | Mapped if the company fills it in; empty on the board we tested | Yes | Yes | Yes | Yes | No |

Notes on honesty:

- `remote` is `true`, `false` or `null`. Ashby and Recruitee send their own flag. Lever's `remote` comes from its workplace type (hybrid and on-site are `false`). Greenhouse has no flag, so it is `true` only when the location text contains the word "Remote", otherwise `null`.
- `employmentType` is passed through as the ATS words it (`FullTime`, `Full-time`, `Permanent`, `fulltime_permanent`). It is not remapped.
- Workable and SmartRecruiters answer "200, zero jobs" for boards that do not exist. Those inputs come back as an error item, as do boards with no open jobs.

### Input

| Field | Meaning |
|---|---|
| `companies` (required) | List of board URLs, `ats:slug` pairs, or careers page URLs. Example: `["greenhouse:airbnb", "https://jobs.lever.co/spotify", "https://www.notion.com/careers"]` |
| `keyword` | Keep jobs whose title contains this text (case-insensitive) |
| `location` | Keep jobs whose location text contains this |
| `remoteOnly` | Keep only jobs with `remote: true` |
| `postedWithinDays` | Keep only jobs published in the last N days. Jobs without a date are dropped |
| `includeDescription` | Add `descriptionText` as plain text |

#### Careers page auto-detect

If an entry is not an ATS URL, the actor downloads that page and looks for embedded ATS links and scripts (for example `boards.greenhouse.io/embed/job_board?for=x`, `jobs.lever.co/x`, `jobs.ashbyhq.com/x`). It uses the board referenced most often. Tested on `notion.com/careers` (Ashby) and `vercel.com/careers` (Greenhouse). Pages that load their job list purely from a different, private API, or that block plain requests, cannot be detected. Use `ats:slug` for those.

#### Unresolvable inputs

An input that cannot be resolved returns one item `{ "input": "...", "error": "..." }`. It is not charged.

### Pricing

Pay per event: **$0.001 per job returned ($1 per 1,000)**, plus Apify's standard start event of $0.00005 per run. Platform compute is included. Error items are free. Set a maximum charge on the run and it stops cleanly when the limit is reached.

### Measured run

One local run, 9 inputs: `greenhouse:airbnb`, `lever:spotify`, `ashby:ramp`, `workable:huggingface`, `smartrecruiters:Ubisoft2`, `https://bunq.recruitee.com`, `https://www.notion.com/careers`, `https://vercel.com/careers`, and one board that does not exist.

- 1,055 jobs in 8.5 seconds (Greenhouse 287 across airbnb and vercel, Lever 89, Ashby 308 across ramp and notion, Workable 6, SmartRecruiters 344, Recruitee 21), plus 1 error item.
- Run from a home connection. Platform timings will differ.

### Limits

- Boards are fetched one company at a time. Very large boards (SmartRecruiters `BoschGroup` has about 5,000 postings) take many pages.
- Lever EU (`jobs.eu.lever.co`) and Greenhouse EU boards are not supported.
- Greenhouse, Lever, Ashby and Workable return the whole board in one response, so memory grows with board size. A 2.7 MB board is normal.

# Actor input Schema

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

One entry per company. Each can be an ATS board URL (boards.greenhouse.io/x, jobs.lever.co/x, jobs.ashbyhq.com/x, apply.workable.com/x, careers.smartrecruiters.com/x, x.recruitee.com), an `ats:slug` pair such as `greenhouse:airbnb`, or the company's own careers page URL (the ATS is auto-detected).

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

Keep only jobs whose title contains this text (case-insensitive).

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

Keep only jobs whose location text contains this (case-insensitive), for example `Berlin` or `United States`.

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

Keep only jobs the ATS marks as remote. Jobs with unknown remote status are dropped.

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

Keep only jobs published in the last N days. Jobs without a publish date are dropped.

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

Add `descriptionText` (plain text) to each job. Slower on SmartRecruiters (one extra request per job).

## Actor input object example

```json
{
  "companies": [
    "greenhouse:airbnb",
    "lever:spotify",
    "https://www.notion.com/careers"
  ],
  "remoteOnly": false,
  "includeDescription": false
}
```

# Actor output Schema

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

No description

# 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": [
        "greenhouse:airbnb",
        "lever:spotify",
        "https://www.notion.com/careers"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("techymaki/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 = { "companies": [
        "greenhouse:airbnb",
        "lever:spotify",
        "https://www.notion.com/careers",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("techymaki/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 '{
  "companies": [
    "greenhouse:airbnb",
    "lever:spotify",
    "https://www.notion.com/careers"
  ]
}' |
apify call techymaki/ats-jobs --silent --output-dataset

```

## MCP server setup

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