Greenhouse Jobs API.
Popular ATS for tech companies and startups. Access jobs from thousands of innovative companies.
Try the API.
Test Jobs, Feed, and Auto-Apply endpoints against https://connect.jobo.world with live request/response examples, then copy ready-to-use curl commands.
What's in every response.
Data fields, real-world applications, and the companies already running on Greenhouse.
- Tech-focused job listings
- Startup ecosystem coverage
- Structured job data
- Department & team info
- Location flexibility data
- Structured pay ranges
- 01Tech job boards
- 02Startup job aggregation
- 03Remote work tracking
How to scrape Greenhouse.
Step-by-step guide to extracting jobs from Greenhouse-powered career pages—endpoints, authentication, and working code.
from urllib.parse import urlparse
# URL patterns:
# https://job-boards.greenhouse.io/{companyToken}
# https://boards.greenhouse.io/{companyToken}
url = "https://job-boards.greenhouse.io/anthropic"
company_token = urlparse(url).path.strip("/")
print(f"Company token: {company_token}") # "anthropic"import requests
import time
def get_greenhouse_jobs(company_token: str) -> list[dict]:
all_jobs = []
page = 1
base_url = f"https://job-boards.greenhouse.io/{company_token}"
while True:
url = f"{base_url}?page={page}&_data="
response = requests.get(url, headers={"Accept": "application/json"})
response.raise_for_status()
data = response.json()
# Jobs are in data["jobPosts"]["data"]
if data.get("jobPosts", {}).get("data"):
all_jobs.extend(data["jobPosts"]["data"])
# Check for more pages
total_pages = data.get("jobPosts", {}).get("total_pages", 1)
if page >= total_pages:
break
page += 1
time.sleep(0.3) # Rate limiting
return all_jobs
jobs = get_greenhouse_jobs("anthropic")
print(f"Found {len(jobs)} jobs")import html
for job in jobs:
# Decode HTML entities in the content
description = html.unescape(job.get("content", ""))
parsed = {
"id": job.get("id"),
"title": job.get("title"),
"location": job.get("location", {}).get("name"),
"department": job.get("departments", [{}])[0].get("name") if job.get("departments") else None,
"description_html": description[:500], # Full description available
"url": f"https://job-boards.greenhouse.io/anthropic/jobs/{job.get('id')}",
}
print(parsed)import requests
def get_job_details(company_token: str, job_id: str) -> dict:
url = f"https://job-boards.greenhouse.io/{company_token}/jobs/{job_id}?_data="
response = requests.get(url, headers={"Accept": "application/json"})
response.raise_for_status()
data = response.json()
return {
"id": data.get("jobPostId"),
"title": data.get("jobPost", {}).get("title"),
"location": data.get("job_post_location"),
"description": data.get("jobPost", {}).get("content"),
"employment_type": data.get("employment", {}).get("name"),
"pay_ranges": [
{
"min": pr.get("min"),
"max": pr.get("max"),
"currency": pr.get("currency"),
"title": pr.get("title"),
}
for pr in data.get("pay_ranges", [])
],
"department": data.get("departments", [{}])[0].get("name") if data.get("departments") else None,
"published_at": data.get("published_at"),
"apply_url": f"https://job-boards.greenhouse.io/{company_token}/jobs/{job_id}#app",
}
details = get_job_details("anthropic", "5431234")
print(f"Title: {details['title']}")
print(f"Pay ranges: {details['pay_ranges']}")def parse_locations(location_str: str) -> list[str]:
"""Parse semicolon-separated locations into a list."""
if not location_str:
return []
# Split by semicolon and clean up whitespace
locations = [loc.strip() for loc in location_str.split(";")]
return [loc for loc in locations if loc]
# Example usage
location_raw = "San Francisco, CA; New York, NY; Remote"
locations = parse_locations(location_raw)
print(f"Locations: {locations}")
# Output: ['San Francisco, CA', 'New York, NY', 'Remote']import requests
def get_jobs_via_boards_api(company_token: str) -> list[dict]:
"""Fetch all jobs using the simpler Boards API."""
url = f"https://boards-api.greenhouse.io/v1/boards/{company_token}/jobs"
response = requests.get(url)
response.raise_for_status()
data = response.json()
# Returns: {"jobs": [{"id", "title", "location": {"name"}, "absolute_url", ...}]}
return data.get("jobs", [])
jobs = get_jobs_via_boards_api("anthropic")
print(f"Found {len(jobs)} jobs")
for job in jobs[:5]:
print(f"- {job.get('title')} ({job.get('location', {}).get('name')})")Verify the company token from the actual career page URL. Some companies use custom domains that redirect away from greenhouse.io. Check both job-boards.greenhouse.io and boards.greenhouse.io domains.
Some company tokens redirect to custom domains. Check for HTTP redirects and follow them. The response may also indicate the company uses a different ATS.
Add 200-500ms delays between requests. While Greenhouse has no official rate limits, aggressive scraping may trigger throttling. Use exponential backoff if errors occur.
Not all companies expose salary information. The pay_ranges array may be empty or missing. Check if the company publishes salaries on their job board and handle null values gracefully.
Job descriptions contain HTML entity encoded text (<, &, etc.). Use Python's html.unescape() function to decode before storage or display.
The location field may contain semicolon-separated values like 'San Francisco; New York; Remote'. Split by semicolon to handle multiple locations properly.
- 1Use the ?_data= query parameter for clean JSON responses
- 2Fetch job details endpoint for structured pay_ranges data
- 3Add 200-500ms delay between requests to avoid throttling
- 4Decode HTML entities in job descriptions using html.unescape()
- 5Handle semicolon-separated locations for multi-location jobs
- 6Cache results - job boards typically update daily
One endpoint. All Greenhouse jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://enterprise.jobo.world/api/jobs?sources=greenhouse" \
-H "X-Api-Key: YOUR_KEY" Access Greenhouse
job data today.
One API call. Structured data. No scraping infrastructure to build or maintain — start with the free tier and scale as you grow.