# Greenhouse Jobs Scraper API & Hiring Monitor (`automa-flow/greenhouse-jobs-hiring-monitor`) Actor

Greenhouse jobs scraper API for complete live company boards. Export jobs with salary, remote, seniority and country fields, or monitor verified new, updated, closed and reopened vacancies. Uses the official public Job Board API; no login, browser or proxy.

- **URL**: https://apify.com/automa-flow/greenhouse-jobs-hiring-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Jobs, Lead generation, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.50 / 1,000 job 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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Greenhouse Jobs Scraper API & Hiring Monitor

Scrape every live vacancy from any public Greenhouse board through the official
Job Board API. Export jobs with flat salary, remote, seniority and country
fields, or monitor a company watchlist for verified `NEW`, `UPDATED`, `CLOSED`
and `REOPENED` vacancies. No login, browser, CAPTCHA or proxy is required.

Every run reads Greenhouse's official Job Board API **at the moment you run it**.
There is no cached jobs database behind this Actor: if an employer removes a
role this morning, it will not remain in your next snapshot. No login, browser,
CAPTCHA or proxy is required.

### Choose your workflow

| Goal | Settings | What you get |
| --- | --- | --- |
| Export a live job board to CSV, JSON or Excel | `snapshot` + `all` | One clean row per current vacancy, plus a company summary |
| Find targeted roles across companies | `snapshot` + `filters` | Only matching jobs, with salary, seniority, remote type and apply links |
| Track competitor or portfolio hiring | `monitor` + `changesOnly` | Only jobs that moved, plus company, department and location deltas |
| Feed a CRM, dashboard or AI agent | Any mode + schedule/webhook | Stable IDs, explicit statuses and machine-ready change records |

### Why teams choose this Actor

**One request per company, whatever its size.** Greenhouse returns a complete
board in a single response, descriptions included. In the September 2026 release
benchmark, an 853-vacancy board required one HTTP request. That is why
descriptions cost no extra requests here and large boards stay fast.

**Predictable result-linked pricing.** Snapshot exports charge for delivered job
rows. Monitoring charges for successfully verified companies plus only the
changed job rows when you use `changesOnly`. Failed, partial and missing boards
do not become billable results.

**It never hands you a truncated board.** Greenhouse reports how many jobs it is
returning. If that count does not match the jobs actually delivered, the run
reports `FAILED` for that company instead of writing a short list that looks
complete. A silently truncated export is the worst failure mode for anything you
run on a schedule, and it is the one this Actor is built to make impossible.

**Flat columns, not nested JSON.** `salary_min`, `salary_max`, `salary_currency`,
`salary_period`, `remote_type`, `seniority`, `location_cities`,
`location_countries` and `days_since_published` are derived on every row, so the
output drops straight into a spreadsheet, a CRM or an agent prompt.

**A real change monitor, not a diff you compute yourself.** Switch `mode` to
`monitor` and later runs return only `NEW`, `UPDATED`, `CLOSED` and `REOPENED`
jobs, with the exact fields that changed and a per-company hiring delta.

### Quick start: inspect one company

Paste board tokens, one per line. The token is the last part of a Greenhouse
board URL: `job-boards.greenhouse.io/stripe` means the token is `stripe`. Full
URLs work too.

If you do not know the token, open any vacancy on the employer's careers page
and copy its `boards.greenhouse.io/...` or `job-boards.greenhouse.io/...` URL.
You may paste the board URL, an individual job URL or just the first path segment.
The Actor normalizes all three forms, so users do not need to guess or maintain
a separate company-name resolver.

```json
{
  "boards": ["figma"],
  "mode": "snapshot",
  "outputMode": "all",
  "includePayTransparency": true
}
```

That returns every current Figma vacancy plus one company summary. In the
September 2026 release benchmark the same input returned 161 job rows. Once the
output fits your workflow, add up to 100 companies in the same run.

The Actor also includes ready-made Task configurations for a full CSV/JSON
export, competitor monitoring, remote senior engineering jobs with salary, and
cross-company hiring comparison. Replace the sample board tokens with your own
watchlist and save the Task before scheduling it.

### Filtering

Ask for the roles you actually want instead of exporting everything and
filtering afterwards. All filters combine with AND.

```json
{
  "boards": ["stripe", "anthropic", "databricks", "figma"],
  "filters": {
    "titleIncludes": ["engineer", "developer"],
    "titleExcludes": ["intern", "manager"],
    "seniorities": ["senior", "staff", "principal"],
    "remoteTypes": ["remote"],
    "countries": ["US", "GB"],
    "withSalaryOnly": true,
    "publishedAfter": "2026-08-01"
  }
}
```

| Filter | Matches |
| --- | --- |
| `titleIncludes` / `titleExcludes` | Case-insensitive substrings of the job title |
| `locationIncludes` | The raw location string or any parsed city |
| `departments` | Department names exactly as the board publishes them |
| `countries` | ISO 3166-1 alpha-2 codes parsed out of the location |
| `remoteTypes` | `remote`, `hybrid`, `onsite` |
| `seniorities` | `intern`, `entry`, `junior`, `senior`, `staff`, `principal`, `director`, `vp`, `executive` |
| `withSalaryOnly` | Only jobs that published a pay range |
| `publishedAfter` | ISO date or timestamp |

Filters decide which rows are **written**, never what is **observed**. The
monitoring snapshot and the company summary always describe the whole board, so
narrowing a filter can never make a job look `CLOSED` and widening one can never
make an old job look `NEW`. `matched_jobs` on the company summary tells you how
many rows a run wrote; `active_jobs` always tells you the size of the board.

A derived value that is unknown is excluded rather than assumed. A job whose
wording gives no remote signal has `remote_type: null` and will not appear under
`remoteTypes: ["onsite"]`.

### Output

Each job row carries the source fields plus the derived columns:

| Field | Example | Notes |
| --- | --- | --- |
| `title`, `company_name`, `job_id` | `Senior Software Engineer` | |
| `apply_url` | `https://job-boards.greenhouse.io/stripe/jobs/1234567` | The employer's own apply link |
| `location` | `San Francisco, CA \| New York City, NY` | Raw, exactly as published |
| `locations` | `[{city: "San Francisco", region: "CA", country: "US"}, …]` | Every office, parsed |
| `location_cities`, `location_countries` | `["San Francisco"]`, `["US"]` | Flat, for filtering and grouping |
| `remote_type` | `remote` | `null` when the posting gives no signal |
| `seniority` | `senior` | From level words in the title; `null` when absent |
| `employment_type` | `full_time` | Usually `null`: Greenhouse titles rarely state it |
| `salary_min`, `salary_max` | `222800.0`, `290000.0` | Currency units, not cents |
| `salary_currency`, `salary_period` | `USD`, `year` | Period read from the range title |
| `first_published`, `days_since_published` | `2026-08-01T…`, `29` | |
| `departments`, `offices` | `[{id, name}]` | As published |
| `description_html`, `description_text` | | Set `includeDescription: true` |
| `change_type`, `changed_fields` | `UPDATED`, `["title", "pay_ranges"]` | Monitor mode |

Boards write locations in several conventions at once. `San Francisco, CA | New
York City, NY | Seattle, WA`, `Atlanta, Georgia`, `Remote - California` and
`San Francisco, CA • New York, NY • United States` all parse into structured
offices with an ISO country code. In the September 2026 release benchmark across
five live boards and 1,963 vacancies, a country resolved for 99% of jobs that
stated a location.

`seniority`, `remote_type` and `employment_type` are derived from wording with
fixed, published rules rather than a model, so the same input always gives the
same answer. "Manager" and "Lead" are deliberately not treated as seniority
levels: on real boards "Customer Success Manager" and "APAC Tax Lead" are job
functions, and reading them as levels both mislabels individual contributors and
hides the level in "Senior Customer Success Manager".

Every record also carries `source`, a stable `source_id`
(`greenhouse:{board_token}:{job_id}`), `source_url`, UTC `scraped_at`,
`schema_version` and a SHA-256 `fingerprint` over the semantic fields. Derived
columns are excluded from the fingerprint, so refining a heuristic here can never
announce thousands of fake `UPDATED` jobs.

#### Statuses

| Status | Meaning | Monitoring state advanced? |
| --- | --- | --- |
| `SUCCESS` | Complete verified board response | Yes, in monitor mode |
| `NOT_FOUND` | Greenhouse returned HTTP 404 for that board | No |
| `PARTIAL` | A suspicious drop could not be consistently verified | No |
| `FAILED` | Transport error, exhausted 429/5xx retries, malformed JSON, or a job count that did not match | No |

A genuinely empty board is `SUCCESS` with `active_jobs: 0`. It is never confused
with `NOT_FOUND` or `FAILED`.

### Monitoring changes

```json
{
  "boards": ["stripe", "airbnb"],
  "boardAliases": { "stripe": "account-1041", "airbnb": "account-2277" },
  "mode": "monitor",
  "outputMode": "changesOnly"
}
```

The first run records the full board as a baseline. Every later run returns only
what moved:

- `NEW` - a job that was not on the board last time
- `UPDATED` - with `changed_fields` naming exactly what changed
- `CLOSED` - gone from the board, rebuilt from stored state
- `REOPENED` - a previously closed job that came back

Plus one `company_summary` per company with `active_jobs_delta`, `new_jobs`,
`closed_jobs` and per-department and per-location deltas, ready to drop into a
CRM trigger, a dashboard or an alert.

`boardAliases` maps a company to your own identifier. It is echoed into every
result as `external_id` and is never sent to Greenhouse.

A failed, partial or rate-limited check never manufactures `CLOSED` jobs and
never replaces the last known-good state, so a bad network minute cannot tell you
that a company stopped hiring.

### Pricing

The live **Pricing** tab is authoritative for the run you are about to start.
Significant pricing changes have a notice period, so the rate currently charged
can temporarily differ from the next configured model shown below. During that
transition you pay the rate displayed on the live Pricing tab.

| Event | Price | Charged when |
| --- | --- | --- |
| `job-result` | $0.0008 Free/Bronze; $0.00065 Silver; $0.0005 Gold+ | Each job row written to the dataset |
| `board-check` | $0.02 | A company returned a complete `SUCCESS` response in monitor mode only |

Retries, duplicate aliases, `NOT_FOUND`, `PARTIAL` and `FAILED` checks are never
charged. `outputMode: all`, the default, writes and charges every current job.
Setting `outputMode: changesOnly`, which is the point of a monitoring schedule,
writes and charges only the jobs that moved: `UNCHANGED` jobs are neither written
nor charged. Company summaries and board-status rows are always free.

What that means in practice:

| Run | Cost |
| --- | --- |
| Full export, the 853-job release benchmark | $0.68 |
| Full export, 10 boards averaging 200 jobs | $1.60 |
| Monitoring 100 companies, a quiet day | $2.00 |
| Monitoring 100 companies, 40 jobs moved | $2.03 |
| Daily monitoring of 100 companies, 30 days | $60 plus baseline and changed job rows |

Snapshot runs have no fixed per-company charge, so a small board is not made
artificially expensive. The second monitoring run is where this Actor becomes
especially cheap: a watchlist that reports "nothing changed" costs two cents per
company, because you are billed for the verified check, not for re-exporting a
board you already have.

Apify's synthetic `apify-actor-start` event adds $0.00005 per run at the default
512 MB. Platform usage is included in these event prices rather than billed
separately. You can cap a run with `maxTotalChargeUsd`: once the remaining budget
cannot pay for the next row, the Actor stops writing, reports what it skipped in
`RUN_SUMMARY`, and does **not** advance the monitoring snapshot, so nothing that
was dropped is missing from the next run's changes.

### Input reference

| Field | Default | Purpose |
| --- | --- | --- |
| `boards` | Required | 1 to 100 Greenhouse board tokens or board URLs |
| `mode` | `snapshot` | `snapshot` returns the board and touches no state; `monitor` compares with the last successful run |
| `outputMode` | `all` | `all` returns every job; `changesOnly` omits `UNCHANGED` jobs |
| `filters` | none | Narrows which job rows are written. See above |
| `boardAliases` | none | Map of board token to your own ID, echoed as `external_id` |
| `includeDescription` | `false` | Adds description HTML and text, and includes them in change detection. No extra requests |
| `includePayTransparency` | `true` | Adds published pay ranges and the flat salary columns. No extra requests |

### Scheduling

Save your watchlist as an Apify Task and pick a daily schedule in Console, or
create one through the API. Keep the input JSON inside `runInput.body` as a
string.

```bash
curl -X POST "https://api.apify.com/v2/schedules" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "name": "daily-greenhouse-watchlist",
  "isEnabled": true,
  "cronExpression": "0 7 * * *",
  "timezone": "UTC",
  "actions": [{
    "type": "RUN_ACTOR",
    "actorId": "boyPuAVGr92b4EujA",
    "runInput": {
      "contentType": "application/json; charset=utf-8",
      "body": "{\"boards\":[\"stripe\",\"airbnb\"],\"mode\":\"monitor\",\"outputMode\":\"changesOnly\"}"
    },
    "runOptions": { "build": "latest", "memoryMbytes": 512 }
  }]
}
JSON
```

### Webhooks

Push successful-run metadata, including the Dataset ID, to your own endpoint.
The receiver fetches the change rows from that dataset.

```bash
curl -X POST "https://api.apify.com/v2/webhooks" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d @- <<'JSON'
{
  "eventTypes": ["ACTOR.RUN.SUCCEEDED"],
  "condition": { "actorId": "boyPuAVGr92b4EujA" },
  "requestUrl": "https://example.com/hooks/hiring-signals",
  "shouldInterpolateStrings": true,
  "payloadTemplate": "{\"runId\":\"{{resource.id}}\",\"datasetId\":\"{{resource.defaultDatasetId}}\",\"itemsUrl\":\"https://api.apify.com/v2/datasets/{{resource.defaultDatasetId}}/items?clean=true\"}"
}
JSON
```

### Run it from your own code

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/automa-flow~greenhouse-jobs-hiring-monitor/run-sync-get-dataset-items" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"boards":["stripe","anthropic"],"filters":{"seniorities":["senior","staff"],"withSalaryOnly":true}}'
```

#### Use it from an AI agent through MCP

Apify's hosted MCP server can expose this public Actor as a tool with its input
and output schemas. Add this server URL to an MCP-compatible client and sign in
through Apify when prompted:

```text
https://mcp.apify.com?tools=automa-flow/greenhouse-jobs-hiring-monitor
```

A concrete prompt is more reliable than asking an agent to invent the input:

```text
Run automa-flow/greenhouse-jobs-hiring-monitor for the Stripe, Anthropic and
Figma Greenhouse boards. Return remote senior or staff roles with published
salary ranges, then group the results by company and country.
```

For recurring use, tell the agent to keep `mode: monitor` and
`outputMode: changesOnly`, and to inspect company summaries separately from job
rows. The explicit status and `record_type` fields let it distinguish a valid
empty result from a failed source check.

### Reliability and source health

The client uses bounded exponential backoff with jitter for 408, 429, selected
5xx responses, timeouts and connection resets, plus a run-wide retry budget.
Concurrency is fixed at three and is not exposed as a Store knob.

One company failing never affects the others: 100 companies in means 100
answers out, each with its own status.

When a board falls sharply from its last successful job count, the Actor makes
an independent verification request. It accepts closures only if both complete
responses contain the same job IDs. Otherwise it emits `PARTIAL`, keeps the
known-good state and produces no closure events.

### Known limitations

- Only public Greenhouse Job Board API data is supported. Private or internal
  boards and application submission are out of scope.
- `pay_transparency=true` is documented by Greenhouse for individual jobs and
  was live-verified on the list endpoint. If Greenhouse removes list-level
  support, pay ranges may be empty until the Actor is reassessed; it will not
  silently add one request per job.
- `includeDescription=false` minimizes output and stored state, but the source
  call still uses `content=true` because the departments and offices needed for
  hiring deltas are embedded with content.
- `employment_type` is usually `null`. Greenhouse job titles rarely state it and
  the Actor does not guess.
- Compact tombstones are retained in the named monitoring key-value store to
  detect `REOPENED` jobs. Delete that store to reset all baselines.
- A board token that Greenhouse no longer serves is `NOT_FOUND`; the Actor does
  not search the web to guess a replacement.
- Apify-container reachability is a release smoke gate. Direct local requests
  require no proxy; a future source change that requires CAPTCHA, browser or
  residential stealth triggers reassessment rather than an automatic bypass.

### Source attribution and responsible use

Data comes from Greenhouse's official public
[Job Board API](https://docs.greenhouse.io/job-board.html) and remains subject
to Greenhouse's and each hiring company's terms. This Actor collects public
job and company data only. It does not request application questions, applicant
records, demographic form answers or candidate information.

Run output is stored in the requesting user's Apify Dataset. In monitor mode,
only compact last-successful job state and tombstones are retained in the
requesting user's named key-value store. The Actor is not affiliated with
Greenhouse Software.

# Actor input Schema

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

Greenhouse board tokens or board URLs, one per line. The token is the last part of the board URL: job-boards.greenhouse.io/stripe means the token is stripe. Up to 100 companies per run.

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

Get all jobs returns every live vacancy and touches no monitoring state. Monitor changes compares each board with its last complete successful run and reports what is new, changed, closed or reopened.

## `outputMode` (type: `string`):

All jobs returns every current vacancy. Changes only omits UNCHANGED jobs, which is what makes a scheduled monitoring run small and cheap. Company summaries and closures are always returned.

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

Return normalized description HTML and plain text and include them in change detection. Costs no extra requests: Greenhouse returns descriptions in the same board response.

## `includePayTransparency` (type: `boolean`):

Request and normalize published pay ranges, and fill salaryMin, salaryMax, salaryCurrency and salaryPeriod. Uses the same board request, not one request per job.

## `filters` (type: `object`):

Narrow which job rows are written. Filters never affect monitoring: the snapshot and the company summary always describe the whole board, so filtering can never invent a NEW or a CLOSED job.

## `boardAliases` (type: `object`):

Optional map of board token to your own identifier, for example {"stripe": "account-1041"}. Echoed into every result as externalId so results join back to your CRM or watchlist. Never sent to Greenhouse.

## Actor input object example

```json
{
  "boards": [
    "stripe",
    "https://job-boards.greenhouse.io/airbnb"
  ],
  "mode": "snapshot",
  "outputMode": "all",
  "includeDescription": false,
  "includePayTransparency": true,
  "filters": {
    "titleIncludes": [
      "engineer",
      "developer"
    ],
    "titleExcludes": [
      "intern"
    ],
    "countries": [
      "US",
      "GB"
    ],
    "remoteTypes": [
      "remote"
    ],
    "seniorities": [
      "senior",
      "staff"
    ],
    "withSalaryOnly": true
  },
  "boardAliases": {
    "stripe": "account-1041",
    "airbnb": "account-2277"
  }
}
```

# Actor output Schema

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

No description

## `runSummary` (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 = {
    "boards": [
        "stripe",
        "anthropic",
        "databricks",
        "figma"
    ],
    "filters": {},
    "boardAliases": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/greenhouse-jobs-hiring-monitor").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": [
        "stripe",
        "anthropic",
        "databricks",
        "figma",
    ],
    "filters": {},
    "boardAliases": {},
}

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/greenhouse-jobs-hiring-monitor").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": [
    "stripe",
    "anthropic",
    "databricks",
    "figma"
  ],
  "filters": {},
  "boardAliases": {}
}' |
apify call automa-flow/greenhouse-jobs-hiring-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/greenhouse-jobs-hiring-monitor"
        }
    }
}

```

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/boyPuAVGr92b4EujA/builds/WYaSSB94JPP3ooS3U/openapi.json
