How to scrape Indeed, Greenhouse, and Handshake for a recruiting pipeline
Key Takeaways
Why scrape three job boards instead of one? Because they don't overlap the way you'd expect: Indeed is broad but under-discloses salary, Greenhouse is salary-rich but limited to the ~140 companies actually using it as their public board, and Handshake is the only one of the three with meaningful internship and early-career coverage.
Do any of these have an official public API for bulk job search? No. Indeed and Handshake have no public bulk-search API at all; Greenhouse's own API is per-company (you'd need each employer's board token and would still have to query them one at a time), which is exactly the gap a cross-company scraper is built to close.
Is the salary data on these boards reliable, or estimated?
It varies by source and by law — Greenhouse's compensation field is parsed directly from the posting text, so it's only populated when the employer actually discloses a range (increasingly required by state pay-transparency laws). Indeed's salary object includes a salarySource field specifically so you can tell an employer-stated figure from an Indeed estimate.
Three Boards, Three Different Jobs
| Board | Best for | What it uniquely covers |
|---|---|---|
| Indeed | Volume, broad market scanning | 60+ country editions, company revenue/size/CEO enrichment on every job |
| Greenhouse | Tech-sector salary benchmarking | Disclosed compensation ranges at 140+ named tech employers (Anthropic, Stripe, Figma, Coinbase) |
| Handshake | Early-career and internships | The largest early-career platform in the US, 15M+ students across 1,500+ universities |
Indeed: Breadth and Company Enrichment
The Indeed Jobs Scraper reads Indeed's own mobile data source rather than parsing rendered HTML, which is why it returns 40+ fields per job instead of the handful visible on a search results page:
{
"title": "Senior Data Analyst",
"companyName": "Acme Analytics",
"salary": { "salaryMin": 95000, "salaryMax": 130000, "salaryType": "yearly", "salarySource": "employer" },
"location": { "city": "Austin", "admin1Code": "TX", "latitude": 30.267, "longitude": -97.743 },
"isRemote": true,
"companyNumEmployees": "51 to 200",
"companyIndustry": "Data Analytics"
}
It supports two input modes: paste ready-made Indeed search or company URLs directly, or build a search from query + country (a required 2-letter code covering 60+ editions) plus filters for radius, job type, experience level, and how recently a job was posted. companyNumEmployees, companyRevenue, and companyIndustry come attached to every single job — useful for filtering a candidate pipeline by employer size without a separate enrichment step.
Greenhouse: Salary Transparency at Named Tech Companies
Greenhouse has no single search endpoint spanning every company that uses it — each employer runs an isolated board. The Greenhouse Jobs Scraper maintains a curated list of 140+ active boards (Anthropic, Stripe, Databricks, Cloudflare, Roblox, SpaceX, and more) and queries all of them in one call:
{
"title": "Senior Software Engineer, Infrastructure",
"normalized_title": "Senior Software Engineer",
"company": { "name": "Stripe", "board_token": "stripe" },
"compensation": { "min": 190000, "max": 250000, "currency": "USD", "period": "year" },
"workplace_type": "remote",
"experience_level": "senior"
}
compensation is null when an employer doesn't disclose a range — it's parsed directly from the posting text, never estimated. min_salary_usd/max_salary_usd filters let you skip straight to jobs with a disclosed floor above whatever your benchmark is, and employment_type/workplace_type/experience_level are all classified by transparent keyword rules against the title and posting, not a black-box model — so a wrong tag is traceable back to the specific keyword that caused it.
Handshake: Early-Career and Internships
Neither of the above is built for internship or new-grad search. The Handshake Jobs Scraper covers Handshake's 50,000+ active postings — the largest early-career job network in the US:
{
"job_title": "Marketing Intern",
"company_name": "Acme Corp",
"location": "Remote",
"is_remote": true,
"job_type": "Internship",
"salary_min": 20,
"salary_max": 25,
"salary_period": "hourly",
"date": "2026-09-15"
}
datePosted filters to today, 3days, week, or month — for a recurring recruiting feed, today on a daily schedule catches new postings the same day they go live, which matters more here than on Indeed given how quickly early-career roles fill.
Merging Into One Feed
function normalizeJob(source, job) {
const map = {
indeed: () => ({ title: job.title, company: job.companyName, salaryMin: job.salary?.salaryMin, salaryMax: job.salary?.salaryMax, remote: job.isRemote, url: job.jobUrl }),
greenhouse: () => ({ title: job.title, company: job.company.name, salaryMin: job.compensation?.min, salaryMax: job.compensation?.max, remote: job.workplace_type === "remote", url: job.listing_url }),
handshake: () => ({ title: job.job_title, company: job.company_name, salaryMin: job.salary_min, salaryMax: job.salary_max, remote: job.is_remote, url: job.URL }),
};
return { source, ...map[source]() };
}
Once normalized, run the merged list through the Data Deduplicator on title + company — the same postings occasionally get cross-posted to Indeed by companies that also run a Greenhouse board.
FAQ
Which one should you scrape first if you can only pick one? Indeed, for breadth — it's the only one of the three that isn't scoped to a specific company list or a single early-career segment.
Do these actors need a login or API key for the underlying site? No — all three read public search and job-detail pages directly. None require an Indeed, Greenhouse, or Handshake account.
For scraping European job boards specifically, see Scraping European Job Boards for Multi-Market Recruiting, or Aggregating Job Boards Into One Recruiting Feed for the general dedup pattern.