Greenhouse is one of the most widely deployed applicant tracking systems in tech, used by thousands of companies including Stripe, Notion, Airbnb, DoorDash, Instacart, and a long tail of well-funded startups. Each company that uses Greenhouse exposes a public job board at boards.greenhouse.io, and collectively those boards represent a continuously refreshed view of where tech hiring is actually happening — not where companies say they are hiring on LinkedIn, but the actual roles posted, the actual departments, the actual remote/onsite mix, and the actual time-on-board for each requisition.
For job board operators, recruiting platforms, talent analysts, and compensation researchers, this is high-value structured data. This post explains why Greenhouse listings matter, what makes large-scale collection harder than it looks, and how to extract it cleanly without writing scraping code yourself.
Greenhouse is unusual in the ATS world: it exposes a public job board at a predictable URL pattern (boards.greenhouse.io/<company_slug>) and even a public JSON endpoint per company. That makes it uniquely accessible compared to ATS systems that hide everything behind authenticated portals. But uniquely accessible at the per-company level is not the same as easy to aggregate at scale.
Coverage is the real engineering problem: A useful Greenhouse dataset is not one company — it is hundreds or thousands. Discovering which companies use Greenhouse, maintaining that slug list as companies migrate ATSs, deduping cross-posted roles, normalizing department and location strings across companies that each invent their own taxonomy, and resolving employment-type and office-type fields that are inconsistently populated — that is the bulk of the work. Single-company collection is trivial. Aggregating across a continuously changing universe of companies and producing a clean, normalized output is not.
Rate behavior is the second issue. Hitting one company slug occasionally is fine. Sweeping thousands of slugs daily, paginating each, and pulling the per-job detail pages where the richer fields live is a different traffic profile, and naive collection scripts get throttled or blocked when the request pattern looks like aggregation. Reliable extraction requires request pacing calibrated to platform tolerance, distributed network paths, and retry logic that handles transient failures cleanly without corrupting the dataset.
Field completeness varies wildly by company. Some companies fill in every department, every office type, every employment type, and every location field. Others list a job title and a city and nothing else. A pipeline that does not normalize across this variance — or that crashes on missing fields — produces brittle outputs that downstream job boards or analytics dashboards cannot rely on. Department normalization in particular is messy: one company’s “Engineering” is another’s “Product Engineering” is another’s “Software Development.” Useful aggregation requires consistent field resolution.
Office-type classification (remote, onsite, hybrid) is the fourth dimension of difficulty. Greenhouse does not enforce a single field for this; companies signal it through location strings (“Remote — United States”), department conventions, or unstructured description text. A useful dataset has to extract a normalized office_type from these inconsistent signals so downstream filters (“remote-only roles”) actually work.
Posted-date drift is the fifth. Greenhouse exposes a posted timestamp that some companies update on every requisition refresh, making roles look newer than they are. Tracking the true age of a listing across daily snapshots requires storing first-seen timestamps and reconciling against Greenhouse’s own field, which is not trivial in a streaming pipeline.
We maintain a Greenhouse Jobs Scraper on Apify that handles per-company collection, pagination, retry logic, field normalization, and office-type resolution. You give it a company slug and an optional keyword filter; it returns clean structured listing data ready for your job board, recruiting product, or research dataset.
Pull all open roles at Stripe:
{
"company_slug": "stripe",
"max_results": 200
}
Pull engineering roles at Notion:
{
"company_slug": "notion",
"keyword": "engineer",
"max_results": 100
}
Sweep a watchlist of companies in one run:
{
"company_slug": ["stripe", "notion", "airbnb", "doordash", "instacart"],
"max_results": 500
}
Keyword is optional — omit it to pull every open role for the company.
Using the Apify Python client:
import apify_client
client = apify_client.ApifyClient('YOUR_API_TOKEN')
run_input = {
'company_slug': ['stripe', 'notion', 'airbnb'],
'keyword': 'engineer',
'max_results': 200,
}
run = client.actor('cryptosignals/greenhouse-jobs-scraper').call(run_input=run_input)
for item in client.dataset(run['defaultDatasetId']).iterate_items():
print(item)
That is the entire integration. No selectors to maintain, no slug discovery to manage, no retry logic to write.
Each listing returns a structured object:
{
"job_title": "Senior Software Engineer, Payments",
"company_name": "Stripe",
"location": "San Francisco, CA",
"department": "Engineering",
"employment_type": "Full-time",
"office_type": "hybrid",
"posted_date": "2026-04-22",
"apply_url": "https://boards.greenhouse.io/stripe/jobs/4567890",
"gh_job_id": "4567890",
"scraped_at": "2026-05-01T09:14:00.000Z"
}
| Field | Type | Description |
|---|---|---|
job_title | string | Role title as posted by the recruiter |
company_name | string | Company name resolved from the Greenhouse board |
location | string | Posted location string (city, region, or remote tag) |
department | string | Department or team as tagged on the listing |
employment_type | string | Full-time, contract, intern, etc. (when available) |
office_type | string | Normalized office type: remote, onsite, or hybrid |
posted_date | string | Date the role was posted (ISO 8601) |
apply_url | string | Direct apply URL on the Greenhouse board |
gh_job_id | string | Greenhouse internal job ID, useful for dedup |
scraped_at | string | ISO 8601 collection timestamp |
Output is available as JSON, CSV, or XLSX. CSV drops straight into pandas, Excel, or a Postgres load for hiring analytics. Apify’s scheduling and webhook integrations let you run a daily refresh of your company watchlist without managing any infrastructure yourself.
The actor uses Pay Per Event pricing at $0.005 per job listing. Free Apify plan users get 10 listings per run for testing; the cap is removed on any paid Apify plan.
| Volume | Cost |
|---|---|
| 1,000 listings (single-company sweep across watchlist) | $5.00 |
| 10,000 listings (broad multi-company aggregation) | $50.00 |
| Daily 2,000-listing refresh | $300.00/month |
Try the Greenhouse Jobs Scraper free on Apify Store →
Apify’s free tier covers initial testing. Sign up here if you do not have an account. The actor plugs into Apify’s scheduling, webhook, and dataset APIs so you can automate recurring ATS data pipelines without building scraping infrastructure yourself.