# Greenhouse Jobs Scraper - No API Key (`renzomacar/greenhouse-jobs-scraper`) Actor

Pull every open role from any Greenhouse job board by company token. Titles, departments, offices, direct apply links. No API key, no login, no proxy.

- **URL**: https://apify.com/renzomacar/greenhouse-jobs-scraper.md
- **Developed by:** [Renzo Madueno](https://apify.com/renzomacar) (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 $3.00 / 1,000 job scrapeds

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/platform/actors/running/actors-in-store#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 — Company Career Pages, No API Key

Pull every open role from any **Greenhouse job board** by company token. No API key, no login, no cookies, no proxy, no browser. You give it `stripe`, it gives you all 572 of Stripe's open postings with titles, departments, offices, full descriptions and a direct apply link for each one.

Greenhouse powers the careers page of a very large share of funded startups and mid-market tech companies. Every one of those boards is served by a public JSON endpoint that Greenhouse documents and keeps open. This Actor reads that endpoint properly, normalises the payload and hands you clean rows.

***

### What you get, with the fill rate actually measured

We ran this Actor across **9 real boards — 2,628 live postings** (stripe, anthropic, databricks, airbnb, coinbase, discord, robinhood, figma, dropbox) and counted how often each field came back populated. These are the real numbers, not a wish list:

| Field | Fill rate | Notes |
|---|---|---|
| `title` | 100% | |
| `companyName` | 100% | As Greenhouse has it, e.g. "Stripe" |
| `jobId` | 100% | Greenhouse's public posting id |
| `internalJobId` | 100% | Greenhouse's internal id |
| `location` | 100% | The single location string on the posting |
| `locations[]` | 100% | Every office attached to the posting |
| `department` / `departments[]` | 100% | Requires `content=true`, which we always send |
| `postedAt` | 100% | `first_published`, ISO 8601 |
| `updatedAt` | 100% | ISO 8601 |
| `jobUrl` | 100% | Canonical posting URL |
| `applyUrl` | 100% | Deep link straight into the application form |
| `boardUrl` | 100% | |
| `descriptionText` | 100% | Full description, HTML stripped and entities decoded |
| `descriptionHtml` | on request | Off by default; roughly triples dataset size |
| `isRemote` | 100% | Always present as `true`/`false` — see the honesty note below |
| `requisitionId` | **78.2%** | Placeholder junk is nulled, see below |
| `workplaceType` | **25.1%** | Only when the location text actually says so |
| `salaryMin` / `salaryMax` / `salaryCurrency` / `salaryText` | **57.0%** | Parsed from the posting text, see below |
| `educationRequirement` | ~98% | Greenhouse's `education` field |
| `applicationDeadline` | rare | Almost always null; most boards do not set it |

#### Three fields this Actor deliberately does **not** return

The Greenhouse board API simply has no such data, on any board we measured. Rather than ship you three columns that are null on 100% of rows, they are absent from the schema:

- **`employmentType`** — Greenhouse's public board payload has no full-time/part-time/contract field.
- **`country`** — there is no ISO country on the posting. Use `location` and `locations[]`.
- **`team`** — Greenhouse exposes a flat department list, not a department/team hierarchy.

If you need employment type or a normalised country, the **Ashby Jobs Scraper** and **Lever Jobs Scraper** in this same fleet both return them at ~99–100% fill, because those platforms actually publish them.

***

### The two fields you should read carefully

#### Salary: 57%, and it is parsed, not published

**Greenhouse's public board API exposes no structured pay field.** We checked `pay_input_ranges` across all 2,628 postings on 9 boards: populated on **zero** of them. Anyone claiming a structured Greenhouse salary field is not reading the same API.

What we do instead is parse the posting text, because a lot of companies now write the range into the description to satisfy pay-transparency laws. That gets a range on **57.0%** of postings. We then sanity-check every hit: of 1,207 extractions in a 2,245-row audit, **1,193 (98.8%) landed in a plausible annual band**, the rest being genuine hourly or part-time roles.

Every salary row carries `salarySource: "description"` so you always know it was parsed rather than published. `salaryText` keeps the original snippet so you can verify it yourself. European formatting is handled: `€92.300 — €130.000` is read as 92300–130000, not 92.3–130.

#### Remote: honest nulls instead of confident guesses

`isRemote` is always present. `remoteSource` tells you where the signal came from:

- `inferred_location` — **27%** of rows. The location text says so: `"US-Remote"`, `"Poland - Remote OR Romania - Remote"`, `"Remote in the US"`, `"SF, NYC, remote"`. These are reliable.
- `unknown` — **73%** of rows, returned as `isRemote: false`.

An earlier build also scanned the description body for the word "remote". We killed that path after measuring it: on 2,245 Stripe/Databricks/Airbnb postings it flagged **95 roles as remote whose location was Singapore, Dublin or Bengaluru**. It was matching company boilerplate ("we are a remote-friendly company"), which describes the employer, not the job. A confident wrong `true` is worse than an honest `false`, so that inference is gone.

Filter on `remoteSource === "inferred_location"` if you only want rows where the board really said it.

#### Requisition IDs: junk removed

`requisition_id` comes back populated on 100% of postings, but **572 of 2,245 (25%) contained the literal placeholder text `"See Opening ID"`**. Those are nulled out. Real fill after cleaning: **78.2%**.

***

### Input

The only required field is the list of companies.

```json
{
  "companies": ["stripe", "anthropic", "databricks"],
  "maxItems": 1000,
  "titleKeywords": ["engineer", "data"],
  "locationKeywords": ["New York", "Remote"],
  "remoteOnly": false,
  "includeDescription": true
}
```

#### Accepted company formats

All of these resolve to the same board:

```
stripe
https://boards.greenhouse.io/stripe
https://job-boards.greenhouse.io/stripe
https://job-boards.greenhouse.io/stripe/jobs/8130725
https://boards-api.greenhouse.io/v1/boards/stripe/jobs
https://anything.com/careers?for=stripe
```

You can also paste a newline- or comma-separated blob into a single string; it is split for you.

#### Input aliases

Different tools name things differently, so the Actor accepts synonyms and you never have to guess:

- companies: `companies`, `company`, `boards`, `boardTokens`, `companyUrls`, `startUrls`
- limit: `maxItems`, `maxResults`, `limit`, `maxJobs`
- keywords: `titleKeywords`, `keywords`, `searchTitle`
- locations: `locationKeywords`, `locations`, `location`
- departments: `departmentKeywords`, `departments`, `department`
- descriptions: `includeDescription`, `includeContent`, `fullDescription`

#### Filters

| Option | What it does |
|---|---|
| `maxItems` | Hard ceiling on rows written, across all boards. **This is your spend cap.** |
| `maxJobsPerCompany` | Stops one 800-posting board from eating the whole budget |
| `titleKeywords` | Keep only titles containing one of these |
| `locationKeywords` | Match against `location` and every entry in `locations[]` |
| `departmentKeywords` | Match against the department list |
| `remoteOnly` | Keep only `isRemote: true` |
| `postedAfter` | ISO date; drops anything published earlier |
| `includeDescription` | Off makes the dataset far smaller |
| `includeHtmlDescription` | Adds raw HTML; off by default |
| `dedupe` | Drops repeat ids, and repeats of company+title+location |
| `concurrency` | Boards fetched in parallel, 1–15, default 5 |

***

### Output sample

```json
{
  "source": "greenhouse",
  "companyToken": "stripe",
  "companyName": "Stripe",
  "jobId": "8130725",
  "internalJobId": 3520748,
  "requisitionId": null,
  "title": "Account Executive, AI Startups (Hunter)",
  "department": "1653 Startups - Account Executives (NA)",
  "departments": ["1653 Startups - Account Executives (NA)"],
  "location": "San Francisco",
  "locations": ["US", "San Francisco"],
  "isRemote": false,
  "workplaceType": null,
  "remoteSource": "unknown",
  "salaryMin": null,
  "salaryMax": null,
  "salarySource": null,
  "postedAt": "2026-08-19T14:02:07-04:00",
  "updatedAt": "2026-08-19T14:02:07-04:00",
  "jobUrl": "https://stripe.com/jobs/search?gh_jid=8130725",
  "applyUrl": "https://job-boards.greenhouse.io/stripe/jobs/8130725#app",
  "boardUrl": "https://job-boards.greenhouse.io/stripe",
  "descriptionText": "Who we are About Stripe ...",
  "scrapedAt": "2026-08-22T04:11:02.884Z"
}
```

Four saved dataset views ship with the Actor: **Job overview**, **Compensation**, **Remote & locations** and **Apply links**, so you can export a clean CSV without picking columns by hand.

***

### How errors are handled

**Errors never enter your dataset.** Charging you for a row that says "this failed" is charging you for a message. Everything that did not work goes into the `FAILURES` record in the run's key-value store:

```json
{
  "runFailed": false,
  "boardsRequested": 3,
  "boardsWithJobs": 2,
  "jobsDelivered": 815,
  "duplicatesDropped": 0,
  "byReason": { "board_not_found": 1 },
  "entries": [
    { "target": "notarealboard", "reason": "board_not_found",
      "detail": "no Greenhouse board with this token (HTTP 404)",
      "at": "2026-08-22T04:11:02.884Z" }
  ]
}
```

The reasons are deliberately distinct, because they mean different things:

- `board_not_found` — HTTP 404. That company is not on Greenhouse, or the token is wrong. This is the normal outcome when you feed a list of companies, and it is not a bug.
- `board_empty` — HTTP 200 with an empty list. The board is real; the company just has nothing open right now.
- `unresolvable_input` — we could not derive a token from what you passed.
- `budget_exhausted` — `maxItems` was reached before this board was written. Nothing is ever skipped silently.
- `fetch_failed` — network or upstream error after retries.

**If nothing was delivered, the run ends FAILED.** A run that quietly succeeds with an empty dataset is how you find out a week later that your pipeline has been dead. You get a clear failure and a reason.

***

### Pricing and the free tier

Pay per event, and only for results:

- **Actor start** — one small charge per gigabyte of memory
- **Job scraped** — charged **after** the row is written to the dataset

Rows are pushed in batches and the per-job event is only charged for rows that actually landed. Nothing is billed for a failure, a filtered-out posting or a deduplicated repeat.

**The free tier returns real data.** There is no proxy requirement, no API key, no credential of any kind. Everything this Actor touches is a public JSON endpoint. A free Apify account running `{"companies": ["stripe"], "maxItems": 10}` gets ten real jobs. Nothing in the code path throws because you lack a paid feature.

Use `maxItems` as your budget control: it is a hard ceiling enforced before rows are written.

***

### Speed and cost efficiency

Greenhouse serves one board per request, so a board of 800 postings costs exactly one HTTP call. In our measured run, **2,628 postings from 9 boards took 5.0 seconds**. Requests are gzip-compressed, retried with exponential backoff on 429/5xx, and never retried on 404.

Note that `content=true` is not optional and we always send it. Without it Greenhouse omits `content`, `departments` **and** `offices` from the response entirely — that is why some scrapers return no department data at all. The cost is a bigger payload (Stripe's board is 360 KB without descriptions, 4.4 MB with) which gzip absorbs.

If you are crawling 50+ large boards in one run, raise memory to 2 GB.

***

### Common questions

**Do I need a Greenhouse API key?** No. The board endpoint is public and unauthenticated. The Harvest API needs a key; this is not that.

**How do I find a company's board token?** It is the path segment in their Greenhouse URL. If their careers page is `job-boards.greenhouse.io/acme`, the token is `acme`. Many companies embed the board on their own domain — view the page source and look for `boards-api.greenhouse.io/v1/boards/<token>` or a `gh_jid` parameter.

**Why does `jobUrl` sometimes point to the company's own site?** Greenhouse returns `absolute_url`, which large companies redirect to their branded careers page. `applyUrl` always points at the Greenhouse application form itself, so use that one for automation.

**Can I get application questions or the form schema?** Not in this Actor. It returns postings.

**Is there an EU board host?** Yes, and `boards.eu.greenhouse.io/<token>` URLs are parsed correctly.

**How fresh is the data?** Live. Every run hits Greenhouse directly; nothing is cached.

***

### Related Actors in this fleet

- **Ashby Jobs Scraper** — structured salary ranges on ~72% of postings, plus a real remote flag
- **Lever Jobs Scraper** — team, commitment, workplace type and country at ~99–100%
- **Workday Jobs Scraper** — the enterprise side, by career-site URL
- **Startup Jobs Aggregator** — give it company handles, it finds each on Greenhouse, Ashby or Lever and returns one deduplicated schema

### Legal

This Actor reads a public, unauthenticated JSON endpoint that Greenhouse publishes so job boards and aggregators can syndicate postings. It sends no credentials, solves no challenges and bypasses no access control. You are responsible for how you use the data, including any applicable data-protection rules.

# Actor input Schema

## `companies` (type: `array`):

One per line. A board token ("stripe") or any Greenhouse URL (https://job-boards.greenhouse.io/stripe). Aliases accepted in raw JSON input: company, boards, boardTokens, companyUrls.

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

Hard ceiling on rows written to the dataset, across all boards. This is your spend cap. Aliases: maxResults, limit, maxJobs.

## `maxJobsPerCompany` (type: `integer`):

Stops one huge board from eating the whole budget.

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

Keep only postings whose title contains one of these (case-insensitive). Leave empty for all. Aliases accepted in raw input: keywords, searchTitle.

## `locationKeywords` (type: `array`):

Keep only postings whose location or office list contains one of these. Example: "New York", "Remote", "London".

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

Keep only postings flagged remote. Check the remoteSource field to see whether the flag came from the ATS or was inferred.

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

ISO date, e.g. 2026-07-01. Drops anything published before it.

## `departmentKeywords` (type: `array`):

Keep only postings in matching departments, e.g. "Engineering", "Sales".

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

Plain-text job description. Turn it off for a much smaller dataset.

## `includeHtmlDescription` (type: `boolean`):

Adds the original HTML alongside the text version. Off by default because it roughly triples dataset size.

## `dedupe` (type: `boolean`):

Drops repeats by job id, and repeats of the same company + title + location published twice.

## `concurrency` (type: `integer`):

How many boards to fetch at once.

## Actor input object example

```json
{
  "companies": [
    "stripe",
    "anthropic",
    "databricks"
  ],
  "maxItems": 1000,
  "maxJobsPerCompany": 100000,
  "titleKeywords": [],
  "locationKeywords": [],
  "remoteOnly": false,
  "departmentKeywords": [],
  "includeDescription": true,
  "includeHtmlDescription": false,
  "dedupe": true,
  "concurrency": 5
}
```

# Actor output Schema

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

One row per open role: title, department, office, location and the direct apply link.

# 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 = {
    "companies": [
        "stripe",
        "anthropic",
        "databricks"
    ],
    "titleKeywords": [],
    "locationKeywords": [],
    "departmentKeywords": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("renzomacar/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 = {
    "companies": [
        "stripe",
        "anthropic",
        "databricks",
    ],
    "titleKeywords": [],
    "locationKeywords": [],
    "departmentKeywords": [],
}

# Run the Actor and wait for it to finish
run = client.actor("renzomacar/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 '{
  "companies": [
    "stripe",
    "anthropic",
    "databricks"
  ],
  "titleKeywords": [],
  "locationKeywords": [],
  "departmentKeywords": []
}' |
apify call renzomacar/greenhouse-jobs-scraper --silent --output-dataset

```

## MCP server setup

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