# Greenhouse Jobs Scraper - Job Board API to CSV & JSON (`maydit/greenhouse-jobs-scraper`) Actor

Scrape live job postings from any Greenhouse job board. Full descriptions as clean text, department and office breakdowns, and a new-postings-only mode. No API key needed.

- **URL**: https://apify.com/maydit/greenhouse-jobs-scraper.md
- **Developed by:** [Brandt May](https://apify.com/maydit) (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.60 / 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 Jobs Scraper

Scrape live job postings from any **Greenhouse job board** and get them back as flat CSV, JSON or Excel rows. Give it a board token (`figma`) or a board URL (`https://job-boards.greenhouse.io/figma`) and this Greenhouse scraper returns every public opening on that board: job title, department, office, location, a remote flag, the posting URL, and the **full job description converted from Greenhouse's HTML into clean plain text**, plus a short excerpt for quick scanning.

Built for recruiters, sourcers, job-board operators, ATS data pipelines and anyone tracking hiring at a list of companies. It reads Greenhouse's public job-board API, so there is **no API key, no login and no Greenhouse account** involved. Pass one company or a hundred.

This Actor covers **Greenhouse only**. That is deliberate - it is built as a dedicated, well-documented option for Greenhouse boards rather than a thin wrapper over many platforms. A company that is not on Greenhouse cannot be scraped here; see the FAQ for what to use instead.

### What you get

One row per public job posting.

| Field | What it is |
| --- | --- |
| `company` | Company name exactly as Greenhouse publishes it (for example `Dropbox`). Falls back to the board token if the board omits it. |
| `boardToken` | The Greenhouse board the row came from, so multi-company runs stay separable. |
| `jobId` | Greenhouse job ID, as a string. Stable - use it to dedupe across runs. |
| `title` | Job title. |
| `department` | Department(s) the employer filed the role under. Multiple departments are joined with `; `. |
| `office` | Office(s) attached to the role, joined with `; ` (a role open in three offices lists three). |
| `location` | The free-text location line the employer published (for example `Remote - Mexico` or `San Francisco, CA`). |
| `remote` | `true`/`false`, inferred from the location text. Greenhouse has no remote flag of its own - see the limits section. |
| `firstPublishedAt` | ISO 8601 UTC timestamp of when the posting first went live. |
| `updatedAt` | ISO 8601 UTC timestamp of the last edit. Rows are sorted newest-updated first *within each board*; boards are written in the order you listed them. |
| `absoluteUrl` | Public link to the posting (often on the employer's own careers domain). |
| `contentText` | The whole job description as clean plain text: HTML unescaped, tags removed, list items kept as `- ` bullets. Capped at 20,000 characters. |
| `contentExcerpt` | First ~300 characters of `contentText`, cut on a word boundary. |
| `scrapedAt` | ISO 8601 UTC timestamp of the run. |

A `SUMMARY` record is written to the run's key-value store with the boards that succeeded, the boards that failed and why, the total postings seen per run, and **department and office breakdowns** (the 25 most common departments and the 25 most common offices across everything saved, with counts).

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `boards` | string list | `figma`, `dropbox`, `reddit` | Board tokens or board URLs, one per company. Tokens are case-insensitive. Accepts `figma`, `https://boards.greenhouse.io/figma`, `https://job-boards.greenhouse.io/figma/jobs/123456`, and `.../embed/job_board?for=figma`. |
| `includeContent` | boolean | `true` | Keep `contentText` and `contentExcerpt`. Turn it off for a much smaller dataset; `department` and `office` are unaffected. |
| `departmentFilter` | string list | empty | Keep only postings whose department contains one of these strings. Case-insensitive, partial match. |
| `maxResultsPerBoard` | integer | `200` | Row cap per board. Postings are sorted newest-updated first, so a low cap keeps the freshest roles. |
| `onlyNewSinceLastRun` | boolean | `false` | Save only postings this Actor has not seen on that board before. See the FAQ for exactly how the first run behaves. |
| `maxRunSeconds` | integer | `240` | Wall-clock budget. When it is reached the run stops cleanly, keeps everything already saved, and logs which boards it did not reach. |

### Example output

A real row from a default run (description truncated here for readability):

```json
{
  "company": "Dropbox",
  "boardToken": "dropbox",
  "jobId": "8220580",
  "title": "Product Manager",
  "department": "Prod - DocSend (Sub Team)",
  "office": "Canada; Mexico; US",
  "location": "Remote - Mexico",
  "remote": true,
  "firstPublishedAt": "2026-09-23T19:29:17.000Z",
  "updatedAt": "2026-09-23T19:59:04.000Z",
  "absoluteUrl": "https://jobs.dropbox.com/listing/8220580?gh_jid=8220580",
  "contentText": "Role Description\nDocSend is expanding from a virtual data room solution into a deal execution platform. ...",
  "contentExcerpt": "Role Description DocSend is expanding from a virtual data room solution into a deal execution platform. ...",
  "scrapedAt": "2026-09-24T00:52:59.742Z"
}
```

### FAQ

**Where do I find a company's board token?**
Open the company's Greenhouse board in a browser. The token is the path segment: `https://job-boards.greenhouse.io/figma` and `https://boards.greenhouse.io/figma` both give `figma`. You can paste the whole URL into `boards` and the Actor will parse it. A token with no board returns HTTP 404 and is reported for that board only - the rest of the run continues.

**Does it also cover Lever, Ashby, Workday or SmartRecruiters?**
No. This Actor talks to Greenhouse and nothing else, and a company that is not on Greenhouse will 404 here no matter how the token is spelled. Thumbtack is a good example: it is often assumed to be on Greenhouse, but its public board runs on Ashby (checked 2026-09-23), so it cannot be scraped with this Actor - the run log says so explicitly when you pass `thumbtack`. For multi-platform coverage, use our **ATS Job Postings Scraper**, which handles Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee and Workable. For Thumbtack specifically, run that Actor with `companies: ["thumbtack"]` - it detects the Ashby board on its own (verified 2026-09-23: 47 open roles returned).

**What exactly does "only postings new since the last run" do?**
With the option on, the Actor records the job IDs of every posting on each board it successfully fetched, in a named key-value store called `greenhouse-jobs-baseline`. On the **first** run for a board there is no baseline to compare against, so you get **every** posting and the baseline is written. From the second run on you get only IDs that were not there before. A run with nothing new finishes successfully with an empty dataset - that is the expected result, not a failure. The baseline is only written on runs where the option is on. Two things to know: the baseline records every posting the board returned, including any the `maxResultsPerBoard` cap kept out of the dataset, so a cap lower than the board size means the capped-out postings will not turn up as "new" later - keep the cap above the board size when you use this option; and the store is shared by every run of this Actor on your account, so two schedules watching the *same* board will each consume the other's new postings (different board lists are safe, the baseline is kept per board token).

**Is the description really plain text?**
Yes. Greenhouse returns descriptions as HTML-escaped markup, and parts of it are escaped twice (`&amp;nbsp;` in the raw payload), which is why naive scrapers leave `&nbsp;` and `&amp;` scattered through the text. This Actor decodes repeatedly until the string stops changing, then strips the tags and normalizes the whitespace. On a 360-posting sample run, 0 rows contained leftover HTML tags or HTML entities.

**Why is there no salary field?**
Because Greenhouse's public board API does not return a structured salary. Pay ranges show up inside the description when the employer includes them, so they are in `contentText` - searchable, but not parsed into a number. No made-up column.

**How fast is it?**
Greenhouse returns an entire board in a single response with no pagination, so cost per board is one request. Measured locally on 2026-09-23, end to end including writing the rows: a default run (3 boards, 360 postings, full descriptions) took about 2.3 seconds; 10 large boards totalling 3,056 postings with full descriptions took about 15 seconds. Platform runs add the time to write results over Apify Storage, so expect somewhat more there. Large runs are bounded by `maxRunSeconds`, not by the number of requests.

### Data source, access and limits

- **Source:** the public Greenhouse job-board API, `https://boards-api.greenhouse.io/v1/boards/{board_token}/jobs?content=true`. No key, no login, no Greenhouse account, no cookies. Only postings the employer has published publicly on their own board.

- **`content=true` is always requested**, because Greenhouse returns the `departments` and `offices` arrays only on that endpoint (verified: without it, neither is present in the response). `includeContent` controls whether the description text is written to the output, not whether it is fetched.

- **`remote` is inferred, not reported.** Greenhouse has no remote field. This Actor marks a row remote when the employer's location text contains remote/distributed/work-from-home/anywhere/virtual. A role described as remote only in the body text will read `false`.

- **Everything else is passed through as published.** Titles, departments, offices, locations and timestamps come from the employer's own Greenhouse data, including its inconsistencies - department names are whatever each company set up.

- **No personal data is extracted.** Every row describes a *job posting*, not a person: there is no recruiter name, email or phone field, and none is inferred. `contentText` is the employer's own published description reproduced as text, so if an employer wrote a contact address into its posting it stays there - on the 360-row default sample that was one corporate accommodations alias and nothing else. Nothing is collected from candidates or applications.

- **Failures are explicit.** Boards that 404 or error are named in the log and in `SUMMARY`, and the run still saves everything from the boards that worked. If *every* board fails, the run fails with a message naming the likely cause instead of returning an empty dataset that looks like "no jobs".

- **Billing:** this Actor is billed per result on Apify - the current rate is on the Pricing tab of this Actor's page. There is nothing else to buy; the data source itself is free and keyless.

- **Not affiliated with Greenhouse Software, Inc.** Greenhouse is a trademark of its owner; this Actor is an independent tool that reads publicly available job-board data.

# Actor input Schema

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

One entry per company. Paste the board token or the full board URL - both work. The token is the path segment in https://job-boards.greenhouse.io/<token> or https://boards.greenhouse.io/<token> (for example 'figma'). Embed URLs of the form .../embed/job\_board?for=<token> are parsed too. Tokens are case-insensitive. A token with no Greenhouse board returns HTTP 404 and is reported per board rather than failing the run.

## `includeContent` (type: `boolean`):

Keep the full job description as clean plain text (contentText) plus a short excerpt (contentExcerpt). Greenhouse returns descriptions as escaped HTML; this Actor unescapes and strips it. Turn this off for a much smaller dataset - the department and office fields are unaffected.

## `departmentFilter` (type: `array`):

Optional. Keep only postings whose Greenhouse department contains one of these strings, for example 'Engineering' or 'Design'. Matching is case-insensitive and partial. Leave empty for every department. Department names are set by each employer, so run once without a filter to see the names a board actually uses.

## `maxResultsPerBoard` (type: `integer`):

Upper bound on rows saved for each board. Postings are sorted newest-updated first, so a low cap keeps the freshest roles. Greenhouse returns a whole board in one request, so raising this costs almost nothing in time - a 900-posting board is a single response.

## `onlyNewSinceLastRun` (type: `boolean`):

Save only postings this Actor has not seen before. Job IDs from each successful board are recorded in a named key-value store ('greenhouse-jobs-baseline') and compared on the next run with this option enabled. The FIRST run for a board has no baseline, so it returns every posting and records the baseline; from the second run on you get only new ones. A run with nothing new finishes successfully with an empty dataset.

## `maxRunSeconds` (type: `integer`):

Wall-clock budget for the crawl. When it is reached the Actor stops cleanly, keeps everything already saved, and logs which boards it did not reach - instead of being killed with a failed run. Raise it when you pass a long list of boards.

## Actor input object example

```json
{
  "boards": [
    "figma",
    "dropbox",
    "reddit"
  ],
  "includeContent": true,
  "maxResultsPerBoard": 200,
  "onlyNewSinceLastRun": false,
  "maxRunSeconds": 240
}
```

# Actor output Schema

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

One row per job posting from each Greenhouse board, with department, office and clean-text description.

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

Totals for the run, including how many records were requested versus returned and whether the run stopped early on its time budget.

# 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 = {
    "boards": [
        "figma",
        "dropbox",
        "reddit"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("maydit/greenhouse-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 = { "boards": [
        "figma",
        "dropbox",
        "reddit",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("maydit/greenhouse-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 '{
  "boards": [
    "figma",
    "dropbox",
    "reddit"
  ]
}' |
apify call maydit/greenhouse-jobs-scraper --silent --output-dataset

```

## MCP server setup

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