Greenhouse Jobs Scraper : $1 per 1,000 Jobs
Pricing
from $1.00 / 1,000 job posting returneds
Greenhouse Jobs Scraper : $1 per 1,000 Jobs
Give it a Greenhouse board token, URL, or company and get every live job on that board as clean, normalized JSON: read in real time from the official Greenhouse Job Board API, never from a stale index.
Pricing
from $1.00 / 1,000 job posting returneds
Rating
0.0
(0)
Developer
Workware
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
21 hours ago
Last modified
Categories
Share
Greenhouse Jobs Scraper: Any Company Board → JSON
Give it a Greenhouse board token, a board URL, or a company: get every live job on that company's Greenhouse board as clean, normalized JSON, in seconds, straight from Greenhouse. Not from an index that was built a few hours ago: this Actor reads the employer's own Greenhouse Job Board API at the moment you ask, so what you get back is what a candidate would see on the careers page right now.
- What you put in: one or more Greenhouse boards. The fastest path is the board token: the
boards.greenhouse.io/{token}slug, but you can also paste a board URL or a company name and let the Actor resolve it. - What you get back: one record per live job posting, in a single canonical schema, plus one status
record per board telling you exactly what happened:
found,no_board, orerror. - Why this one: the indexed alternatives sell you a pre-built corpus with an indexing delay, a six-month window, and a per-run cap. An index cannot answer "what is on this board right now". This can, at roughly $1 per 1,000 jobs.
- Boosted by the Apify platform: full REST API access, scheduling, monitoring, webhooks and integrations, datacenter proxy rotation, and dataset export to JSON, CSV, or Excel.
What is the Greenhouse Jobs Scraper and what can it do?
The Greenhouse Jobs Scraper is a live Greenhouse Job Board API: you name a board and it returns every open role on it as structured JSON. It is the tool you want when you already know whose jobs you care about (a specific company on Greenhouse, the accounts in your CRM, the portfolio companies in your fund) and you want their current openings on demand or on a schedule.
It reads Greenhouse through its official, public, no-authentication JSON API: the most stable class of source that exists for jobs data, and the reason this Actor needs no browser, no residential proxy, and no anti-bot workarounds. It is Greenhouse-focused on purpose: a narrow, testable promise beats a broad, flaky one. If you need Lever, Ashby, or Workable too, or you do not know which system a company uses, the Job Postings API flagship resolves the board for you across all four.
Live vs. indexed: why freshness is the whole point:
| This Actor (live) | Indexed job APIs (fantastic-jobs, jobo.world) | |
|---|---|---|
| Where the data comes from | The employer's Greenhouse board, read when you ask | A crawled copy, refreshed on the vendor's schedule |
| Freshness | Real time: the board as it is right now | An indexing delay (often twice-hourly at best); expired jobs purged only daily |
| Coverage window | The whole current board | Commonly a rolling 6-month window |
| Per-run limits | None, retrieve the entire board | Per-run job caps (e.g. 5,000) are common |
| "What is on board X right now?" | Answered directly | Structurally cannot answer it |
What it deliberately does not do: it does not search across companies by keyword, it does not maintain an index or a history, it does not fetch per-posting application questions, and it does not enrich postings with AI-inferred salaries or skills. Inferred fields are exactly where indexed competitors accumulate quiet errors. This Actor's promise is narrower and honest: the live board, correctly normalized.
What data does it extract? (output fields)
Every record is a canonical JobPosting: a stable id, the source (greenhouse) and its native id, the
posting URL, raw and normalized titles, company, structured location, department, description in HTML and
plain text, and the dates the role was posted and observed.
Two things are worth calling out:
salaryis not a native Greenhouse field, so it is read out of the description when the employer states one, and labelledsource: "parsed". If a role's pay cannot be established honestly, the field is absent: it is never guessed. Descriptions are always fetched so this can run even when you switch descriptions off in the output.- Greenhouse-specific fields that do not fit the canonical schema:
requisition_id,internal_job_id,offices,departments,metadata,first_published,data_compliance, are preserved underextras.greenhouserather than dropped or forced into a field where they do not belong.
Alongside the job records, each input board produces one board-status row:
{ board, company?, jobCount, status }found, no_board, or error. This is what makes an empty result unambiguous:
you always know whether a board has no jobs, does not exist, or simply could not be reached.
| Field | Type | Always present | Description |
|---|---|---|---|
id | string | Always | Stable id: sha1(source + sourceId) (computed by normalizers/actors, not here). |
source | greenhouse | lever | ashby | workable | wellfound | seek | stepstone | dice | ziprecruiter | bayt | xing | reed | Always | Always greenhouse from this Actor; the other values are reserved for sibling Actors. |
sourceId | string | Always | Native id at the source. |
url | string | Always | Canonical public posting URL. |
applyUrl | string | Optional | Direct apply link when distinct from url. |
title | string | Always | Raw title as posted. |
titleNormalized | string | Always | Cleaned title (no req-ids, no ALL CAPS, trimmed seniority tags): normalized in M1b. |
company.name | string | Always | Display name. |
company.website | string | Optional | Company website when known. |
company.sourceSlug | string | Always | Source-native company slug/identifier. |
location.raw | string | Always | Raw location string as provided by the source. |
location.city | string | Optional | |
location.region | string | Optional | |
location.country | string | Optional | ISO-3166 country code when resolvable. |
location.remote | full | hybrid | onsite | unknown | Always | Remote-work posture; unknown when it cannot be determined. |
location.remoteScope | string[] | Optional | For remote roles: the geography a remote candidate must be eligible to work in (visa / right-to-work), e.g. ['US','CA'], ['EU'], ['GLOBAL']. Values are ISO-3166-1 alpha-2 country codes and/or canonical macro-region labels (GLOBAL|EU|EMEA|APAC|LATAM|MENA|ANZ|DACH| AMERICAS|NORAM). Absent when the source states no restriction: never assumed global. |
department | string | Optional | Source-provided team/department. |
employmentType | full_time | part_time | contract | intern | temporary | unknown | Optional | |
salary.raw | string | Optional | Original salary display string when present. |
salary.min | number | Optional | |
salary.max | number | Optional | |
salary.currency | string | Optional | ISO-4217 currency code when known. |
salary.period | year | month | day | hour | Optional | |
salary.source | stated | parsed | Optional | |
descriptionHtml | string | Optional | Sanitized HTML description when available. |
descriptionText | string | Always | Plain-text description. |
postedAt | string | Optional | When the role was published at the source (not scrape time). |
firstSeenAt | string | Always | Our observation window. In live-fetch actors both are simply the run's scrape time: do NOT build cross-run observation tracking here; that is new-job-postings-api's (#18) concern. |
lastSeenAt | string | Always | |
extras | object | Optional | Source-specific fields, namespaced under extras.<source>.*; never breaks the schema. |
How to use the Greenhouse Jobs Scraper (tutorial)
- Click Start with the example input already filled in: one board, no options.
- Watch the run finish (a single board typically completes in a few seconds).
- Open the Dataset tab. You will see the job records plus one
board-statusrow. - Export as JSON, CSV, or Excel, or call the run from your own code through the Apify API.
To run it on your own boards, replace the boards array. Each entry needs one of token, boardUrl, or
company:
token: theboards.greenhouse.io/{token}slug, e.g.airbnb. This is the fastest, zero-guess path.boardUrl: a board URL to parse the token from;boards.greenhouse.io/…,job-boards.greenhouse.io/…, and embed links are all understood.company: a company name or slug; the Actor tries it directly as a token, and, when it looks like a domain, reads that company's careers page for a Greenhouse link (with a similarity guard so it never resolves the wrong board).
Boards in a batch are processed one after another, at a deliberately polite request rate to Greenhouse. If you are polling on a schedule, prefer several small runs over one very large one.
How much does it cost to scrape Greenhouse?
Pay-per-event, with two events:
- $0.002 per board processed: charged once per board that reached a definitive answer, whether that answer is "here are the jobs" or "there is no such Greenhouse board". The resolve-and-fetch work has value either way. This is lower than the flagship's fixed fee because there is no cross-ATS detection.
- $0.001 per job posting returned: charged per validated record ($1 per 1,000 jobs).
Failed lookups are free. A board whose status is error is never charged, and neither are the records
of a board whose fetch failed part-way through: they are discarded rather than delivered. We charge for
answers, not for our own failures.
A worked example: 10 boards averaging 40 open roles each costs 10 × $0.002 + 400 × $0.001 = $0.42. The
comparable indexed APIs charge $1.20–$1.50 per 1,000 jobs; this undercuts them on the marginal price while
answering a question they cannot. Platform usage costs are absorbed, not passed through.
Input and output examples
Input: the simplest useful run:
{ "boards": [{ "company": "airbnb" }] }
Input: a filtered batch by token and URL:
{"boards": [{ "token": "airbnb" },{ "boardUrl": "https://job-boards.greenhouse.io/gitlab" }],"titleFilter": "engineer","remoteOnly": true,"maxJobsPerBoard": 25}
Output: one job record (trimmed) and its board's status row:
{"id": "8f2c…","source": "greenhouse","url": "https://job-boards.greenhouse.io/airbnb/jobs/4001","title": "Senior Backend Engineer","titleNormalized": "Senior Backend Engineer","company": { "name": "Airbnb", "sourceSlug": "airbnb" },"location": { "raw": "San Francisco, CA", "city": "San Francisco", "remote": "unknown" },"department": "Engineering","postedAt": "2026-06-20T10:00:00Z","extras": { "greenhouse": { "requisition_id": "REQ-1234", "offices": [{ "name": "San Francisco" }] } }}
{ "type": "board-status", "board": "airbnb", "company": "airbnb", "jobCount": 42, "status": "found" }
titleFilter, departmentFilter, and locationFilter are plain, case-insensitive substring matches, not
regular expressions. remoteOnly keeps every role that is not clearly on-site, because Greenhouse
locations are free text, a role whose posture cannot be determined is kept rather than silently dropped.
| Event | What it is | Charged for | Price |
|---|---|---|---|
board-processed | Board processed (definitive answer) | each run | $2.00 / 1,000 |
job-result | Job posting returned | each record delivered | $1.00 / 1,000 |
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
boards | array | Yes | - | The Greenhouse boards to scrape: one run handles the whole batch, one board at a time. Each entry needs exactly one of token, boardUrl, or company. token is the fastest, zero-guess path. |
includeDescription | boolean | No | true | Include the full job description in each record. Descriptions are always FETCHED (salary parsing and normalization read them): turning this off strips them from the output to keep datasets small. |
remoteOnly | boolean | No | false | Return only roles whose remote posture is not on-site. Greenhouse locations are free text, so a role whose posture cannot be determined is kept: only a role clearly marked on-site is dropped. |
titleFilter | string | No | - | Keep only roles whose title contains this text, case-insensitively. Plain substring matching: not a regular expression. |
departmentFilter | string | No | - | Keep only roles whose department contains this text, case-insensitively. Plain substring matching: not a regular expression. |
locationFilter | string | No | - | Keep only roles whose location contains this text, case-insensitively. Matched against the raw location string. Plain substring matching: not a regular expression. |
maxJobsPerBoard | integer | No | 0 | Caps the jobs returned per board. 0 means no cap: return the whole board. |
Other Actors you might need
| Actor | Use it when |
|---|---|
| Job Postings API | You need Lever, Ashby, or Workable too, or you don't know which ATS a company uses: the flagship resolves the board across all four. |
| Ashby Jobs Scraper | You only care about Ashby boards. |
If your list spans several applicant tracking systems, start with the flagship: resolving the board is the part it does for you.
FAQ, legality, and support
How do I find a company's Greenhouse board token?
The token is the slug in the board URL. If a company's careers page links to boards.greenhouse.io/acme
or job-boards.greenhouse.io/acme, the token is acme. You can paste that token, paste the whole URL, or
just give the company name and let the Actor resolve it.
How is this different from an indexed jobs API?
An indexed API crawls many career sites on its own schedule and serves you a copy. That is the right shape for "show me every Python job posted this week", and the wrong shape for "what is on this board right now": the copy is only as fresh as the last crawl. This Actor holds no index. It reads the board when you ask, which means no indexing delay and no per-run cap on how much of a board you retrieve.
Can I scrape multiple companies' Greenhouse boards at once?
Yes. Put several entries in the boards array: up to 500 per run, mixing tokens, URLs, and company
names freely. They are processed one after another at a polite request rate, and each gets its own
board-status row.
What if a board does not exist?
You get a board-status row with status: "no_board", and the run still succeeds. "There is no such
Greenhouse board" is a genuine, useful answer, and it is priced as one processed board with no job results.
What happens when Greenhouse changes its API?
Every response is shape-checked before it is normalized. A change that breaks the shape fails the affected board loudly instead of emitting silently wrong records, and a scheduled smoke test runs against known-stable boards so we usually find out before you do.
Is this legal to use?
The data is public, employer-published job advertising, retrieved through Greenhouse's own public Job Board API. Job postings are not personal data, attribution and apply URLs are preserved on every record, and DEI demographic questions are never fetched or stored. As always, how you use the data is your responsibility.