# Greenhouse, Lever & Ashby Job Postings Scraper (`creatortsv/greenhouse-lever-ashby-ats-jobs-scraper`) Actor

\[From $1.00/1K, no usage fees] Export open jobs from public Greenhouse, Lever and Ashby career boards into one schema: title, location, remote flag, department, seniority, tech tags and salary where the employer publishes it.

- **URL**: https://apify.com/creatortsv/greenhouse-lever-ashby-ats-jobs-scraper.md
- **Developed by:** [Vladimir Tsaplin](https://apify.com/creatortsv) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.70 / 1,000 results

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?

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

## Greenhouse, Lever & Ashby Job Postings Scraper

Export Greenhouse, Lever and Ashby jobs from public career boards into one table, with full descriptions. Use it to see who is hiring, where and for what. Name a company, paste a board URL, or search the built-in catalogue of 8,661 boards. Sample record from a real run on 2026-10-31:

```json
{
  "ats_provider": "ashby",
  "company_name": "Ramp",
  "title": "Machine Learning Engineer",
  "department": "Data",
  "location": "New York, NY (HQ)",
  "workplace_type": "hybrid",
  "is_remote": false,
  "employment_type": "Full-time",
  "salary_min": 200000.0,
  "salary_max": 330000.0,
  "salary_currency": "USD",
  "salary_interval": "YEAR",
  "salary_summary": "200,000 - 330,000 USD / YEAR",
  "salary_source": "ats_structured",
  "tech_stack_tags": [
    "python",
    "postgres",
    "pytorch",
    "llm",
    "snowflake",
    "dbt"
  ],
  "published_at": "2026-09-23T19:39:11.015+00:00",
  "job_url": "https://jobs.ashbyhq.com/ramp/2888b101-b1da-4e53-a02e-1bb9b1b5a951"
}
```

### What you get

| Field | Example | Notes |
|---|---|---|
| `title` | Machine Learning Engineer | As published |
| `company_name` | Ramp | From Greenhouse. Ashby and Lever do not publish it, so it comes from the board address and can differ in spelling, such as Openai |
| `location` | New York, NY (HQ) | As published. All offices are in `offices` |
| `workplace_type` | hybrid | remote, hybrid or onsite, as the ATS publishes it. Otherwise from the location text |
| `is_remote` | false | True when the ATS or the location says remote, which includes a choice of offices or remote, as in San Francisco; Seattle; Remote. False for hybrid and on-site jobs. Null when neither the ATS nor the location says |
| `department` | Data | As published |
| `employment_type` | Full-time | Full-time, Part-time, Contract, Temporary or Internship, read from whole words of what the employer wrote. Ashby and Lever publish it for most jobs, Greenhouse only where the employer adds an Employment Type field. Other values, such as Regular or Per Diem, stay null, and so does a value that offers a choice, such as Full Time/Part Time. A term beats the hours, so Contract Full time is Contract |
| `seniority_level` | null | Only when the title says it: entry, senior, lead_staff or executive. The word Staff counts only in front of a role, as in Staff Engineer, not in Staff Nurse or Member of Technical Staff. Chief of Staff and Lead Generation Specialist name no level. Otherwise null |
| `tech_stack_tags` | python, postgres, pytorch, llm, snowflake, dbt | 24 technologies found in the title and description |
| `salary_summary` | 200,000 - 330,000 USD / YEAR | Only where the employer publishes it. The amounts are printed as published, with cents only where they are not whole, as in 16.50 - 17.50 USD / HOUR |
| `salary_ranges` | One entry per range | Every range the job publishes, each with its amounts, currency, interval and label |
| `published_at` | 2026-09-23T19:39:11.015+00:00 | Publication time from Greenhouse and Ashby. Lever publishes none, so for Lever this is when the posting was created |
| `description` | Full text | The whole job description as plain text, not a snippet. For Lever that is the text its public job page shows, with the lists, pay note and closing section. Phone numbers, email addresses that may name a person, personal profile or scheduling links and names given as someone to contact are masked. See the FAQ for what stays |
| `apply_url` | Link | Application page |

### Three ways to choose boards

- **Company names.** Enter names such as Stripe or Linear. The Actor reads the catalogue board with exactly that name. A few names belong to 2 or 3 boards, often of different companies, and then all of them are read. A name it does not know is tried as a board address on the systems you selected, or on all of them if you selected none, so another company with that address can answer. Turn on **Also match names that start with this text** to include near names: Meta then also finds Metabase. `RUN_SUMMARY` lists the board each name reached.
- **Board URLs or slugs.** Paste `https://boards.greenhouse.io/figma`, or write `greenhouse:figma`, `ashby:linear` or `lever:palantir`. Boards on Lever's EU instance work the same way: `https://jobs.eu.lever.co/some-company` or `lever_eu:some-company`. Lever tells upper from lower case, so write such a board as its address does, as in `lever:BeemEnergy`. Ashby boards can hold spaces, as in `ashby:The Browser Company`.
- **Catalogue search.** Turn on **Search the whole company catalogue**, then add a keyword or location. It reads the largest boards first, up to **Boards to read from the catalogue**. See Limits for how many boards a run gets through.

The catalogue holds 8,661 boards (4,220 Greenhouse, 2,803 Ashby, 1,638 Lever) that had at least one open job on 2026-10-31. Each one was checked against the board's own API on that date, and vendor demo boards and sandbox or test instances were left out. The 100 largest are 68 Greenhouse, 20 Lever and 12 Ashby boards. It is a list of where to look: the jobs themselves are read live on every run. It holds no boards on Lever's EU instance yet, no Lever boards with capitals in their address and no Ashby boards with spaces in theirs, so give those by URL or by name. A name outside the catalogue is also tried as you typed it, so The Browser Company reaches its Ashby board.

A slug or name without a system is tried on each ATS until one answers with open jobs. An empty board on one system does not end the search, and `probed_boards` in `RUN_SUMMARY` shows which board answered and which were empty.

### Salary coverage

A salary appears only when the employer publishes it, so many jobs have none. In one run on 2026-10-31 over every open job on 8 technology company boards (Figma, Stripe, Brex, Affirm, Linear, Ramp, Notion and Palantir), 2,043 jobs in all, the salary was filled for 550 of 1,394 Greenhouse jobs, 264 of 350 Ashby jobs and 193 of 299 Lever jobs. Expect it to vary by employer.

`salary_source` shows where a salary came from: `ats_structured` is the ATS pay field, `description_text` is a range found in the job text. A range in the text counts only when the words before it name pay, so equity grants, bonuses and phrases such as high-wage careers are left out. In job text, a range below 15,000 or stated per hour, day or week is not captured, so hourly pay such as $28 - $32 per hour stays null unless the ATS pay field holds it. A range in the text is read when a sign stands in front of it, with or without cents, as in $156,800.00 - $235,200.00/yr; when it is written between $130,000 and $155,000; or when a currency code stands right behind it and there is no sign, as in 101,000 - 151,000 CAD. Two amounts joined by and without the word between, and a pair of numbers with neither a sign nor a code, are missed. Compact and European forms such as $135-145k and 70.000 bis 80.000 EUR are also missed.

`salary_basis` is `ote` when the employer states on-target earnings, which include commission, in the pay field or in the words that qualify the range. In job text, that is the word directly behind a range, or else the nearest one in front of it. In a pay field, it is the label of that range, and an OTE word with a figure of its own outside the range is about that figure: a $150K - $200K salary with the note $300-400k OTE has a null basis. Sales jobs often give both figures, as in "base salary range $80,000 - $115,000, with a target OTE of $160,000 - $230,000": the base range then has a null basis and the OTE range has `ote`. `salary_min`, `salary_max` and `salary_basis` describe the first range the employer lists, and `salary_ranges` holds each range with its own basis. A null basis means no such word governs that range, not that it is proven base pay. OTE given as one amount and not as a range, such as "OTE of up to $350,000", is not captured, and neither is an OTE figure in a pay field note.

Employers type these figures by hand, so a pay field that cannot be real is dropped and the Actor then looks in the job text, leaving the salary null if there is none. That covers placeholders such as 1 - 1, a maximum over 10 times the minimum, amounts below a floor for their interval (3 per hour, 10 per day, 50 per week, 100 per month, 1,000 per year) and, in USD, EUR, GBP, CAD, AUD, CHF, SGD and NZD, amounts above a ceiling (1,000 per hour, 20,000 per day, 50,000 per week or month, 2,500,000 per year). Of 12,521 pay ranges sampled from 721 boards on 2026-10-31, this dropped 235, such as 93,600 USD per hour and 40 USD per year. Lever pay every two weeks or twice a month keeps its amounts, with `salary_interval` null.

`salary_currency` is null when a range shows only a bare $ and no currency is named next to it, because $ can also mean CAD, AUD or others. That was 603 of the 1,007 salaries in the run above, so the amount is there but the currency is yours to judge. `salary_interval` is null when the text does not say the range is per year or per month, either in front of it, as in annual salary range, or right behind it, as in $100,000 - $120,000 annually: the Actor does not assume it. A unit word that goes with something else stays out of it, as in $100,000 - $120,000 plus annual bonus. That was 273 of the 1,007.

Some jobs publish several ranges, one per pay zone or level: 139 jobs in the run above. `salary_min`, `salary_max` and `salary_summary` then show the first range the employer lists, which is often the highest zone, and `salary_ranges` holds all of them with their labels. Ashby jobs show the span across their tiers in the first fields and each tier in `salary_ranges`. For pay benchmarks, use `salary_ranges`.

### How much does it cost to scrape Greenhouse jobs?

You pay per job in the output, with no usage fees.

| Apify plan | Price per 1,000 jobs | 10,000 jobs |
|---|---|---|
| Free | $1.00 | $10.00 |
| Starter (Bronze) | $0.90 | $9.00 |
| Scale (Silver) | $0.80 | $8.00 |
| Business (Gold) | $0.70 | $7.00 |

A run also carries Apify's small start fee of $0.00005 per run for each GB of memory. Set **Maximum jobs in total** to cap a run. A board that fails or has no open jobs creates no result and no charge.

### How to run it

1. Enter company names or board URLs, or turn on the catalogue search.
2. Add a keyword, location or **Remote jobs only** if you want.
3. Press **Start** and download CSV, JSON or Excel from the **Output** tab.

A keyword is searched as text in the title, department and team, so engineer also finds Engineering and java also finds a title such as JavaScript Engineer. A technology tag counts only as a whole value: python finds jobs tagged python, while java leaves out a job tagged only javascript and sales leaves out one tagged only salesforce. **Also search full descriptions** adds the job text, also searched as text.

To follow hiring over time, save the input as a task, add a schedule and compare the runs.

### Limits

- Up to 5,000 jobs per board and 1,000 boards per catalogue search. The 100 largest boards hold about 88,000 jobs and the 1,000 largest about 182,000.
- How far a catalogue search gets in the default 15 minutes depends on its filters and on CPU, which follows memory on Apify: 1 GB gets a quarter of a core and 4 GB a full core. A filter rejects a job before its text is processed, so a keyword or location gets through far more boards than no filter at all. The table shows runs of the release image on 2026-10-31, on a fast machine limited to that CPU share and to half of it, because a cloud core can be slower. They ran against the live boards but not on Apify, so treat them as a guide.

| Catalogue run | CPU share of the memory | Half of that share |
|---|---|---|
| 1 GB, keyword in titles such as engineer, 25 jobs per board, 100 boards | All 100 in 5 minutes | All 100 in 6 minutes |
| 1 GB, the same over 1,000 boards | All 1,000 in 15 minutes | 512 boards finished |
| 1 GB, keyword that is a technology tag, such as python, 25 jobs per board, 100 boards | All 100 in 6 minutes | 76 boards finished |
| 1 GB, no filter, 5,000 jobs per board, 100 boards | 54 boards finished, 71,707 jobs | 4 boards finished, 17,818 jobs |
| 4 GB, no filter, 5,000 jobs per board, 100 boards | All 100, 88,489 jobs, in 4 minutes | All 100 in 7 minutes |

- A search with **Also search full descriptions** costs about the same as the technology tag row.
- A run that reaches its timeout keeps what it read and still succeeds. `RUN_SUMMARY` and the status message say how many boards it did not read, and `not_processed_targets` names them. Boards run largest first, so the ones left are the smaller ones. To reach them, add a filter, lower **Boards to read from the catalogue**, choose fewer **Systems to use**, raise the memory or raise the timeout.
- **Maximum jobs in total** defaults to 1,000 and can end a run first: the keyword engineer at 25 jobs per board reached it after 74 of 100 boards. Raise it to read them all.
- Open jobs only, as they stand at the moment of the run. The Actor keeps no history of closed jobs. Ashby jobs the employer marks as unlisted are left out, because they are not on the public board.
- A board with no open jobs is reported in `RUN_SUMMARY` as `BOARD_EMPTY`. A run in which no board had any ends as failed, not as a green run with an empty dataset. A board with open jobs of which none pass your filters is a normal result.
- A job object with no id or title is left out and not charged. Employers do publish these, so one does not cost you the rest of the board: `RUN_SUMMARY` counts them in `jobs_skipped` and per board in `jobs_skipped_by_board`. Greenhouse and Ashby boards are counted whole. On Lever, the count covers the pages the run read. A job with a title but no text at all stays in the output with an empty `description`: 39 of 13,437 Lever jobs sampled on 2026-10-31 were published that way.
- A board entry that is not a job object, or a board where no entry has an id and a title, fails that board with `UNEXPECTED_RESPONSE`. That usually means the ATS changed its API, though a board whose only posting is blank fails the same way. Nothing from it is stored or charged.
- Greenhouse has no employment type field of its own, so that column is filled only where the employer adds one. Most boards do not. Lever and Ashby publish it for most jobs.
- Large boards take a lot of memory, so the Actor needs at least 1 GB and reads fewer boards at once at lower memory settings. At the default 1 GB it reads two.
- Lever asks for one request per second, and the Actor keeps that pace across the whole run, so 100 Lever boards take at least 100 seconds at any memory setting. A name outside the catalogue can take a few requests on each Lever instance, one per spelling.
- The catalogue is a snapshot. A company that joined later still works by name or URL.

### FAQ

**How do I find a company's slug?** You do not need to: enter the company name. If you want it, it is the last part of the board address, such as `figma` in `boards.greenhouse.io/figma`.

**Can a description search find a masked contact?** No. **Also search full descriptions** reads the text as it is stored, with contacts already masked, so a masked name or address finds nothing.

**Is it legal to collect these jobs?** The Actor reads the public job board APIs that these systems offer for publishing open positions, with no login. The job text stays the employer's content, so check your rights before you republish it.

**A company is missing from the results. Why?** Open `RUN_SUMMARY` in the run's key-value store. It lists each failed board with a code such as `BOARD_NOT_FOUND` or `BOARD_EMPTY`, and `not_processed_targets` names the boards a run did not finish, for example because it reached **Maximum jobs in total**. Many companies use another ATS, and the Actor reads only these three.

**Does it collect recruiter names or contacts?** There are no such fields. A description is kept as the employer published it, except for contact details: a phone number becomes `[phone removed]`, an email address that may name a person becomes `[email removed]`, and a LinkedIn or XING profile link or a personal scheduling link such as Calendly becomes `[link removed]`. A name given as someone to contact becomes `[name removed]`: next to a contact (contact Jane Doe at ...), in a signature above one, on a line of its own under a label such as Recruiter: or Hiring Manager:, or below a line that announces a contact person. Shared mailboxes such as careers@ and company pages stay. A company helpline is masked too, because a number alone does not say whose it is.

**Can a person's name still appear in a description?** Yes. Names in ordinary job text stay as published, such as "you will report to Jane Doe" or "an interview with Jane, our recruiter". So do first names next to a masked profile link when no contact word leads to them, as in a list of founders. Telling a name from other capitalized words takes more than a pattern, so masking is a best effort: a number in an unusual format or a name in an unusual layout can get through, and now and then a capitalized word next to a contact, or a long number such as a contract clause, is masked by mistake. Treat the output as public job text, not as text free of personal data, and do not use it to look up or contact people.

### Call it from your own code

```bash
curl -X POST "https://api.apify.com/v2/acts/creatortsv~greenhouse-lever-ashby-ats-jobs-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companyNames": ["Stripe", "Linear"], "keywordFilter": "engineer", "maxItemsPerCompany": 25}'
```

### Scope

**What's Included:** open jobs from public Greenhouse, Lever and Ashby boards, full descriptions, location, remote flag, department, tech tags, salary where the employer publishes it, a built-in company catalogue, per-board failure report.

**What's Not Included:** other ATS systems, company career sites outside these three, closed jobs, job history, salary estimates, candidate data, recruiter contact fields.

Keywords: greenhouse jobs scraper, lever jobs scraper, ashby jobs scraper, ats job postings api, job board data, salary data, hiring signals, company job listings.

# Actor input Schema

## `targets` (type: `array`):

Board URLs such as https://boards.greenhouse.io/stripe, or 'greenhouse:stripe', 'ashby:linear', 'lever:palantir'. Lever's EU instance works too: https://jobs.eu.lever.co/some-company. Write a Lever slug in the case of its address, as in 'lever:BeemEnergy'. A bare slug such as 'stripe' is tried on the systems chosen below until one has open jobs.

## `companyNames` (type: `array`):

Company names to look up in the built-in catalogue, so that you do not need to know the slug. Only the exact name counts. A name that is not in the catalogue is tried as a slug on the systems chosen below until one has open jobs, so another company with that slug can answer.

## `matchNamePrefix` (type: `boolean`):

When a name has no exact match, reads up to 5 boards whose name starts with it. These are often other companies: Meta also finds Metabase. RUN_SUMMARY lists the board each name reached.

## `useCompanyCatalog` (type: `boolean`):

Read the largest boards in the built-in catalogue instead of naming boards. Use it with a keyword or location filter.

## `atsProviders` (type: `array`):

Limits company names, bare slugs and catalogue search to these systems. Empty means all three. A board you give by its URL is read wherever it is.

## `maxCompanies` (type: `integer`):

How many catalogue boards to read, largest first (1 to 1000).

## `keywordFilter` (type: `string`):

Keeps jobs with this text in the title, department or team, or with exactly this technology tag, ignoring case. A tag counts only as a whole value, so java does not find a job tagged only javascript, and sales does not find one tagged only salesforce.

## `searchDescriptions` (type: `boolean`):

Applies the keyword to the whole job description too. Company boilerplate can then match common words.

## `locationFilter` (type: `string`):

Keeps jobs with this text in the location or office list, for example 'London' or 'Germany'.

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

Keeps jobs the ATS or the location marks as remote, including a choice of offices or remote. Hybrid and on-site jobs are left out, even when the ATS flags a hybrid job as remote.

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

Matching jobs to keep from each board (1 to 5000).

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

The run stops at this many jobs. This caps what you pay.

## Actor input object example

```json
{
  "targets": [
    "https://boards.greenhouse.io/figma",
    "https://jobs.ashbyhq.com/linear",
    "https://jobs.lever.co/palantir"
  ],
  "matchNamePrefix": false,
  "useCompanyCatalog": false,
  "maxCompanies": 100,
  "searchDescriptions": false,
  "remoteOnly": false,
  "maxItemsPerCompany": 10,
  "maxItems": 30
}
```

# Actor output Schema

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

Open jobs

## `runSummary` (type: `string`):

Per-target report: what succeeded, what failed and why. Failed targets create no result and no charge.

# 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 = {
    "targets": [
        "https://boards.greenhouse.io/figma",
        "https://jobs.ashbyhq.com/linear",
        "https://jobs.lever.co/palantir"
    ],
    "maxItemsPerCompany": 10,
    "maxItems": 30
};

// Run the Actor and wait for it to finish
const run = await client.actor("creatortsv/greenhouse-lever-ashby-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 = {
    "targets": [
        "https://boards.greenhouse.io/figma",
        "https://jobs.ashbyhq.com/linear",
        "https://jobs.lever.co/palantir",
    ],
    "maxItemsPerCompany": 10,
    "maxItems": 30,
}

# Run the Actor and wait for it to finish
run = client.actor("creatortsv/greenhouse-lever-ashby-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 '{
  "targets": [
    "https://boards.greenhouse.io/figma",
    "https://jobs.ashbyhq.com/linear",
    "https://jobs.lever.co/palantir"
  ],
  "maxItemsPerCompany": 10,
  "maxItems": 30
}' |
apify call creatortsv/greenhouse-lever-ashby-ats-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,creatortsv/greenhouse-lever-ashby-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/cQYxXqbebN3HNbkdX/builds/ud23ltKQ1TVwtXzON/openapi.json
