Greenhouse Jobs Scraper - Job Board API & New-Job Tracking
Pricing
from $1.50 / 1,000 jobs
Greenhouse Jobs Scraper - Job Board API & New-Job Tracking
Every open job on any company's Greenhouse board, read from the official Greenhouse Job Board API: title, department, locations, remote, pay ranges on request, description and dates. Built-in change tracking reports new, updated and removed jobs between runs.
Pricing
from $1.50 / 1,000 jobs
Rating
0.0
(0)
Developer
Cedarfetch Labs
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
an hour ago
Last modified
Categories
Features
Share
Get every open job on any company's Greenhouse board, straight from the official Greenhouse Job Board API (boards-api.greenhouse.io), the same public feed that powers the company's careers page. One clean row per job: title, department, office locations, remote or on-site, requisition ID, description as text and HTML, first-published and last-updated dates, and links. Pay ranges are one switch away for companies that publish them.
Turn on change tracking and schedule it: every row then says whether the job is new, updated (with the fields that changed), unchanged, or removed since the last run. With "Output only changes", a daily run returns just the jobs that opened and closed.
No browser, no proxies, no login. A whole Greenhouse board is one request, so runs take seconds.
What you can use it for
- Recruiters and sourcers: watch the Greenhouse boards of target companies and see new roles the day they open.
- Job boards and aggregators: clean, normalised postings from hundreds of Greenhouse companies, with closed jobs flagged so you can expire them.
- Sales and investors: hiring as a buying or growth signal ("which accounts opened data-engineering roles this week?").
- Labour-market analysts: pay ranges, office mix and department growth across Greenhouse employers.
- AI agents and pipelines: one stable JSON shape you can rely on.
Why this one
- Official API only. Reads the documented Greenhouse Job Board API. Nothing is scraped from HTML, nothing is bypassed.
- Every input form works. Board URLs (
job-boards.greenhouse.io,boards.greenhouse.io, EU boards), single-job links, embed links, links with?gh_jid=on a company's own careers site, or just the board token. - Pay ranges on request. Greenhouse lists jobs without pay;
fetchGreenhousePayreads the range each company publishes for pay-transparency laws. - Change tracking built in. New, updated, unchanged and removed jobs, with the changed fields and their previous values. Removed jobs are only reported after a complete, error-free read of the board.
- Honest failures. Unknown boards, empty boards and typos are reported per board in plain words, never as a silent empty dataset. If nothing at all could be read, the run fails and says why.
- Same schema as ATS Jobs Scraper. Datasets from this Actor, the Lever and Ashby editions, and the multi-system ATS Jobs Scraper combine without any mapping.
How to find a company's Greenhouse board token
Open the company's careers page and click any job.
- If the address is
job-boards.greenhouse.io/stripe/jobs/8172487orboards.greenhouse.io/stripe, the token isstripe. Paste the whole URL; the Actor reads the token from it. - If the job opens on the company's own domain with
?gh_jid=in the address (for examplestripe.com/jobs/search?gh_jid=8172487), paste that link. The token is taken from the domain (stripe), and you get a warning if that job is not on the guessed board. If the guess is wrong, find the real token in the page's "Apply" link or the embed code (boards.greenhouse.io/embed/job_board?for=<token>). - To check a token by hand:
https://boards-api.greenhouse.io/v1/boards/<token>/jobsreturns JSON for a real board and 404 otherwise.
Input
Only companies is required. One entry per line:
| You paste | Read as |
|---|---|
https://job-boards.greenhouse.io/stripe | board stripe |
https://boards.greenhouse.io/stripe/jobs/8172487 | board stripe (a single-job link reads the whole board) |
https://job-boards.eu.greenhouse.io/acme | board acme (EU-hosted board) |
https://boards.greenhouse.io/embed/job_board?for=stripe | board stripe |
https://stripe.com/jobs/search?gh_jid=8172487 | board guessed from the domain (stripe), with a warning if job 8172487 is not on it |
stripe or greenhouse:stripe | board stripe |
Duplicate entries for the same board are merged. A URL for a different system (Lever, Ashby, SmartRecruiters...) is not read; it gets a free status row that names the Actor that reads it.
Example: engineering roles at two companies
{"companies": ["https://job-boards.greenhouse.io/stripe", "anthropic"],"titleKeywords": ["engineer*"],"excludeTitleKeywords": ["intern"],"includeDescription": false}
Example: San Francisco jobs with pay ranges
{"companies": ["anthropic", "figma", "airbnb"],"locations": ["San Francisco"],"postedAfter": "14 days","fetchGreenhousePay": true}
All options
| Field | Default | What it does |
|---|---|---|
companies | (required) | Greenhouse boards in any form above. Up to 2,000 per run. |
maxJobsPerCompany | 0 (all) | Stop after this many matching jobs per board. |
includeDescription | true | Add descriptionText and descriptionHtml (no extra request on Greenhouse). |
titleKeywords | none | Keep titles containing these words. |
titleKeywordsMode | any | any or all of the keywords. |
excludeTitleKeywords | none | Drop titles containing any of these words. |
locations | none | Keep jobs whose locations match any of these words. |
departments | none | Keep jobs whose department matches. |
remoteOnly | false | Keep only isRemote: true. |
postedAfter | none | ISO date (2026-11-01) or relative (7 days, 24 hours, 2 weeks), compared with Greenhouse's first-published date. |
employmentTypes | none | Any of full_time, part_time, contract, temporary, internship, apprenticeship, volunteer, other. Most Greenhouse boards do not state a type (see below). |
trackChanges | false | Compare with the previous run and label every row. |
changesOnly | false | With tracking on, deliver only the selected change types. |
changeTypes | new, updated, removed | Which changes changesOnly delivers. |
snapshotStoreName | greenhouse-jobs-scraper-snapshots | Named key-value store for snapshots. Use different names for independent feeds. |
fetchGreenhousePay | false | One extra request per matching job to read its published pay range. |
concurrency | 2 | Parallel requests per board (matters for pay-range requests). |
requestDelayMs | 250 | Pause after each request to the same board. |
maxConcurrentCompanies | 4 | Boards read at the same time. |
requestTimeoutSecs | 60 | Timeout per request. |
maxRetries | 4 | Retries for 429, 5xx and network errors. |
Keyword matching (titles, locations, departments) is case- and accent-insensitive and works on whole words: engineer matches "Senior Engineer" but not "Engineering"; engineer* matches both; US matches "Remote (US)" but not "Austin"; phrases such as data scientist work. Filters run before maxJobsPerCompany, so the cap counts matching jobs.
Output
One row per job in the default dataset. Download as JSON, CSV, Excel or XML from the Output tab or the API. Views: Jobs, Changes, Job details, Companies that could not be read.
A real row from a local run on 2026-11-11 (anthropic, locations: ["San Francisco"], fetchGreenhousePay: true; description shortened):
{"recordType": "job","ats": "greenhouse","companyToken": "anthropic","companyName": "Anthropic","jobId": "5456619008","requisitionId": "REQ-26-1145-01","title": "3P Revenue Accounting & Reporting","department": "Finance","team": null,"location": "San Francisco, CA | Seattle, WA","locations": ["San Francisco, CA", "Seattle, WA"],"country": null,"isRemote": false,"workplaceType": "onsite","employmentType": null,"employmentTypeRaw": null,"compensation": { "min": 190000, "max": 230000, "currency": "USD", "interval": "year", "text": "Annual Salary: 190000 - 230000 USD" },"descriptionText": "About Anthropic\n\nAnthropic’s mission is to create reliable, interpreta...","descriptionHtml": "<div class=\"content-intro\"><h2><strong>A...","postedAt": "2026-10-19T15:17:24.000Z","updatedAt": "2026-10-19T15:17:24.000Z","jobUrl": "https://job-boards.greenhouse.io/anthropic/jobs/5456619008","applyUrl": "https://job-boards.greenhouse.io/anthropic/jobs/5456619008","changeType": null,"changedFields": null,"previous": null,"firstSeenAt": null,"removedAt": null,"matchedBy": "default_ats","input": "anthropic","source": "api","scrapedAt": "2026-11-12T00:42:25.269Z"}
Dates are ISO 8601 in UTC; numbers are numbers; jobId is always a string. The schema is identical to ATS Jobs Scraper, which is why ats is a field.
What Greenhouse exposes, and what it does not
| Field | On Greenhouse |
|---|---|
companyName | Yes, from the board. |
requisitionId | Yes, as the company typed it (Stripe, for example, puts the placeholder "See Opening ID" on every job). |
department, team | First department; further departments are joined into team. |
location, locations | The job's location text, split on ` |
workplaceType, isRemote | From the company's custom "Location Type" / "Workplace" field when it has one, otherwise from location names such as "Remote" or "Hybrid"; null when nothing says. In a full run of Stripe and Anthropic, 743 of 1,455 jobs had a known remote status. |
employmentType | Greenhouse has no standard field. Filled only when the company adds a custom "Employment Type" field, so it is usually null and the employmentTypes filter drops those jobs. |
compensation | Not in the job list. fetchGreenhousePay reads the pay range per job (+1 request each) when the company publishes one; otherwise null. |
postedAt / updatedAt | First published / last updated. updatedAt also moves on trivial edits, so change tracking never compares it. |
descriptionText / descriptionHtml | Yes, included in the list at no extra cost. |
Boards that could not be read
A free company_status row is added for every entry that could not be read, with a status and a plain-language message:
| status | Meaning |
|---|---|
not_found | Greenhouse has no public board with that token (typo, or the company uses another system). |
no_jobs | The board exists but has no open jobs. |
blocked | Greenhouse refused the request (401/403) or answered with a bot-protection challenge. Never worked around. |
rate_limited | Still rate-limited after polite retries. Try later or lower concurrency. |
unreachable / error | Network failure or an unexpected answer. |
invalid_input | Not a Greenhouse URL or token, or a board on another system (the message names the Actor that reads it). |
partial | (in OUTPUT) Some jobs were read, then the run hit an error or the charge limit. |
If a run finds jobs but delivers none (all filtered out, or nothing changed in changes-only mode), one free company_status row per board explains it.
Run summary
The default key-value store record OUTPUT has, per board: how the input was resolved, status, jobs found / matched / delivered, removed jobs reported, requests, retries, duration, whether the board was read completely, filter counts, change counts and warnings. Use it to monitor scheduled runs.
Change tracking
Turn on Track changes between runs and schedule the Actor. Each run compares every matching job with the snapshot from the previous run of the same board and filters, and adds:
| Field | Meaning |
|---|---|
changeType | new, updated, unchanged or removed. |
changedFields | For updated: e.g. ["title", "compensation"]. Tracked: title, department, team, locations, workplaceType, isRemote, employmentType, compensation, description. |
previous | For updated: the previous values of the changed fields (for description only the change is detected; old text is not stored). |
firstSeenAt | When the job first appeared in this feed. |
removedAt | For removed rows: when the removal was detected. Removed rows carry the last known title, location, department, dates and links. |
Rules you can rely on:
- The first tracked run labels every job
new. It sets the baseline. removedneeds a complete read. If the board could not be read, nothing is labelled removed; the next complete run reports it. A Greenhouse board is one request, so even a run capped bymaxJobsPerCompanysees the whole board and can detect removals.- Nothing is lost on a budget stop. The snapshot only records jobs that were delivered, so a change that did not fit in this run is reported in the next one.
- Filters define the feed. A job that stops matching your filters (for example it ages out of
postedAfter: "7 days") is not "removed"; only jobs that disappear from the board are. Each combination of board and filters has its own snapshot. - Descriptions are compared by hash, even with
includeDescriptionoff (Greenhouse sends them with the list; they are read for the comparison, just not output). - Snapshots live in the named key-value store
snapshotStoreNamein your Apify account (compressed, about 60 bytes per job). Delete it, or pick a new name, to start over.
Tested locally on 2026-11-11 with greenhouse and anthropic (693 jobs): run 1 = 693 new, run 2 = 693 unchanged; after editing the snapshot, a changes-only run delivered exactly the 2 new, 2 updated and 2 removed jobs.
Recipe: daily new-jobs feed
- Create a task with your boards and filters, plus:
{"companies": ["stripe", "anthropic", "figma"],"titleKeywords": ["engineer*", "data"],"trackChanges": true,"changesOnly": true,"changeTypes": ["new"],"includeDescription": false}
- Run it once to set the baseline (every job is
newon that run). - Schedule it daily. From then on each run delivers only the jobs that opened since the previous run, and you pay only for those. Add
"removed"tochangeTypesto also get closed jobs. - Connect the dataset to Slack, email, a Google Sheet or a webhook through Apify integrations.
Pricing
Pay per event: one job event per job row delivered to the dataset (including removed rows), plus Apify's small run-start fee. See the Pricing tab for the current price.
company_statusrows are free.- With changes-only output, unchanged jobs are not delivered and not charged.
- Pay ranges cost no extra event; they only add requests.
- Set Maximum cost per run in the run options to cap spending. The Actor stops cleanly at the cap, keeps everything delivered so far, and notes it in
OUTPUT.
Use from an AI agent or via API
Ready-to-run Python, Node.js and cURL examples: github.com/cedarfetch/ats-jobs-scraper-examples. The examples use the multi-system ATS Jobs Scraper; change the Actor id to this one.
Run synchronously and get the rows back in one HTTP call:
curl -X POST "https://api.apify.com/v2/acts/cedarfetch~greenhouse-jobs-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \-H "Content-Type: application/json" \-d '{"companies":["stripe"],"titleKeywords":["engineer*"],"maxJobsPerCompany":50,"includeDescription":false}'
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: process.env.APIFY_TOKEN });const run = await client.actor('cedarfetch/greenhouse-jobs-scraper').call({ companies: ['anthropic'], trackChanges: true, changesOnly: true });const { items } = await client.dataset(run.defaultDatasetId).listItems();
Python:
from apify_client import ApifyClientclient = ApifyClient("<APIFY_TOKEN>")run = client.actor("cedarfetch/greenhouse-jobs-scraper").call(run_input={"companies": ["stripe"]})items = client.dataset(run["defaultDatasetId"]).list_items().items
The Actor is also available to agents through the Apify MCP server, which exposes Store Actors as tools. Tips for agents: pass board tokens or URLs; keep maxJobsPerCompany small and includeDescription off for exploratory calls; filter on recordType == "job"; read OUTPUT for per-board status.
Limits and honest caveats
- Public postings only. It reads what the company publishes on its Greenhouse board. Internal, confidential and unlisted postings are out of reach by design. Nothing about applicants or employees is collected; names that appear inside a description are not extracted into fields.
- Embedded careers sites. A
?gh_jid=link only tells the Actor that the site uses Greenhouse; the board token is guessed from the domain. Check the warning inOUTPUTand pass the real token if the guess is wrong. - Custom career sites that do not use Greenhouse's public board cannot be read.
- EU-hosted boards (
job-boards.eu.greenhouse.io) are read through the global Job Board API, which serves the boards checked so far. Not every EU board has been verified. - Language and location formats are as each company writes them. Locations are not geocoded.
FAQ
Is this the Greenhouse Harvest API? No. Harvest needs a company's private API key and exposes candidates. This Actor uses only the public Job Board API, which exposes published job postings and nothing about candidates.
Can I read many companies in one run? Yes, up to 2,000 boards. Four are read at a time by default; most boards take one or two seconds.
Why does a company show fewer jobs than its website? Some companies run several Greenhouse boards (for example one per region or brand), or list jobs from other systems too. You get the boards you pass. Check OUTPUT for filters and caps.
Do I need proxies? No. The Job Board API is meant to be read by anyone. Boards that refuse plain requests are reported, not worked around.
What happens to a job that is reposted? It usually gets a new Greenhouse id, so it appears as new and the old one as removed.
Is this legal? The Actor reads job postings that companies publish through a public API intended for that purpose, and collects no personal data about applicants or employees. You are responsible for how you use the data and for the source's terms. If in doubt, ask a lawyer.
Not affiliated with Greenhouse Software. "Greenhouse" is used only to describe which public API this Actor reads.
Support
Found a board that behaves oddly? Open an issue on the Issues tab with the board token and the run ID; the OUTPUT record usually shows what happened.
More Actors from Cedarfetch Labs
Built the same way: public data only, typed output, per-source error reporting, change tracking for scheduled runs.
- ATS Jobs Scraper: the multi-system option. Greenhouse, Lever, Ashby, SmartRecruiters, Workable, Recruitee and Personio in one run, same schema, with auto-detection of a company's system.
- Lever Jobs Scraper: every posting on any Lever job site, global or EU, from the official Lever Postings API.
- Ashby Jobs Scraper: every job on any Ashby board, with salary where published, from Ashby's public Job Posting API.
- Remote Jobs Scraper: remote jobs from Remote OK, We Work Remotely and five more boards in one deduplicated feed.