Skip to main content
Thirdwatchthirdwatch
Hiring & talent

Scrape Greenhouse Jobs for ATS Enrichment (2026 Guide)

Pull live Greenhouse job listings into your ATS or CRM with Thirdwatch's Multi-ATS Jobs Scraper. Python, no-code, and webhook recipes.

Editorial illustration for hiring & talent
Apr 27, 2026 · 5 min read · 1,241 words
View the Apify scraper →

Thirdwatch's Multi-ATS Jobs Scraper pulls live job listings from Greenhouse-hosted career boards, returning title, department, location, posting date, apply URL, published pay ranges, and full descriptions ready to merge into an ATS or recruiter CRM. It reads Greenhouse alongside Workday, Lever, SmartRecruiters, Workable, Ashby, Recruitee, and BambooHR — one actor and one schema for eight career-board systems, no per-platform scraper to maintain.

Why scrape Greenhouse jobs for ATS enrichment

Recruiting teams using Greenhouse as their own ATS still need data on roles open at other companies — to source passive candidates, benchmark compensation, time outreach to hiring spikes, and feed candidate-matching models. The cleanest source is the same place candidates apply: each company's job-boards.greenhouse.io/{slug} page. According to Greenhouse's 2025 hiring report, more than 7,500 companies use Greenhouse to publish jobs, including most of the YC and venture-backed cohort that recruiters care about most.

Pulling that data manually scales nowhere. A 100-company watchlist refreshed daily means 100 page loads, 100 manual exports, and a half-day of cleanup. Doing it on a six-hour cadence so your sourcers see fresh roles before competitors do is impossible without automation. The job-to-be-done is structured: pull every open role from a list of Greenhouse-hosted careers pages, normalise the schema, dedupe by apply_url, push into the CRM. That is what this actor exists to do, and it works the same way against the other major ATS platforms when a target company switches.

How does this compare to the alternatives?

Three honest options for getting Greenhouse job data into an ATS or CRM:

Approach Cost per 1,000 jobs Reliability Setup time Maintenance
Greenhouse Job Board API (per-company) Free, but per-company auth High when configured 30 min × N companies You manage tokens for every employer
LinkedIn Jobs aggregator scraper $1–$8 (varies) Lags hours to days, misses stealth roles 1 hour Provider absorbs LinkedIn changes
Thirdwatch Multi-ATS Jobs Scraper Pay per result Eight ATS platforms, one schema 5 minutes Thirdwatch monitors and patches upstream

The Greenhouse Job Board API is technically free per company but requires the target employer to share a token — fine when you want only your own jobs, useless when you're tracking 50 competitors. LinkedIn aggregator data is cheap but stale and incomplete. The Multi-ATS Jobs Scraper actor page documents the exact field mapping, but for ATS enrichment workflows the value is going straight to the source for every company in one call.

How to scrape Greenhouse jobs for ATS enrichment in 4 steps

Step 1: How do I authenticate against Apify?

Sign in at apify.com (free tier, no credit card), go to Settings → Integrations, and copy your personal API token. Every example below assumes the token is in APIFY_TOKEN:

export APIFY_TOKEN="apify_api_xxxxxxxxxxxxxxxx"

Step 2: How do I pull jobs from a list of Greenhouse companies?

Pass each company's Greenhouse board URL (job-boards.greenhouse.io/{slug} or boards.greenhouse.io/{slug}) — or the short form greenhouse:{slug} — in boardUrls. Keep includeDescription: true so the full job description text lands in the dataset (you almost always want it for ATS enrichment), and set maxResults high enough for the whole watchlist.

import os, requests

ACTOR = "thirdwatch~multi-ats-jobs-scraper"
TOKEN = os.environ["APIFY_TOKEN"]

resp = requests.post(
    f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
    params={"token": TOKEN},
    json={
        "boardUrls": [
            "https://job-boards.greenhouse.io/discord",
            "https://job-boards.greenhouse.io/airbnb",
            "greenhouse:stripe",
        ],
        "includeDescription": True,
        "maxResultsPerBoard": 500,
        "maxResults": 1500,
    },
    timeout=900,
)
jobs = resp.json()
print(f"{len(jobs)} jobs across {len({j['company'] for j in jobs})} companies")

The dataset returns one row per job with title, company, department, location, employment_type, posted_at, job_url, apply_url, description, the published pay range where there is one, and platform: "greenhouse". The same call works unchanged if you swap a Lever or Workday URL into the list.

Step 3: How do I dedupe against my existing ATS?

Greenhouse exposes a stable apply_url per job, so it makes the perfect natural key. Diff against your last snapshot and ingest only new rows.

import pandas as pd

df = pd.DataFrame(jobs)
df = df.drop_duplicates(subset=["apply_url"])

# Compare against yesterday's snapshot (saved as Parquet)
yest = pd.read_parquet("greenhouse-2026-04-26.parquet")
new_rows = df[~df.apply_url.isin(yest.apply_url)]
print(f"{len(new_rows)} new jobs to push into the ATS")

df.to_parquet(f"greenhouse-{pd.Timestamp.utcnow().date()}.parquet")

new_rows is the delta you push into Greenhouse, Lever, Ashby, or whatever your team uses, attaching the job to a company record by name. Most ATS systems accept a CSV import or have a REST endpoint for creating jobs.

Step 4: How do I run this on a schedule and pipe to a webhook?

Apify's built-in scheduler runs the actor on cron. Add a webhook in the actor's settings to POST the finished dataset URL to your ingestion service whenever a run finishes:

curl -X POST "https://api.apify.com/v2/schedules?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "greenhouse-watchlist-6h",
    "cronExpression": "0 */6 * * *",
    "timezone": "UTC",
    "isEnabled": true,
    "actions": [{
      "type": "RUN_ACTOR",
      "actorId": "thirdwatch~multi-ats-jobs-scraper",
      "runInput": {
        "boardUrls": ["https://job-boards.greenhouse.io/discord", "https://job-boards.greenhouse.io/airbnb"],
        "includeDescription": true,
        "maxResultsPerBoard": 500,
        "maxResults": 1000
      }
    }]
  }'

Add a webhook of type ACTOR.RUN.SUCCEEDED pointing at your ingestion endpoint and you have a closed-loop pipeline: every six hours, fresh Greenhouse jobs land in your ATS without anyone touching a button.

Sample output

A single row from the dataset with descriptions enabled looks like this (abridged — every row carries the same 28 keys). Five rows of this shape weigh ~25 KB.

{
  "platform": "greenhouse",
  "company": "Discord",
  "company_slug": "discord",
  "job_id": "7341234",
  "title": "Senior Backend Engineer",
  "department": "Engineering",
  "location": "San Francisco, CA",
  "posted_at": "2026-04-22T18:30:00Z",
  "salary_min": null,
  "salary_max": null,
  "job_url": "https://job-boards.greenhouse.io/discord/jobs/7341234",
  "apply_url": "https://job-boards.greenhouse.io/discord/jobs/7341234",
  "description": "Discord is the place where people hang out... You'll own services that route 4B+ messages per day. Requirements: 5+ years backend, distributed systems, Go or Rust experience preferred...",
  "board_url": "https://job-boards.greenhouse.io/discord"
}

platform is the field that lets a downstream pipeline branch — for example, only call the Greenhouse-specific deep enrichment for Greenhouse rows. job_id is the posting ID Greenhouse itself uses, so (platform, company_slug, job_id) — or simply apply_url — works as a primary key for ATS upserts. salary_min/salary_max are filled only when the employer publishes a pay range on the board, so many Greenhouse rows leave them empty; for broader compensation data, pair this actor with Glassdoor Scraper or LinkedIn Jobs Scraper.

Common pitfalls

Three issues bite teams that wire this into production. Board URL variants — Greenhouse serves the same board at boards.greenhouse.io/{company}, job-boards.greenhouse.io/{company} and through an embed widget (.../embed/job_board?for={company}); the actor reads all three, but store one canonical form (or the greenhouse:{company} short form) in your watchlist so diffs stay clean. Description formatting — Greenhouse lets employers customise rich-text formatting; the actor returns descriptions as plain text, but expect varied section headings and bullet styles when you feed them to an LLM. White-labelled career pages — some large employers front their Greenhouse board with a branded domain (for example careers.{company}.com); pass the underlying Greenhouse board URL or greenhouse:{slug} instead, which is also the most stable address to hard-code in a watchlist.

Thirdwatch's actor surfaces platform on every record so a downstream pipeline can branch logic per platform without re-detecting from the URL, and a Greenhouse pull and a Lever pull come back with identical schema.

Related use cases

Frequently asked questions

How much does it cost to scrape Greenhouse jobs?

Thirdwatch's Multi-ATS Jobs Scraper uses pay-per-result pricing with tiered volume discounts at higher commitments. A 50-company watchlist with 30 open roles each runs cheaply enough to refresh hourly without budget pain.

Do I need to know which ATS each company uses?

No. The scraper recognises Greenhouse, Lever, Workday, SmartRecruiters, Workable, Ashby, Recruitee, and BambooHR from the board URL. Pass any mix of board URLs in a single run and the actor routes each to the right reader before merging output into one dataset.

Can I get full job descriptions, not just titles?

Yes. Leave includeDescription on (the default) and every row carries the full job description as plain text. Turn it off when you only need a faster listing summary for a hiring-signal feed; keep it on for ATS enrichment or candidate matching.

How fresh is the data?

Each run pulls live from the company's Greenhouse board at request time — there is no cache. Pair the actor with Apify's built-in scheduler to refresh hourly, daily, or on a webhook trigger. Most ATS enrichment pipelines run every six hours and ingest only new apply_url values.

Can I run this against companies on a non-Greenhouse ATS in the same job?

Yes. boardUrls accepts a heterogeneous list. A single run can pull jobs from job-boards.greenhouse.io/stripe, jobs.lever.co/spotify, and nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite simultaneously. The output dataset uses the same field names across platforms and tags each row with platform.

What's the difference between this actor and LinkedIn Jobs Scraper?

LinkedIn Jobs aggregates from LinkedIn's index, which lags employer career pages by hours to days and sometimes misses senior or stealth roles. The Multi-ATS Jobs Scraper goes straight to the source, so it's faster, more complete, and the apply_url points directly to the company's ATS instead of a LinkedIn redirect.

Related