Live Greenhouse Jobs — Change Tracking & Ghost-Job Detection
Pricing
from $0.80 / 1,000 job results
Live Greenhouse Jobs — Change Tracking & Ghost-Job Detection
Pull every open role from a company's live Greenhouse board with a per-run change feed: each job tagged new, changed, open or closed (ghost-job detection) by diffing the prior run. Authoritative current-open truth a resold database can't give. Domain or slug, no keys. Pay per result.
Pricing
from $0.80 / 1,000 job results
Rating
0.0
(0)
Developer
Rez Archer
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
an hour ago
Last modified
Categories
Share
Greenhouse Jobs — Live Feed with New / Changed / Closed Change Tracking
Pull every open role straight from a company's live Greenhouse board and get back a
per-run change-feed: each job is tagged new, changed, open, or closed by diffing
this run against the last one. You get the authoritative current open set and what moved
since you last looked — not a stale snapshot of a third-party database.
Input: ["stripe.com", "greenhouse:gitlab"]Output: one clean row per role, each tagged:new — opened since your last runchanged — still open, but a tracked field moved (title, location, …)open — unchanged since last runclosed — was open last run, gone now → ghost-job signal
Why this instead of a resold jobs database
Most Greenhouse feeds on the market resell a cached database that refreshes on a schedule and retains postings for months. That cannot authoritatively answer the two questions buyers actually have:
- "Is this company's role still open right now?" — This actor reads the company's live Greenhouse board at run time, so an open role is open now, not "seen within the last N hours".
- "What changed since I last checked?" — A cached database has no memory of your last run.
This actor keeps a per-board snapshot between runs and diffs it, so you get
new/changed/closeddeltas for free — including ghost jobs (listings that quietly disappeared), the #1 complaint about job data.
What you get per role
| Field | Notes |
|---|---|
status | new / changed / open / closed — the change-feed verdict for this run |
changed_fields | on a changed row, exactly which tracked fields moved |
first_seen / last_seen | when the role first appeared in your feed and was last confirmed open |
days_open | how long the role has been open (tenure) |
is_ghost | true for a closed row — the role vanished from the board |
ghost_candidate | true when a still-open role has been live long enough to be a likely ghost job — an evergreen listing the employer keeps posted without actively filling (the #1 job-seeker and recruiting-intel complaint). Distinct from is_ghost (vanished): this flags roles that stay up and go stale. Threshold via ghostDays (default 60). |
ghost_signals | why ghost_candidate fired: open_60d (we have observed it continuously open ≥ threshold — needs our maintained cross-run state), posted_60d (board's own posted_at is ≥ threshold old), stale_update_60d (board has not edited it in ≥ threshold — corroboration). Empty when not a candidate. |
age_days | days since the board's posted_at — the first-run age signal that powers ghost_candidate before cross-run history exists |
title, company, location, remote, department, url, posted_at, updated_at | normalized core fields |
location_parsed | the free-text location normalized into structured geography — {city, state, state_code, region, country, country_code, countries, workplace, is_remote, multi_location} — so you can filter on country_code / state_code / workplace without parsing strings yourself |
workplace | remote / hybrid / onsite / null — the work mode, classified across the many phrasings boards use (Remote - US, WFH, Hybrid — Austin, TX) |
salary | normalized pay-transparency range — {min, max, currency, period, annualized:{min,max}, source} — parsed from the employer's own live posting, or null when no range is disclosed |
salary_disclosed | true when a range was parsed for this role (quick filter / coverage metric) |
employment_type | full_time / part_time / contract / temporary / internship, or null when undisclosed — resolved from the board's metadata type field (authoritative), the title, or the description boilerplate; a tracked field, so a role flipping contract→full-time comes back changed |
seniority | intern / junior / mid / senior / staff / principal / lead / manager / director / vp / executive, mapped from the title (most-senior signal wins), or null — filter a feed to just senior+ / exec roles without regexing titles yourself |
employment_type_source / seniority_source | where each was resolved (metadata / title / description) so you can trust-weight them |
requirements | structured hiring requirements parsed from the description: {skills:[{name,category}], skill_names, skills_count, categories, years_experience_min, education_level, source}. Skills come from a maintained, boundary-anchored taxonomy (languages, frameworks, cloud, data, devops, ML, security, tools); years_experience_min is the lowest explicit experience floor in the posting; education_level is the minimum degree required. null when descriptions are off (never guessed from the title). Filter and rank a feed by tech stack / experience without running your own extractor. |
requisition_id, internal_job_id, offices | Greenhouse-specific identifiers for dedup / joins |
description_text or description_html | optional full description (plain text or raw HTML) |
salary is on by default and a first-class tracked field: when an employer moves a role's
posted range, that row comes back tagged changed with salary in changed_fields — a live pay
signal a resold snapshot database cannot give you. Because Greenhouse publishes no structured salary
field, the range is read and normalized from each posting at run time (period-annualized to a
comparable yearly figure), so you get a uniform range even though every employer phrases it
differently. It is returned without the heavy description payload unless you also ask for that.
location_parsed is on by default and needs no network — Greenhouse exposes location only as one
free-text string ("San Francisco, CA", "Remote, Canada; Remote, United States",
"Remote from the US"multi_location: true and lists every country_code in countries, so a
country_code = "US" filter still matches a globally-open posting.
Input
| Field | Type | Default | Description |
|---|---|---|---|
companies | string[] | — | Domain (stripe.com), board slug (stripe), or greenhouse:token. The board slug is auto-detected from a domain — no lookup needed. Optional when useRoster is on. |
useRoster | boolean | false | Pull from the maintained, live-verified roster of Greenhouse boards — a broad current feed without supplying a company list. Combine with companies to run both. |
rosterLimit | integer | 0 (all) | Cap how many roster boards to pull, richest-first (most open roles). Applies only to roster boards. |
rosterFilter | string | — | Keep only roster boards whose company name or token contains this text (case-insensitive). |
includeClosed | boolean | true | Emit closed / ghost rows (roles present last run, gone now). |
includeSalary | boolean | true | Parse + return the normalized salary range for each role. |
includeDescription | boolean | false | Also return the job description. |
descriptionType | text | html | text | Description format when includeDescription is on. |
maxItemsPerCompany | integer | 0 (all) | Cap live rows per company. The full board is always fetched so the diff is complete; the cap only trims output. |
firstRunNewAsOpen | boolean | false | On a board's first run (no prior snapshot) label everything open instead of flooding you with new. |
ghostDays | integer | 60 | Age (days) at which a still-open role is flagged ghost_candidate — a likely evergreen/unfilled listing. Uses the longest of days_open, posted_at age, and a stale updated_at. 0 = default (60). |
The change-feed needs state between runs. On the Apify platform that state is kept in a named key-value store that persists across runs automatically — just run the actor on a schedule against the same companies and each run reports the delta since the previous one.
No company list? Use the maintained roster
Most Greenhouse feeds make you supply the companies. A resold "all of Greenhouse" database skips
that — but at the cost of stale, cached data. This actor gives you the broad feed and the live
truth: turn on useRoster and it pulls from a built-in roster of Greenhouse boards that is
continuously re-verified and pruned — dead boards (companies that left Greenhouse, renamed their
token, or went private) are dropped and live ones kept. That upkeep is the point: when we first
built the roster, more than half of a naive hand-made list of well-known companies was already
pointing at a dead or moved board. Maintaining a live roster of hundreds of board tokens is exactly
the work you don't want to do — so we do it, and you get a current cross-company feed from one call.
node cli.js --roster-limit=25 # 25 richest boards, no company listnode cli.js --roster-filter=fintech # roster boards matching "fintech"node cli.js stripe.com --roster # your own company PLUS the roster
On the Apify platform set useRoster: true (optionally with rosterLimit / rosterFilter). The
roster is refreshed by scripts/refresh_roster.mjs, which live-probes every board, writes the
verified set, and flags a structural break so the feed never silently decays.
Reliability SLA — not a promise, a signal
Greenhouse's public board JSON can silently drift (a renamed field, a moved endpoint). A plain
"it ran" check misses that — the run succeeds and the data is quietly junk. Every run stamps a
freshness/schema verdict (OK / EMPTY / SCHEMA_DRIFT / DOWN) onto its run metadata by
probing a large known-good board and grading reachability, population, and key-field presence
(id / title / url). The same check runs standalone (npm run freshness) so breakage is
caught and patched upstream — usually before it reaches your runs.
Use without an account (CLI)
The exact same verified core runs as a zero-dependency CLI, with the change-feed snapshot kept in a local file:
node cli.js stripe.com --max=25node cli.js greenhouse:gitlab --html --prettynode cli.js stripe gitlab --state=/tmp/feed.json # run twice to see new → open deltas
Pricing
Pay per result — $0.80 per 1,000 rows (see .actor/PRICING.md). Each emitted row — live or
ghost — is one billable result, and the run stops the instant your charge limit is reached.
Guarantees
- Keyless & free upstream — reads only Greenhouse's public board API. No API keys, no proxies.
- Bounded — per-request timeout + response-size ceiling; a stalled or oversized board can never hang a run or exhaust memory.
- Safe — board tokens are validated and redirects are re-checked on every hop (see
SECURITY.md).