# Greenhouse Jobs Scraper: Job Board API for Any Company (`stippled_journey/greenhouse-jobs`) Actor

Every open job from any Greenhouse job board, or search 3,489 Greenhouse career sites by job title and location. Salary ranges, seniority, skills and visa sponsorship on every row. New, changed and removed jobs on a schedule. Pay per job.

- **URL**: https://apify.com/stippled_journey/greenhouse-jobs.md
- **Developed by:** [Navista Multani](https://apify.com/stippled_journey) (community)
- **Categories:** Jobs, Developer tools
- **Stats:** 2 total users, 1 monthly users, 42.9% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 job rows

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 Jobs Scraper: Job Board API for Any Company

A Greenhouse job board API for any company: get every open job from a company's Greenhouse job board (job-boards.greenhouse.io) in one clean format, or search 3,476 Greenhouse career sites by job title and location. Salary ranges, seniority, skills and visa sponsorship on every row, and new, changed and removed jobs on a schedule.

This Actor reads Greenhouse boards only. To mix platforms in one run (Greenhouse, Lever, Workday, Ashby, Workable and 8 more), use [Career Site Jobs API](https://apify.com/stippled_journey/ats-jobs) (`stippled_journey/ats-jobs`).

### Greenhouse job board API: what you can use it for

- **Search Greenhouse jobs across companies.** Look for a job title and location across
  every Greenhouse board in the directory at once, instead of one company at a time.
- **Monitor the careers pages you care about.** List competitors, customers or portfolio
  companies and get only their new, changed and removed jobs each day.
- **Collect salary data.** Many US Greenhouse postings carry the employer's pay range,
  which comes back as a structured `salary` with minimum, maximum, currency and period.
- **Filter on seniority, years of experience, skills and visa sponsorship** without
  reading descriptions: each is a column on every row.
- **Feed a job board, newsletter or AI agent.** Every row stands alone, with the
  employer's own apply link.

### Greenhouse job posting API: sample output

One real row (discord on Greenhouse, read 2026-10-27; description shortened):

```json
{
  "id": "greenhouse:discord:8885940002",
  "source": "greenhouse",
  "board": "discord",
  "company": "Discord",
  "title": "Engineering Manager, Notifications",
  "department": "Product Engineering",
  "team": null,
  "category": null,
  "employment_type": null,
  "workplace_type": "remote",
  "location": "Remote (U.S.)",
  "locations": [
    "Remote (U.S.)",
    "Remote (US)"
  ],
  "country": null,
  "salary": {
    "min": 248000.0,
    "max": 310000.0,
    "currency": "USD",
    "period": "year",
    "source": "pay_range_html"
  },
  "salary_text": null,
  "seniority": "manager",
  "experience_years_min": 5,
  "visa_sponsorship": null,
  "skills": [
    "Python",
    "Elixir",
    "Kubernetes"
  ],
  "posted_at": "2026-10-13T17:34:29-04:00",
  "updated_at": "2026-10-20T19:56:15-04:00",
  "url": "https://job-boards.greenhouse.io/discord/jobs/8885940002",
  "apply_url": "https://job-boards.greenhouse.io/discord/jobs/8885940002",
  "description_text": "Discord has a highly engaged community of millions of daily active users who use the platform for many differe ...",
  "content_hash": "bf6f7c69d9dabfbd",
  "scraped_at": "2026-10-27T16:30:44.771773Z"
}
```

What each row holds:

- **Job**: `title`, `company`, `department`, `employment_type` (when the board sets a time-type field), `url`, `apply_url`, `posted_at` (first published), `updated_at`
- **Place**: `location`, `locations` (the job's location plus every office it lists), `workplace_type` (`remote` or `hybrid`, read from the location text; Greenhouse has no workplace field)
- **Pay**: `salary` as `{min, max, currency, period, source}` from the pay-range block employers add to Greenhouse postings (`source: "pay_range_html"`), or from a pay range stated in the description
- **Read from the posting by fixed rules, on every row**:
  - `seniority`: intern, entry, mid, senior, lead, manager, director or executive, from the title
  - `experience_years_min`: the years of experience the description asks for ("5+ years" gives 5)
  - `visa_sponsorship`: `true` if the posting says it sponsors visas, `false` if it says it does not, `null` if it says neither
  - `skills`: tools and skills named in the posting (Python, AWS, Salesforce, SQL, Figma and about 480 more)
- **Text**: `description_text` (plain text, email addresses and phone numbers removed), optionally `description_html`
- **Tracking**: stable `id`, `source`, `board`, `content_hash`, `scraped_at`, and in change mode `change` (`new`, `changed`, `removed`) and `removed_at`

Greenhouse's public board feed has no `team`, `category` or `country`; those stay `null`.

The `OUTPUT` record summarizes the run: boards loaded, not found or failed, jobs found and returned. A board that does not exist does not fail the run; a run where no board loads does. The dataset has three views: *Jobs*, *Seniority, skills and visa*, and *Changes*.

### How to find a Greenhouse board

Open the company's careers page and click any job. Greenhouse boards live at one of these addresses, and the company's slug is the first part of the path:

| You see | Board | Short form |
|---|---|---|
| `https://job-boards.greenhouse.io/stripe` | Stripe | `greenhouse:stripe` |
| `https://boards.greenhouse.io/airbnb/jobs/<id>` | Airbnb (any job page works) | `greenhouse:airbnb` |
| `https://job-boards.eu.greenhouse.io/<slug>` | an EU-hosted board | `greenhouse:<slug>` |
| `https://boards.greenhouse.io/embed/job_board?for=discord` | Discord (the embed form) | `greenhouse:discord` |

If the careers page is on the company's own domain and its job links carry `gh_jid=`, it embeds a Greenhouse board: the slug is usually in the page source as `for=<slug>` or `boards.greenhouse.io/<slug>`. Paste the platform URL or the short form; a `careers.example.com` address on its own is not recognized.

### Pricing

Pay per job row returned, $2.00 per 1,000 jobs. No start fee and no charge for jobs your filters drop. A removed-job row in change mode counts as one row. The run stops cleanly at your spending limit.

- Test run, 20 jobs: $0.04.
- Stripe's whole board (712 open jobs on 2026-10-27): $1.42.
- Directory search for one job title, 200 jobs: $0.40.
- Monitoring 25 boards daily: a first full run of about 2,500 jobs costs $5.00; after that a daily change-mode run with about 50 new, changed or removed jobs costs about $0.10, about $3 a month.

### Greenhouse job search API: input examples and settings

#### Search Greenhouse career sites by title and location

```json
{
  "keywords": [
    "data engineer",
    "analytics engineer"
  ],
  "locations": [
    "New York",
    "Remote"
  ],
  "seniority": [
    "senior",
    "lead"
  ],
  "postedWithinDays": 14,
  "maxJobs": 200
}
```

With no `boards`, the run searches the built-in directory of Greenhouse boards live, starting with the boards most likely to match (those whose titles and locations contained your words when the directory was built, largest first). It stops at `maxJobs` matches (500 when `maxJobs` is 0) or when `searchTimeLimitSecs` (default 150) runs out. At the time limit, boards still being read are cut off and the jobs from boards already read are returned, so the run ends close to the limit; its status says how many boards it searched and how many were cut off. A Greenhouse search first reads each board's light job list (titles and locations only), then fetches the full posting only for jobs whose title and location match, so it covers many boards quickly. Boards of any size are searched.

#### Read the Greenhouse boards you choose, in full

```json
{
  "boards": [
    "https://job-boards.greenhouse.io/stripe",
    "https://job-boards.greenhouse.io/airbnb",
    "greenhouse:discord",
    "greenhouse:dropbox"
  ],
  "workplaceTypes": [
    "remote"
  ]
}
```

#### All input fields

| Field | What it does |
|---|---|
| `boards` | Greenhouse career-site URLs or short forms (see *How to find a Greenhouse board*). Empty: search the directory. |
| `keywords` | Title words to look for (any of them). Without boards, these are the directory search. |
| `locations` | Keep jobs whose location or country contains any of these, for example `London`, `Remote`, `United States`. |
| `maxJobs` | Stop after this many jobs, newest first. `0`: no limit with boards, 500 for a directory search. |
| `workplaceTypes` | `remote`, `hybrid`, `onsite`. Jobs whose posting does not say are dropped while this is set. |
| `seniority` | Keep these levels: `intern`, `entry`, `mid`, `senior`, `lead`, `manager`, `director`, `executive`. Titles with no level are dropped while this is set. |
| `postedWithinDays` | Keep jobs first published within this many days. |
| `visaSponsorship` | Keep only jobs whose posting says it sponsors visas. |
| `requireSalary` | Keep only jobs that publish pay. |
| `keywordsInDescription` | Also match keywords in descriptions. Broader and noisier. |
| `onlyChanges` | Change mode (see below). |
| `stateName` | Optional name for a separate change history with the same input. |
| `includeDescription` | Plain-text description (on by default). |
| `includeHtml` | Original description HTML (off by default). |
| `maxJobsPerBoard` | At most this many jobs per board, newest first. |
| `searchMaxBoards` | Directory search only: at most this many boards, largest first. |
| `searchTimeLimitSecs` | Directory search only: the time limit in seconds (default 150); see above. |
| `minRows` | Fail the run when it returns fewer rows than this, so a scheduled run that comes back short triggers your alerts. |

### Track new and removed Greenhouse jobs on a schedule

Turn on `onlyChanges` and run the same input on a schedule. The first run returns every job as `change: "new"` and remembers them; each later run returns only:

- `new`: jobs that appeared since the last run,
- `changed`: jobs whose title, team, location, workplace type, salary, description or apply link changed,
- `removed`: jobs that are gone (filled or taken down), as `{"id", "source", "board", "change": "removed", "removed_at"}`.

A board that fails to load is skipped for that run; its jobs are not reported as removed.

Example: watch two Greenhouse boards every morning.

1. Save this input as a task:

   ```json
   {
     "boards": [
       "https://job-boards.greenhouse.io/stripe",
       "https://job-boards.greenhouse.io/airbnb"
     ],
     "onlyChanges": true,
     "minRows": 0
   }
   ```

2. In Apify Console, create a schedule (for example daily at 07:00, cron `0 7 * * *`) and add the task to it.

3. Read each run's dataset, or connect a webhook or integration to send the rows on.

The change history is kept per input (boards plus filters). Use `stateName` to keep two separate histories for the same input.

### Other platforms and ready-made examples

This Actor accepts Greenhouse boards only and says so if you paste another platform's URL. The same code, fields and price cover 13 platforms in [Career Site Jobs API](https://apify.com/stippled_journey/ats-jobs) (`stippled_journey/ats-jobs`): Greenhouse, Lever, Ashby, Workday, Workable, JazzHR, Recruitee, Personio, Teamtailor, Gem, Pinpoint, Join and Paylocity, with a keyword search across all of their directories in one run. Use it when your companies are spread over several platforms; its page also has ready-made example searches (software engineers in the US, remote data engineers, jobs with salary ranges and more).

### FAQ

**Is there an official Greenhouse public job board API, and how is this different?**
Yes. Greenhouse publishes a public Job Board API (`boards-api.greenhouse.io`) that returns
one company's open jobs when you know its board token, and this Actor reads that API. On
top of it you get a search across many Greenhouse companies by title and location, the
pay-range block parsed into a structured `salary`, the seniority, years, visa and skills
fields, rows in the same format as other platforms, and change tracking between runs.

**How do I get only remote Greenhouse jobs?**
Set `workplaceTypes` to `remote`, or add `Remote` to `locations`. Greenhouse has no
workplace field, so `workplace_type` is read from the location text ("Remote (U.S.)"),
and jobs whose location does not say are dropped while `workplaceTypes` is set.

**Why is `employment_type` often empty?**
Greenhouse has no standard employment-type field; it is filled only when the employer adds a "Time Type" or "Employment Type" field to the board.

**Where does the salary come from?**
Many US postings carry a pay-range block that the employer fills in Greenhouse; that becomes `salary` with `source: "pay_range_html"`. Otherwise a pay range written in the description is used (`source: "description_text"`).

**How fresh is the directory?**
It lists the Greenhouse boards found in Common Crawl's public web index and public job lists on GitHub, each checked to have open jobs when it was built (2026-10-28; about 153,000 open jobs then). The jobs themselves are always read live. A company not in it can be read by listing its board.

**How are seniority, years, visa and skills decided?**
By fixed, published word rules, not an AI model, so the same posting always gives the same answer and a field stays empty rather than guessed. `seniority` comes from title words ("Senior", "Staff", "Head of", "VP"); `experience_years_min` is the first number of years the description ties to experience; `visa_sponsorship` is set only by a sentence that ties sponsorship to a visa or work authorization; `skills` match whole words from a fixed list of 486 tools and skills. The rules read English best.

**Can I use it from Python or an AI agent?**
Yes, through the Apify API (with the official Python or JavaScript client), the Apify MCP
server, or any Apify integration such as Make, Zapier or n8n.

**Does it return personal data?**
No. Email addresses and phone numbers are removed from descriptions, and recruiter or hiring-manager names are never returned.

### Data sources and terms

Every job is read from the public job feed or career-site API that Greenhouse publishes for the company's own careers page, the same postings anyone can see there. No login, no browser and no proxy are used, and nothing is read that the board does not show publicly. This Actor is independent and not affiliated with or endorsed by Greenhouse. Job postings belong to the employers; use the data in line with your own obligations, for example when you republish it.

# Actor input Schema

## `keywords` (type: `array`):

Job title words to look for, for example data engineer or account executive (case-insensitive; a job matches any of them). With no boards listed, these search the built-in directory of 3,476 Greenhouse career sites.

## `locations` (type: `array`):

Keep jobs whose location or country contains any of these, for example London, Remote or United States. Leave empty for every location.

## `maxJobs` (type: `integer`):

Stop after this many jobs, newest first. 0 means no limit when you list boards; a directory search then stops at 500. Your run also stops at your spending limit.

## `workplaceTypes` (type: `array`):

Keep only these workplace types. Jobs whose career site does not state one are dropped while this is set.

## `seniority` (type: `array`):

Keep jobs at these levels, read from the job title (for example Senior, Staff, Head of, VP). Titles that do not state a level, such as plain Software Engineer, are dropped while this is set.

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

Keep jobs first published within this many days. Leave empty for every open job.

## `visaSponsorship` (type: `boolean`):

Keep only jobs whose posting says it sponsors visas (for example "We can sponsor visas"). Most postings say nothing either way and are dropped.

## `requireSalary` (type: `boolean`):

Keep only jobs that publish a pay range or amount.

## `keywordsInDescription` (type: `boolean`):

Also match the keywords in job descriptions, not only titles. Broader but noisier.

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

Leave empty to search the built-in directory. Or list Greenhouse career-site URLs (for example https://job-boards.greenhouse.io/stripe) or short forms like greenhouse:stripe. Every open job on each board is returned, then filtered.

## `onlyChanges` (type: `boolean`):

Return only jobs that are new or changed since the last run with the same input, plus a row for each job that was taken down (expired). Schedule the run to monitor boards. Each row gets a change field: new, changed or removed.

## `stateName` (type: `string`):

Optional name for the change history, to keep separate histories for the same input. By default it is derived from the boards and filters.

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

Plain-text job description. Email addresses and phone numbers are removed.

## `includeHtml` (type: `boolean`):

Also return the original description HTML (contact details removed).

## `maxJobsPerBoard` (type: `integer`):

At most this many jobs from each board, newest first. 0 means no limit.

## `searchMaxBoards` (type: `integer`):

Directory search only: search at most this many boards, largest first. 0 searches the whole directory.

## `searchTimeLimitSecs` (type: `integer`):

Directory search only: the search ends after this many seconds and returns the jobs found so far; boards still being read at that point are cut off and counted in the run status. The boards most likely to match are searched first.

## `minRows` (type: `integer`):

Mark the run as failed when it returns fewer rows than this, so a scheduled run that comes back short triggers your Apify alerts. 0 turns the check off.

## Actor input object example

```json
{
  "maxJobs": 20,
  "visaSponsorship": false,
  "requireSalary": false,
  "keywordsInDescription": false,
  "boards": [
    "https://job-boards.greenhouse.io/dropbox",
    "https://job-boards.greenhouse.io/discord"
  ],
  "onlyChanges": false,
  "includeDescription": true,
  "includeHtml": false,
  "maxJobsPerBoard": 0,
  "searchMaxBoards": 0,
  "searchTimeLimitSecs": 150,
  "minRows": 0
}
```

# Actor output Schema

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

The scraped jobs, one normalized row each.

## `enriched` (type: `string`):

The same rows with the enrichment columns: seniority, years of experience, visa sponsorship, skills, salary.

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

Counts for this run: rows pushed, what was read, and anything that failed or hit a cap.

# 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 = {
    "maxJobs": 20,
    "boards": [
        "https://job-boards.greenhouse.io/dropbox",
        "https://job-boards.greenhouse.io/discord"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("stippled_journey/greenhouse-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 = {
    "maxJobs": 20,
    "boards": [
        "https://job-boards.greenhouse.io/dropbox",
        "https://job-boards.greenhouse.io/discord",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("stippled_journey/greenhouse-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 '{
  "maxJobs": 20,
  "boards": [
    "https://job-boards.greenhouse.io/dropbox",
    "https://job-boards.greenhouse.io/discord"
  ]
}' |
apify call stippled_journey/greenhouse-jobs --silent --output-dataset

```

## MCP server setup

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