Greenhouse Job Board Scraper - Company Jobs, Salary, Remote avatar

Greenhouse Job Board Scraper - Company Jobs, Salary, Remote

Pricing

from $1.05 / 1,000 job posting returneds

Go to Apify Store
Greenhouse Job Board Scraper - Company Jobs, Salary, Remote

Greenhouse Job Board Scraper - Company Jobs, Salary, Remote

Open positions from any company's Greenhouse board, read from the public Job Board API - no key, no browser. One row per posting: title, location, department path, office, published salary range, first published and updated time in UTC, apply link and optional description text.

Pricing

from $1.05 / 1,000 job posting returneds

Rating

0.0

(0)

Developer

Samat Makatov

Samat Makatov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

Give this actor the board token of a company that hires through Greenhouse and get its open positions as flat rows: job title, the location text the employer typed, the department path from the company's own tree, the office, the published pay range as two numbers and a currency, when the posting first appeared and when it last changed (both in UTC), the apply link and — on request — the full description. The data comes from the public Greenhouse Job Board API, the same one a company's own career page reads. No API key, no proxy, no browser, no login.

Coverage is per company, not a search across all employers: you name the companies, the actor reads their boards. That is the point — the rows are exactly what the employer publishes, with no aggregator in between.

Use cases

  • Sourcing for one employer (recruiters, agencies): every open engineering role at one company, newest change first, with the apply link — a shortlist you can refresh every morning.
  • Hiring signals for a B2B pipeline (GTM, sales, lead gen): watch a list of accounts with postedWithinDays: 30; a company that starts hiring data engineers is a company that just bought something.
  • Pay benchmarking (compensation, people analytics): withSalaryOnly: true gives only the postings with a published band, already converted from cents into whole currency units, with the label the employer gave the band.
  • A niche job board or newsletter: a shortlist of employers plus titleKeywords and remoteOnly, refreshed on a schedule with onlyNew: true, so each run writes only what you have not published yet.
  • Feeding an agent or a search index: includeDescription: true adds the full posting as clean text and as the published HTML, ready for embeddings, summarisation or keyword search.
  • Org research (analysts, competitor watching): departments filters on the company's real department tree, so "everything under Sales" is a filter, not a guess from job titles.

Input

Only boardTokens is required. Every filter is optional; filters are combined with AND, and the values inside one filter with OR.

FieldTypeDefaultAllowed values / notes
boardTokensstring[]requiredOne or more board tokens, e.g. ["stripe", "duolingo"]. A full board URL works too — see Finding the board token. An unknown token comes back as one row with found: false, it does not fail the run.
titleKeywordsstring[]all titlesKeep a posting when its title contains at least one of these, case-insensitive: ["engineer", "data"].
excludeTitleKeywordsstring[]noneDrop a posting when its title contains any of these: ["intern", "contract"]. Applied after titleKeywords.
locationContainsstring[]all locationsMatch the free-text location: ["London", "India"]. See Locations are free text.
departmentsstring[]all departmentsMatch at any level of the company's department tree: ["Sales"] also returns "Sales > Enterprise Sales". Typos are corrected against the board's own names — see Departments and offices.
officesstring[]all officesMatch the office a posting hangs under: ["Dublin", "Remote"]. Switches on the large response (slower, see Two responses).
remoteOnlybooleanfalseKeep only postings whose location or office text reads remote. This is a text verdict, not a source flag — see isRemote is our verdict.
withSalaryOnlybooleanfalseKeep only postings that publish a pay range.
postedWithinDaysintegerno limit1–365. Keep postings first published in the last N days (UTC).
sinceHoursintegerno limit1–8760. Keep postings changed in the last N hours (UTC), edits and re-posts included.
onlyNewbooleanfalseEmit only postings this actor has not delivered before — see onlyNew.
includeDescriptionbooleanfalseAdd descriptionHtml and descriptionText. Switches on the large response.
sortBystringupdatedDescupdatedDesc, publishedDesc, titleAsc, boardOrder — see Sort orders.
maxItemsinteger251–5000. Total rows written, across all companies.
maxItemsPerBoardintegerno cap1–2000. Rows taken from each company before the global cap, so one big employer cannot fill the dataset.
fieldsstring[]all fieldsKeep only these output fields, in this order. boardToken and found are always kept.

Reference

Finding the board token

The token is the last part of the board address a company links to from its careers page:

What you haveToken
https://job-boards.greenhouse.io/stripestripe
https://boards.greenhouse.io/databricks/jobs/8439170002databricks
https://boards.greenhouse.io/embed/job_board?for=cloudflarecloudflare
https://boards-api.greenhouse.io/v1/boards/gitlab/jobsgitlab

Paste any of those addresses into boardTokens and the token is taken from them. The token is not always the company name (stripe works, plenty of others differ), and a link with ?gh_jid=… on a company's own domain does not contain it — open the board itself and copy the address. Tokens are matched case-insensitively and written back in lower case.

A token nobody publishes ends as one row with found: false and an error that says so; the other companies in the same run are unaffected.

Sort orders

ValueOrder
updatedDescLast change first (updatedAt) — the default, best for watching a board.
publishedDescNewest posting first (firstPublishedAt) — best for hiring signals.
titleAscJob title A→Z.
boardOrderUntouched, exactly as the company's board returns them.

With several companies the rows are sorted across all of them, after maxItemsPerBoard has been applied per company.

Departments and offices

There is no global list: every company builds its own tree ("Sales > Enterprise Sales", "Engineering > Platform"). To see what a company uses, run it once without the filter and read departmentPath and offices. The filter matches at any level, so ["Sales"] returns the postings of every child department too.

A filter value no department on that board contains is compared with the board's own names: an obvious typo is corrected and logged (filter value "Enginering" read as "Engineering"), and a value nothing resembles is kept as typed and reported in the log and in the run's SUMMARY. A filter never silently widens into the whole board.

Two responses

The actor makes two requests per company: the board's postings and the department tree. When you ask for includeDescription or an offices filter, the first request switches to the variant that carries the full posting text — for a board with a few thousand openings that is a multi-megabyte answer, so those runs take a few seconds longer. Nothing else changes the number of requests: one company is one pass, the API returns the whole board at once and there is no pagination to walk.

Locations are free text

location is a string the employer typed into Greenhouse, not a structured place. Real examples from one day: Dublin, Seattle, San Francisco, New York City, Remote - California, Bengaluru, India; Mumbai, India, US-SF-HQ, US-NYC, US-CHI, US-SEA, and at one company simply Hybrid. Match on a city, state or country word rather than on an exact string, and expect a posting to name several places at once.

isRemote is our verdict

Greenhouse has no remote flag. isRemote is true when the location or an office name contains wording such as remote, distributed, anywhere, work from home or virtual, and remoteOnly uses the same test. A posting an employer describes only as "Hybrid" is therefore not remote here, and a company that hides remote work in the description text will not be caught.

onlyNew across scheduled runs

With onlyNew: true the actor remembers company, job id and updatedAt of every row it wrote, in its own named key-value store, and skips those rows next time. An edited posting counts as new again, because its updatedAt moved. The first run returns everything it finds; rows cut off by maxItems are not remembered, so the next run picks them up.

Examples

Every open engineering role at one employer

{ "boardTokens": ["stripe"], "titleKeywords": ["engineer"], "sortBy": "updatedDesc", "maxItems": 20 }

Remote openings across a shortlist, fairly shared between companies

{ "boardTokens": ["duolingo", "gitlab", "databricks"], "remoteOnly": true, "maxItemsPerBoard": 10, "sortBy": "publishedDesc", "maxItems": 25 }

Only the postings with a published pay range

{ "boardTokens": ["gitlab"], "withSalaryOnly": true, "sortBy": "updatedDesc", "maxItems": 20 }

Hiring signals: what these accounts started advertising in the last 30 days

{ "boardTokens": ["stripe", "duolingo", "databricks", "cloudflare"], "postedWithinDays": 30, "maxItemsPerBoard": 10, "sortBy": "publishedDesc", "maxItems": 30 }

Everything under Sales, using the company's own department tree

{ "boardTokens": ["databricks"], "departments": ["Sales"], "sortBy": "updatedDesc", "maxItems": 20 }

Full posting text for a search index or an agent, few columns

{ "boardTokens": ["duolingo"], "includeDescription": true, "fields": ["company", "title", "url", "descriptionText"], "maxItems": 10 }

Output

One row per posting. A real row, written by the run uDIlCS5kneZZpDgUs on 2026-09-20 with {"boardTokens": ["gitlab"], "withSalaryOnly": true, "maxItems": 20}:

{
"boardToken": "gitlab",
"company": "GitLab",
"jobId": 8746461002,
"title": "Senior Professional Services Engineer- PubSec - DC Metro",
"url": "https://job-boards.greenhouse.io/gitlab/jobs/8746461002",
"location": "Remote, United States",
"isRemote": true,
"departments": ["Sales", "Consulting Delivery"],
"department": "Consulting Delivery",
"departmentPath": "Sales > Consulting Delivery",
"offices": [],
"office": null,
"salaryMin": 136080,
"salaryMax": 230040,
"salaryCurrency": "USD",
"salaryRangeTitle": "United States Salary Range",
"hasSalary": true,
"firstPublishedAt": "2026-09-02T18:53:10.000Z",
"updatedAt": "2026-09-18T20:46:24.000Z",
"applicationDeadline": null,
"requisitionId": "6881",
"internalJobId": 6497900002,
"language": "en",
"found": true,
"error": null,
"sourceUrl": "https://boards-api.greenhouse.io/v1/boards/gitlab/jobs?pay_transparency=true",
"fetchedAt": "2026-09-20T19:23:38.640Z"
}
FieldTypeAlways filledMeaning
boardTokenstringyesThe company's board token, in lower case
companystringfor found: trueCompany name as the board publishes it
jobIdnumberfor found: trueGreenhouse posting id (the gh_jid in apply links)
titlestringfor found: trueJob title
urlstringfor found: trueWhere the posting is published — often the company's own careers site
locationstringalmost alwaysFree text the employer typed (see Locations are free text)
isRemotebooleanyesOur text verdict, not a source flag
departmentsstring[]when the tree is readableDepartment names from root to leaf
departmentstringwhen the tree is readableThe leaf department
departmentPathstringwhen the tree is readabledepartments joined with >
officesstring[]only in the large responseOffices the posting hangs under, all of them
officestringonly in the large responseThe first office of offices
salaryMinnumberwhen publishedLower end of the pay range, whole currency units (the API sends cents)
salaryMaxnumberwhen publishedUpper end of the pay range
salaryCurrencystringwhen publishedISO currency code, e.g. USD, GBP
salaryRangeTitlestringwhen publishedThe employer's label for the band, e.g. "United States Salary Range", "Zone 3 Pay Range"
hasSalarybooleanyesWhether a pay range was published for this posting
firstPublishedAtstringfor found: trueWhen the posting first appeared, ISO 8601 UTC
updatedAtstringfor found: trueWhen it last changed, ISO 8601 UTC
applicationDeadlinestringrarelyDeadline the employer set, ISO 8601 UTC
requisitionIdstringusuallyThe employer's own requisition number
internalJobIdnumberfor found: trueGreenhouse's internal id of the job behind the posting
languagestringusuallyLanguage code of the posting
descriptionHtmlstringwith includeDescriptionThe description as published, HTML
descriptionTextstringwith includeDescriptionThe same text with entities decoded and tags stripped
foundbooleanyesfalse marks a board token that returned nothing
errorstringon found: falseWhy that token gave no postings
sourceUrlstringyesThe exact API URL this row came from
fetchedAtstringyesWhen the run read the board, ISO 8601 UTC

A board token the API does not serve produces one row like this instead of failing the run:

{
"boardToken": "zzz-no-such-board-xyz",
"found": false,
"error": "no Greenhouse board is served for the token \"zzz-no-such-board-xyz\" (the API answered 404). Check it against the company's board URL job-boards.greenhouse.io/<token> — the token is often not the company name.",
"title": null,
"hasSalary": false,
"sourceUrl": "https://boards-api.greenhouse.io/v1/boards/zzz-no-such-board-xyz/jobs?pay_transparency=true",
"fetchedAt": "2026-09-20T19:23:52.437Z"
}

Dataset views: Open positions (the everyday columns), Published pay ranges (only the money columns), Recently published (first published, department, location — the hiring-signal view). A SUMMARY record in the run's key-value store holds what each company returned, which filters were applied or corrected, and what was cut.

Use it from code / agents

curl -X POST "https://api.apify.com/v2/acts/yadroo~greenhouse-jobs/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"boardTokens":["stripe"],"titleKeywords":["engineer"],"fields":["company","title","location","url"],"maxItems":20}'
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('yadroo/greenhouse-jobs').call({ boardTokens: ['gitlab'], withSalaryOnly: true, maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("yadroo/greenhouse-jobs").call(run_input={"boardTokens": ["databricks"], "departments": ["Sales"], "maxItems": 20})
items = client.dataset(run["defaultDatasetId"]).list_items().items

The dataset can also be downloaded as JSON, CSV, XLSX or XML from the run's dataset URL, and fields keeps the export to the columns you need.

MCP: add https://mcp.apify.com to Claude / Cursor / any MCP client and call the yadroo/greenhouse-jobs tool with the same JSON input.

Pricing

Pay per event: $0.001 per run start + $0.0015 per dataset row. A row is one posting — or the marker row of a board token that returned nothing. Nothing else is charged; maxItems is the hard ceiling on what a run can cost.

RunCost
25 rows (the default)$0.0385
200 rows$0.301
1000 rows$1.501

Apify's paid plans discount every tier: Bronze −10 %, Silver −20 %, Gold and above −30 % on the per-row price.

Limits & FAQ

Which companies can I read? Only those that publish through Greenhouse and leave the board's public API on — which is the normal setup, because the company's own careers page reads the same API. There is no search across employers here: you bring the board tokens.

How fresh is it? The API answers live, so a run sees the board as it is at that moment. Closed postings disappear from the board, and this actor keeps no history — if you need one, run it on a schedule and keep the datasets, or use onlyNew.

Why does a posting have no pay range? Because the employer did not publish one. Ranges come from the pay transparency data an employer fills in, mostly under US, EU or UK disclosure rules; whole boards can be without them (on 2026-09-20, GitLab published a range on 86 of 216 postings, Stripe on none of 665). hasSalary tells you which case you are in. When an employer publishes several bands for one posting, the row carries the first one and salaryRangeTitle names it.

Why is office empty? Offices only come with the large response, so a run without includeDescription and without an offices filter leaves offices/office empty. departmentPath is always filled — that tree is read separately.

Rate limits. Greenhouse documents none for this API. The actor stays polite anyway: it reads the companies one after another, two requests each, and retries with a growing delay on 429 and 5xx. If a board keeps refusing, that company ends as a found: false row with the reason and the run continues.

Empty result, no error? That means the filters matched nothing on boards that were read fine — the log and the SUMMARY record say how many postings each company had and how many the filters cut. Invalid input is different: a value that is not a board token stops the run with a message naming it, and a token the API does not serve becomes a found: false row.

What is not collected. No candidate, recruiter or applicant data — the Job Board API serves company postings only, and this actor reads nothing else. It never touches the /embed/ paths that the source's robots.txt disallows, uses no login, no captcha solving and no proxy.

If the source changes. Should Greenhouse restrict or retire the public Job Board API, this actor will be marked paused here rather than worked around.


Made by Yadroo. Sibling actors: hh-kz-vacancies · github-repo-intel · domain-intel · google-news-search · rss-to-json