- highCompany subdomain returns 404
- Not every company enables BambooHR's public career page, and some use a different subdomain. Validate with /careers/company-info first; a 404 there means the tenant is invalid, not that scraping failed.
- mediumEndpoint returns an HTML login page instead of JSON
- Private or gated tenants redirect to a login page rather than serving the JSON feed. Detect an HTML body (leading '<' or a login marker) and treat it as auth-required instead of trying to parse it as jobs.
- mediumMissing posted date and description in listings
- The /careers/list feed only carries basic fields. datePosted, description, compensation, and minimumExperience live on /careers/{id}/detail, so fetch details per job when you need them.
- lowTwo location objects with different data
- Responses include both 'location' and 'atsLocation'. Prefer 'atsLocation' for country and state, and fall back to 'location' fields like postalCode when they are present.
- lowRemote jobs have null city and state
- Remote roles often leave the 'location' object mostly null. Use locationType '1' to flag remote positions and read atsLocation as a fallback for any available geography.
- lowCompensation is null or inconsistently formatted
- Salary is optional and arrives as a free-text string when present. Null-check the field and treat it as an opaque display string rather than a parseable range.
BambooHR Jobs API.
Extract job listings from thousands of small and mid-market employers through a clean, no-auth JSON API that returns departments, workplace types, and experience levels in structured form.
What's in every response.
Data fields, real-world applications, and the companies already running on BambooHR.
Data fields
- Full Job Descriptions
- Department & Employment Type
- Workplace Type Codes
- City / State / Country Locations
- Posted Dates
- Minimum Experience Level
Use cases
- 01SMB Job Monitoring
- 02Mid-Market Talent Sourcing
- 03Hiring Signal Tracking
- 04Careers Page Validation
Trusted by
- Cortina Solutions
- Rightsline
- Helcim
How to scrape BambooHR.
Step-by-step guide to extracting jobs from BambooHR-powered career pages—endpoints, authentication, and working code.
import requests
def validate_company(subdomain: str) -> dict | None:
"""Validate a BambooHR company subdomain exists."""
url = f"https://{subdomain}.bamboohr.com/careers/company-info"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
return data.get("result")
except requests.RequestException:
return None
# Example usage
company = validate_company("cortina")
if company:
print(f"Company: {company['name']}")
else:
print("Invalid company subdomain")import requests
def fetch_job_listings(subdomain: str) -> list[dict]:
"""Fetch all job listings from a BambooHR company."""
url = f"https://{subdomain}.bamboohr.com/careers/list"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
jobs = data.get("result", [])
total_count = data.get("meta", {}).get("totalCount", 0)
print(f"Found {total_count} jobs for {subdomain}")
return jobs
# Example usage
jobs = fetch_job_listings("cortina")
for job in jobs[:3]:
print(f"- {job['jobOpeningName']} ({job['departmentLabel']})")def map_workplace_type(location_type: str | None) -> str:
"""Map BambooHR location type codes to readable values."""
workplace_map = {
"0": "On-site",
"1": "Remote",
"2": "Hybrid",
}
return workplace_map.get(location_type, "Not Specified")
# Parse job listings with workplace type
for job in jobs:
workplace = map_workplace_type(job.get("locationType"))
location = job.get("atsLocation", {})
print(f"{job['jobOpeningName']}: {workplace} - {location.get('city', 'N/A')}, {location.get('state', 'N/A')}")import requests
import time
def fetch_job_details(subdomain: str, job_id: str) -> dict:
"""Fetch detailed information for a specific job."""
url = f"https://{subdomain}.bamboohr.com/careers/{job_id}/detail"
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
return data.get("result", {}).get("jobOpening", {})
# Fetch details for all jobs (with rate limiting)
for job in jobs[:5]: # Limit to first 5 for example
details = fetch_job_details("cortina", job["id"])
print({
"title": details.get("jobOpeningName"),
"posted": details.get("datePosted"),
"experience": details.get("minimumExperience"),
"compensation": details.get("compensation"),
})
time.sleep(1) # Be respectful with rate limitingimport requests
import time
def scrape_bamboohr_jobs(subdomain: str) -> list[dict]:
"""Complete BambooHR job scraper with error handling."""
base_url = f"https://{subdomain}.bamboohr.com"
# Validate company exists
try:
info_resp = requests.get(f"{base_url}/careers/company-info", timeout=10)
info_resp.raise_for_status()
company_name = info_resp.json().get("result", {}).get("name", subdomain)
except requests.RequestException:
print(f"Company '{subdomain}' not found or invalid")
return []
# Fetch job listings
try:
list_resp = requests.get(f"{base_url}/careers/list", timeout=10)
list_resp.raise_for_status()
listings = list_resp.json().get("result", [])
except requests.RequestException as e:
print(f"Error fetching listings: {e}")
return []
# Fetch details for each job
results = []
for job in listings:
try:
detail_url = f"{base_url}/careers/{job['id']}/detail"
detail_resp = requests.get(detail_url, timeout=10)
detail_resp.raise_for_status()
details = detail_resp.json().get("result", {}).get("jobOpening", {})
results.append({
"external_id": f"{subdomain}:{job['id']}",
"title": job["jobOpeningName"],
"company": company_name,
"department": job.get("departmentLabel"),
"location": job.get("atsLocation", {}),
"workplace_type": map_workplace_type(job.get("locationType")),
"employment_type": job.get("employmentStatusLabel"),
"description": details.get("description"),
"posted_date": details.get("datePosted"),
"experience": details.get("minimumExperience"),
"compensation": details.get("compensation"),
"url": details.get("jobOpeningShareUrl"),
})
time.sleep(0.5) # Rate limiting
except requests.RequestException as e:
print(f"Error fetching job {job['id']}: {e}")
return results
# Run the scraper
jobs = scrape_bamboohr_jobs("cortina")
print(f"Scraped {len(jobs)} complete job listings")- 1Validate subdomains with /careers/company-info before pulling listings
- 2Detect HTML/login responses and treat them as auth-required, not parse errors
- 3Fetch /careers/{id}/detail only when you need descriptions, dates, or pay
- 4Prefer atsLocation over location for the most complete geography
- 5Throttle detail calls and back off on HTTP 429 rather than hammering the API
- 6Cache results and re-scrape on a schedule rather than fetching on every request
One endpoint. All BambooHR jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=bamboohr" \
-H "X-Api-Key: YOUR_KEY"Try the API.
Test Jobs and Feed endpoints against https://connect.jobo.world with live request/response examples, then copy ready-to-use curl commands.
Access BambooHR
job data today.
One API call. Structured data. No scraping infrastructure to build or maintain — start with the $5 free starting balance and scale as you grow.