# ATS Job Scraper: Greenhouse, Lever, Ashby, Workable, Recruitee (`openquarry/ats-job-extractor`) Actor

Returns open jobs from career boards on Greenhouse, Lever, Ashby, Workable and Recruitee in one normalized JSON format: title, department, location, country, remote/hybrid/onsite, employment type, salary range, posted date, job and apply URLs, plain-text description. Filter by title keywords.

- **URL**: https://apify.com/openquarry/ats-job-extractor.md
- **Developed by:** [OpenQuarry](https://apify.com/openquarry) (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 job returneds

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 Job Scraper: Greenhouse, Lever, Ashby, Workable, Recruitee

Returns the open jobs on companies' career boards hosted by Greenhouse, Lever, Ashby, Workable and Recruitee, all in one normalized JSON format. Each job comes with its title, department, location, country, remote/hybrid/onsite status, employment type, salary range, posted date, job and apply URLs, and the description as plain text. Give it a list of board URLs, optionally with title keywords. It reads each ATS's public job-board API, so you don't need a login, proxy or API key.

### What you get

You get one dataset item per job. All 22 fields are present on every item, in the same order. A field is `null` (or `[]`) when the ATS doesn't provide it.

| Field | Type | Description |
|---|---|---|
| `ats` | string | `greenhouse`, `lever`, `ashby`, `workable` or `recruitee` |
| `companySlug` | string | Company identifier from the board URL, e.g. `stripe` |
| `companyName` | string or null | Company name as the employer publishes it |
| `jobId` | string | The ATS's own ID for the job |
| `title` | string | Job title as published |
| `department` | string or null | Department or team |
| `location` | string or null | Main location as published, e.g. `New York, NY` or `Remote (US)` |
| `allLocations` | string\[] | Every location listed for the job, main location first |
| `countryCode` | string or null | ISO 3166-1 alpha-2 code of the main location, e.g. `US` |
| `workplaceType` | string or null | `remote`, `hybrid` or `onsite` |
| `employmentType` | string or null | `full_time`, `part_time`, `contract`, `internship` or `temporary` |
| `salaryMin` | number or null | Lowest base pay in the published range |
| `salaryMax` | number or null | Highest base pay in the published range |
| `salaryCurrency` | string or null | Currency of the pay range, e.g. `USD` |
| `salaryPeriod` | string or null | `year`, `month`, `week`, `day` or `hour` |
| `salaryText` | string or null | The employer's own pay summary, e.g. `$211.4K – $290.6K • Offers Equity` |
| `postedAt` | string or null | When the job was published (ISO 8601, UTC) |
| `updatedAt` | string or null | When the job was last updated (ISO 8601, UTC) |
| `jobUrl` | string | Public job posting page |
| `applyUrl` | string | Application page (same as `jobUrl` when the ATS has no separate one) |
| `descriptionText` | string or null | Full job description as plain text; `null` when `includeDescription` is off |
| `sourceUrl` | string | The input URL this job came from |

`workplaceType`, `employmentType` and `salaryPeriod` take their values only from the ATS's own fields. They are never guessed from the description text, so you can filter on them reliably.

#### Which fields each ATS fills

Each ATS's public API publishes different fields. This table shows which fields can be filled for each one. Salary appears only when the employer publishes it.

| Field | Greenhouse | Lever | Ashby | Workable | Recruitee |
|---|---|---|---|---|---|
| `companyName` | ✓ | – | – | ✓ | ✓ |
| `countryCode` | – | ✓ | ✓ | ✓ | ✓ |
| `workplaceType` | – | ✓ | ✓ | remote only | ✓ |
| `employmentType` | – | ✓ | ✓ | ✓ | ✓ |
| Salary range | – | ✓ | ✓ | – | ✓ |
| `salaryText` | – | ✓ | ✓ | – | – |
| `updatedAt` | ✓ | – | – | – | ✓ |

### Input

```json
{
    "urls": [
        "https://boards.greenhouse.io/stripe",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp"
    ],
    "titleKeywords": ["engineer", "data scientist"],
    "maxJobsPerCompany": 100,
    "includeDescription": true
}
```

- **`urls`** (required): career board URLs. A job URL on the board works too, and so does the board's API URL. These formats are recognized:

  | ATS | URL formats |
  |---|---|
  | Greenhouse | `boards.greenhouse.io/{company}`, `job-boards.greenhouse.io/{company}`, `boards.greenhouse.io/embed/job_board?for={company}` |
  | Lever | `jobs.lever.co/{company}`, `jobs.eu.lever.co/{company}` (EU accounts) |
  | Ashby | `jobs.ashbyhq.com/{company}` |
  | Workable | `apply.workable.com/{company}`, `{company}.workable.com` |
  | Recruitee | `{company}.recruitee.com` |

  `https://` is optional. If the same board appears twice, it's fetched once.
- **`titleKeywords`**: return only jobs whose title contains at least one of these words or phrases. Matching ignores case and works at word starts, so `engineer` matches "Engineering Manager", but `AI` doesn't match "Retail". Leave it empty to get every job.
- **`maxJobsPerCompany`**: the most jobs returned for each board, counted after the title filter (default 100; `0` means no limit).
- **`includeDescription`**: include the full description in `descriptionText` (default `true`). Turn it off when you only need titles, locations and links. Results get much smaller.

### Sample output

The descriptions are shortened here.

```json
[
    {
        "ats": "ashby",
        "companySlug": "ramp",
        "companyName": null,
        "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
        "title": "Security Engineer, Cloud",
        "department": "Engineering",
        "location": "New York, NY (HQ)",
        "allLocations": ["New York, NY (HQ)", "Remote (Canada)", "Remote (US)", "Miami, FL"],
        "countryCode": "US",
        "workplaceType": "hybrid",
        "employmentType": "full_time",
        "salaryMin": 211400,
        "salaryMax": 290600,
        "salaryCurrency": "USD",
        "salaryPeriod": "year",
        "salaryText": "$211.4K – $290.6K • Offers Equity",
        "postedAt": "2026-04-07T17:12:35.753Z",
        "updatedAt": null,
        "jobUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
        "applyUrl": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
        "descriptionText": "ABOUT RAMP\n\nRamp is building the smart infrastructure for finance teams...",
        "sourceUrl": "https://jobs.ashbyhq.com/ramp"
    },
    {
        "ats": "greenhouse",
        "companySlug": "stripe",
        "companyName": "Stripe",
        "jobId": "8172503",
        "title": "Abuse Research Engineer",
        "department": "8611 Security Analytics",
        "location": "Remote from the US",
        "allLocations": ["Remote from the US"],
        "countryCode": null,
        "workplaceType": null,
        "employmentType": null,
        "salaryMin": null,
        "salaryMax": null,
        "salaryCurrency": null,
        "salaryPeriod": null,
        "salaryText": null,
        "postedAt": "2026-09-09T14:52:09.000Z",
        "updatedAt": "2026-09-25T20:45:00.000Z",
        "jobUrl": "https://stripe.com/jobs/search?gh_jid=8172503",
        "applyUrl": "https://stripe.com/jobs/search?gh_jid=8172503",
        "descriptionText": "Who we are\n\nAbout Stripe\n\nStripe is a financial infrastructure platform for businesses...",
        "sourceUrl": "https://boards.greenhouse.io/stripe"
    }
]
```

### Use cases

- **Job aggregators and niche job boards:** pull fresh postings from a list of target employers in one format, whichever ATS each one uses.
- **Hiring signals for sales and investors:** track which companies are hiring, for which teams and where, as a growth signal.
- **Salary research:** collect published pay ranges by title, location and company.
- **Recruiting and job search:** watch a shortlist of companies for new roles that match your keywords. Run it on a schedule and compare `jobId`s between runs.
- **AI agents:** one call returns structured, filterable jobs from many companies, with no HTML to parse.

### How it handles problems

The run never fails because one board is broken. Jobs from the other boards are still returned, and the run's status message lists every input that produced no jobs, with the reason:

- `board not found (HTTP 404), check the company name in the URL`: the company slug is wrong, or the company has left that ATS.
- `not a Greenhouse, Lever, Ashby, Workable or Recruitee board URL`: the input isn't in one of the formats above.
- `no open jobs on this board`: the board exists but lists no jobs right now.
- `no job titles matched titleKeywords`: the board has jobs, but none match your keywords.

Network errors, rate limits (HTTP 429) and server errors are retried twice with backoff, and each request times out after 30 seconds.

### Limits

- **Custom career domains aren't supported.** `stripe.com/jobs` won't work, but the company's ATS board (`boards.greenhouse.io/stripe`) will. To find it, open any job on the company's careers page and look for a `greenhouse.io`, `lever.co`, `ashbyhq.com`, `workable.com` or `recruitee.com` link.
- **The Actor doesn't look up companies by name.** You need each company's board URL.
- **Greenhouse publishes less.** Its public API has no pay, country, remote status or employment type, so those fields are `null` for Greenhouse jobs. The `location` text often says "Remote" anyway.
- **Only jobs the employer shows publicly are returned.** Unlisted Ashby jobs are skipped, and pay is hidden when the employer hides it on its own board.
- **No personal data is added.** The Actor never extracts or adds contact names or emails. Recruitee's per-job reply inbox and screening questions are dropped. Descriptions are passed through as published.
- **Other ATSs aren't supported:** SmartRecruiters, Workday, BambooHR, Personio, Teamtailor and others.
- **Big boards:** some employers list 1,000+ jobs. The default limit of 100 per company keeps runs to a few seconds. With no limit, a run over several large boards takes longer and costs more.

### Pricing

Pay per result: **$1.00 per 1,000 jobs** returned (all 718 jobs on Stripe's board cost about $0.72). Apify adds a small start fee of $0.00005 per run. Boards that return no jobs cost you nothing beyond that start fee. To cap spending, use `maxJobsPerCompany`, `titleKeywords` or the run's maximum cost setting. The Actor stops cleanly when your limit is reached.

### Changelog

- **0.1** (2026-10): First release. Supports Greenhouse, Lever, Ashby, Workable and Recruitee.

# Actor input Schema

## `urls` (type: `array`):

Company job boards on Greenhouse, Lever, Ashby, Workable or Recruitee, e.g. "boards.greenhouse.io/stripe", "jobs.lever.co/palantir", "jobs.ashbyhq.com/openai", "apply.workable.com/huggingface", "bunq.recruitee.com". A job URL on the board also works. Custom career domains (e.g. stripe.com/jobs) are not supported.

## `titleKeywords` (type: `array`):

Return only jobs whose title contains at least one of these words or phrases (case-insensitive, matched at the start of a word, so "engineer" also matches "Engineering"). Leave empty for all jobs.

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

Maximum jobs returned for each board, after the title filter. 0 means no limit. Large employers list 1,000+ jobs, so keep this low for fast runs.

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

Return the full job description as plain text in descriptionText. Turn off for much smaller results when you only need titles, locations and links.

## Actor input object example

```json
{
  "urls": [
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "titleKeywords": [],
  "maxJobsPerCompany": 100,
  "includeDescription": true
}
```

# 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 = {
    "urls": [
        "https://boards.greenhouse.io/stripe",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("openquarry/ats-job-extractor").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 = { "urls": [
        "https://boards.greenhouse.io/stripe",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("openquarry/ats-job-extractor").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 '{
  "urls": [
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ]
}' |
apify call openquarry/ats-job-extractor --silent --output-dataset

```

## MCP server setup

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

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/vuqGGnplaysVB0bn7/builds/5XldDdVXshhWUWcPF/openapi.json
