# Greenhouse Jobs Scraper - Fast ATS API (`zet46/greenhouse-jobs-scraper`) Actor

Jobs from Greenhouse boards (Airbnb, Stripe, Anthropic, Figma, thousands more). Title/dept/location filters + diff mode for daily new-jobs. New `report` mode: 1 row/company with hiringScore, velocity, priorityRoles, remoteShare. $0.50/1k jobs or $0.025/report.

- **URL**: https://apify.com/zet46/greenhouse-jobs-scraper.md
- **Developed by:** [An Tan Huynh](https://apify.com/zet46) (community)
- **Categories:** Jobs, AI, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

## Greenhouse Jobs Scraper – Fast ATS API

Scrape Greenhouse-hosted career pages via the public JSON API. One row per
job, all open positions returned in a single API call, no proxy required.

**$0.50 per 1,000 jobs** (`$0.0005 / job`, pay-per-event).

### Who is this for?

Thousands of companies host their careers on Greenhouse — including Airbnb,
Stripe, Anthropic, Coinbase, Reddit, Figma, Robinhood, Instacart, Discord,
Pinterest, DoorDash, Chime, SoFi and many more. If a board URL looks like
`https://boards.greenhouse.io/<company>` or
`https://job-boards.greenhouse.io/<company>`, this actor can scrape it.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `boardSlugs` | string\[] | **required** | Company slugs or full board URLs. The actor extracts the slug automatically. Pass as many companies as you want in a single run. |
| `includeContent` | bool | `false` | Add `?content=true` to pull full description HTML, departments, offices and AI disclaimer flags. Response grows ~15× (Airbnb: 127 KB → 2.3 MB). |
| `maxJobsPerBoard` | int | `5000` | Safety cap per board (no pagination on Greenhouse). |
| `titleKeywords` | string\[] | *(empty)* | Keep only jobs whose title contains any of these (case-insensitive substring). Example: `["engineer","data scientist","ml"]`. |
| `titleExclusions` | string\[] | *(empty)* | Drop any job whose title contains any of these. Example: `["senior","manager","intern"]`. |
| `departments` | string\[] | *(empty)* | Keep jobs whose department name contains any of these substrings. Auto-enables `includeContent` because departments are only returned with content. Case-insensitive. |
| `locations` | string\[] | *(empty)* | Keep jobs whose `locationName` contains any of these substrings. Case-insensitive. |
| `workplaceTypes` | string\[] | *(empty)* | Match the normalised `workplaceType`: `remote`, `hybrid`, `onsite`. Many boards expose this via "Workplace Type" or "Location Type" metadata (Airbnb, Anthropic). |
| `onlyRemote` | bool | `false` | Shortcut: keep only jobs with `workplaceType=Remote` or `locationName` containing "remote". |
| `updatedAfter` | string | *(empty)* | ISO date / datetime cut-off; keep only jobs with `updatedAt` on or after. |
| `onlyNewSinceLastRun` | bool | `false` | **Diff mode.** The actor remembers (per `stateKey` + board) the set of `jobId`s seen on previous runs in a named key-value store, and on each subsequent run only pushes jobs that are new. Every pushed row carries `isNew: true`. Pair with Apify Scheduler for daily deltas. |
| `stateKey` | string | `default` | Optional label for the diff state bucket. Different `stateKey` values keep independent "seen" memories; shared values share memory. |
| `proxyConfiguration` | object | *(none)* | Optional Apify Proxy – the API is open and usually works direct. |

Example input:

```json
{
  "boardSlugs": ["airbnb", "stripe", "anthropic"],
  "includeContent": false,
  "maxJobsPerBoard": 5000
}
```

### Output (one row per job)

```json
{
  "boardSlug": "airbnb",
  "jobId": 8184174,
  "internalJobId": 3541546,
  "title": "Account Manager",
  "absoluteUrl": "https://careers.airbnb.com/positions/8184174?gh_jid=8184174",
  "locationName": "London, United Kingdom",
  "workplaceType": "Hybrid",
  "metadata": [
    {"name": "Is this job part of ACC?", "value": "no", "valueType": "yes_no"},
    {"name": "Workplace Type", "value": "Hybrid", "valueType": "single_select"}
  ],
  "departments": [],
  "offices": [],
  "requisitionId": "MULTI",
  "firstPublished": "2026-09-09T04:35:19-04:00",
  "updatedAt": "2026-09-29T20:00:54-04:00",
  "applicationDeadline": null,
  "language": "en",
  "companyName": "Airbnb",
  "contentHtml": null,
  "aiDisclaimer": null,
  "aiOptOutUrl": null,
  "scrapedAt": "2026-10-15T20:55:27+00:00"
}
```

When `includeContent=true`, `contentHtml`, `departments`, `offices`,
`aiDisclaimer` and `aiOptOutUrl` are populated.

#### `workplaceType` normalisation

Boards tag workplace under different metadata names. The actor looks for
`Workplace Type`, `Location Type`, `Work Arrangement`, `Work Setup`,
`Remote Type`, `Work Location`, `Remote Work`, and maps the value to
`Remote` / `Hybrid` / `Onsite` whenever recognisable. Boards that don't
tag workplace at all (Stripe, Figma, Discord in our smoke tests) return
`workplaceType: null`.

### Smoke run numbers (2026-10-16)

| Input | Boards | Rows | Cost |
|---|---|---|---|
| Default smoke – 3 boards no content | airbnb, discord, figma | **355** | ~$0.00013 compute + $0.1775 PPE |
| Content + filter – engineering & data, remote/hybrid | airbnb only (`includeContent=true`, `departments=[engineering,data]`, `workplaceTypes=[remote,hybrid]`) | **48** | ~$0.00029 compute + $0.024 PPE |
| Big boards + updatedAfter | anthropic + stripe, `updatedAfter=2026-09-01` | **1115** | ~$0.00076 compute + $0.5575 PPE |

Field coverage over the default smoke (355 rows):

| Field | Coverage |
|---|---|
| `jobId` / `title` / `absoluteUrl` / `locationName` / `companyName` / `firstPublished` / `updatedAt` / `language` / `requisitionId` | 100% |
| `workplaceType` | 46% (Airbnb tags it; Discord and Figma don't) |

Airbnb workplace distribution: 136 Remote / 27 Hybrid / 2 Onsite.
Anthropic workplace distribution: 348 Onsite / 14 Remote / 3 Hybrid / 22 untagged.

### Pricing

Pay-per-event. Two charge types:

- `$0.0005` per `job` row pushed (default `mode: "jobs"`).
- `$0.025` per `report` row pushed (`mode: "report"`, one row per company).

Example runs:

- 3 boards (Airbnb + Discord + Figma, 355 jobs, jobs mode) → **$0.18**.
- 2 big boards (Anthropic + Stripe, 1,115 jobs, jobs mode) → **$0.56**.
- 1 board full content HTML (Airbnb, ~165 jobs, jobs mode) → **$0.08**.
- 3 boards in **report mode** (Airbnb + Stripe + Anthropic) → **$0.075** (3 reports).

Compute is almost free (direct API, no proxy, ~1 second per board).

### Report mode (`mode: "report"`)

Instead of one row per job, emit **one hiring-signal report per company**.
Each report row carries:

- `totalOpenRoles` + `hiringScore` 0-100 + `hiringLabel`
  (`aggressive_hiring` / `active_hiring` / `steady_hiring` / `low_hiring` / `no_signal`).
- `hiringVelocity`: `{ recentWindowDays, netOpenRolesChange, hadPreviousRun, previousTotal }`
  — the delta vs the previous run of the same `stateKey`.
- `priorityRoles[]`: top 5 role families (engineering, data, research,
  product, design, sales, customer_success, support, marketing, operations,
  finance, legal, people_hr, security, other) with example titles.
- `departmentSignals[]`, `regionSignals[]` (us / canada / europe_uk /
  europe_eu / latam / mena / apac / africa / remote_global / remote_other /
  other / unknown), `remoteShare` 0.0-1.0, `seniorityMix` (junior / mid /
  senior / leadership).
- `keywordHits`: user-supplied strings mapped to how many open titles
  contain each (case-insensitive substring).
- `expansionSignals`: `newDepartments[]`, `newRegions[]`,
  `newRoleFamilies[]` compared to the previous run.
- `recommendedOutreach[]`: short English suggestions for BD / sales /
  talent outreach based on the above signals.
- `reportConfidence` 0.1-1.0 plus `warnings[]`.

Example input:

```json
{
  "boardSlugs": ["airbnb", "stripe", "anthropic"],
  "mode": "report",
  "keywordHits": ["AI", "ML", "LLM", "security"],
  "stateKey": "weekly-pipeline"
}
```

Previous-run deltas live in the named key-value store
`greenhouse-report-<stateKey>` on your account.

### Daily deltas (`onlyNewSinceLastRun`)

Pair the actor with Apify Scheduler to get only newly-posted jobs each run.
The first run of a given `(stateKey, boardSlug)` records every open `jobId`
and pushes the first `maxJobsPerBoard` rows (all tagged `isNew: true`).
Every later run compares the current open set against the stored one and
emits only rows whose `jobId` was not seen before.

```json
{
  "boardSlugs": ["airbnb", "stripe", "anthropic"],
  "onlyNewSinceLastRun": true,
  "stateKey": "daily-pipeline",
  "titleKeywords": ["engineer", "data"],
  "includeContent": true
}
```

The dedup state lives in a named key-value store `greenhouse-diff-<stateKey>`
on your account; delete it from the Apify Console to force the next run to
treat every job as new. Use different `stateKey` names when you want
independent memories for different downstream pipelines (e.g. `daily-slack`
vs `weekly-report`).

### Notes

- The Greenhouse public API returns every open job on a board in a single
  response. There is no pagination – use `maxJobsPerBoard` only to trim
  very large boards (Stripe/Anthropic ~700 jobs).
- All timestamps are ISO 8601 with timezone offset exactly as Greenhouse
  returns them (`2026-09-29T20:00:54-04:00`).
- Unknown board slugs return HTTP 404; the actor logs and skips.
- The actor also accepts pasted URLs like
  `https://boards.greenhouse.io/airbnb` or
  `https://job-boards.greenhouse.io/anthropic` – the slug is extracted
  automatically.

# Actor input Schema

## `boardSlugs` (type: `array`):

Required unless you pass boardSlugsUrl. List of Greenhouse board slugs to fetch. Each slug is the last path segment of a Greenhouse board URL, e.g. 'airbnb' from https://boards.greenhouse.io/airbnb or https://job-boards.greenhouse.io/airbnb. Thousands of companies host their careers on Greenhouse: Airbnb, Stripe, Anthropic, Coinbase, Instacart, Reddit, Figma, Robinhood, Discord, SoFi, Pinterest, DoorDash, Chime...

You can also paste a full job board URL (https://boards.greenhouse.io/<slug> or https://job-boards.greenhouse.io/<slug>) — the slug is extracted automatically. Provide at least one entry.

## `boardSlugsUrl` (type: `string`):

Optional. Download a list of board slugs from a public URL and merge with boardSlugs. Supports: direct .csv or .xlsx URL (first column of the first sheet is read), or a Google Sheets URL (viewing link works, e.g. https://docs.google.com/spreadsheets/d/<ID>/edit#gid=<GID> — the sheet must be shared 'Anyone with the link'). Great for feeding a 500+ company watchlist you maintain in a sheet.

## `mode` (type: `string`):

`jobs` (default) emits one row per open job. `report` emits one `hiring_signal_report` row per company with totalOpenRoles, hiringScore 0-100, hiringLabel, hiringVelocity vs the previous run, priorityRoles\[] by role family, departmentSignals\[], regionSignals\[], remoteShare, seniorityMix, keywordHits, expansionSignals and recommendedOutreach\[]. Report mode is priced per report ($0.025 each) and auto-enables includeContent for richer signals.

## `keywordHits` (type: `array`):

Only used when mode=report. For each entry we count how many open job titles contain the string (case-insensitive substring). Example: \['AI', 'LLM', 'GTM'] gives you `keywordHits: {AI: 12, LLM: 3, GTM: 5}` per company. Leave empty if you don't need keyword counts.

## `recentWindowDays` (type: `integer`):

Only used when mode=report. The hiringVelocity.recentWindowDays label we embed on each report row (purely informational — the actual delta is measured between the two latest runs stored in KVS).

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

When enabled, fetches `?content=true` so each job row includes the full description HTML plus departments, offices and AI disclaimer flags. Response size grows ~15x (e.g. Airbnb goes from 127 KB to 2.3 MB).

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

Safety cap on jobs returned per board. Greenhouse returns all open jobs in a single response (no pagination), so this only trims very large boards (Stripe/Anthropic ~700 jobs).

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

Optional. Case-insensitive substring match on the job title. Example: \['engineer', 'data scientist', 'ml'] keeps only jobs whose title contains any of those. Leave empty to keep all titles.

## `titleExclusions` (type: `array`):

Optional. Case-insensitive substring match on the job title. Example: \['senior', 'manager', 'intern'] drops any job whose title contains those words.

## `departments` (type: `array`):

Optional. Case-insensitive substring match on job's department names (requires includeContent=true). Example: \['engineering', 'data'] keeps jobs whose any department name contains 'engineering' or 'data'. Leave empty to keep all departments.

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

Optional. Case-insensitive substring match on job's locationName. Example: \['remote', 'san francisco', 'london'] keeps jobs whose locationName contains any of the strings. Leave empty to keep all locations.

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

Optional. Match the 'Workplace Type' metadata field. Allowed values: 'remote', 'hybrid', 'onsite', 'in-office'. Many Greenhouse boards tag jobs with this field (Airbnb, Stripe, Figma). Empty = no filter.

## `onlyRemote` (type: `boolean`):

Shortcut: keep only jobs with locationName matching 'remote' OR 'Workplace Type' metadata = Remote.

## `seniorityLevels` (type: `array`):

Optional. Keep only jobs whose auto-classified seniority is in this list. Classifier reads the title and tags one of: intern, entry, associate, mid, senior, staff, principal, director, vp, svp, exec. Example: \['senior', 'staff', 'principal'] drops juniors and executives.

## `employmentTypes` (type: `array`):

Optional. Keep only jobs matching an auto-classified employment type. Allowed values: 'full_time', 'part_time', 'contract', 'internship'. Example: \['full_time'] drops interns and contractors. Greenhouse rarely exposes this field in the API, so the filter uses the title + description heuristics.

## `remoteTypes` (type: `array`):

Optional. Keep only jobs tagged as one of: 'remote', 'hybrid', 'on_site'. The classifier first checks the 'Workplace Type' metadata, then falls back to title + location keywords (remote, WFH, hybrid, on-site, in-office).

## `minSalary` (type: `integer`):

Optional. Keep only jobs whose maxSalary (annualised) is ≥ this value. Greenhouse rarely fills a structured salary field, so we parse the description HTML (`pay-range` block + currency range regex) and expose `minSalary`, `maxSalary`, `salaryCurrency`, `salaryPeriod` and `salaryRaw` on each row. Jobs without a parsed salary are dropped by this filter. Enable `includeContent` for best coverage.

## `salaryCurrencies` (type: `array`):

Optional. When combined with minSalary, keep only jobs whose parsed salaryCurrency is in this list (ISO codes, e.g. \['USD', 'EUR', 'GBP']).

## `updatedAfter` (type: `string`):

ISO date (YYYY-MM-DD) or ISO datetime. Keep only jobs whose updated_at is on or after this. Useful for incremental refresh. Alias: 'postedAfter' (same semantics; our other ATS scrapers also accept 'postedAfter').

## `postedAfter` (type: `string`):

Alias of 'updatedAfter' for compatibility with our other ATS scrapers. ISO date (YYYY-MM-DD) or datetime. Ignored if 'updatedAfter' is also set.

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

When enabled, the actor remembers the set of jobIds it saw per board in its default key-value store and on subsequent runs only pushes jobs whose jobId was not present before. Each pushed row carries `isNew: true`. Pair with a scheduler for daily deltas. Dedup state is scoped by (stateKey, boardSlug).

## `stateKey` (type: `string`):

Optional label for the diff state record (defaults to 'default'). Use different names when running the same actor for different downstream pipelines so their 'seen' sets stay separate; use the same name across runs that should share memory.

## `proxyConfiguration` (type: `object`):

Optional. The Greenhouse public API usually serves direct from any IP without blocking, but you can route through Apify Proxy if preferred.

## Actor input object example

```json
{
  "boardSlugs": [
    "airbnb",
    "stripe",
    "anthropic"
  ],
  "mode": "jobs",
  "recentWindowDays": 7,
  "includeContent": false,
  "maxJobsPerBoard": 5000,
  "onlyRemote": false,
  "onlyNewSinceLastRun": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `jobs` (type: `string`):

All scraped Greenhouse jobs as JSON items (one per row).

## `jobsCsv` (type: `string`):

The same jobs as a CSV file.

# 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 = {
    "boardSlugs": [
        "airbnb",
        "stripe",
        "anthropic"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("zet46/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 = {
    "boardSlugs": [
        "airbnb",
        "stripe",
        "anthropic",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("zet46/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 '{
  "boardSlugs": [
    "airbnb",
    "stripe",
    "anthropic"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call zet46/greenhouse-jobs-scraper --silent --output-dataset

```

## MCP server setup

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