BambooHR Jobs API
Query jobs from BambooHR career boards through one API — and how its list/detail endpoints and numeric locationType codes actually behave.
BambooHR is HR software with hiring built in, and it's the ATS of choice for a
very large number of very small companies. Boards live at
<company>.bamboohr.com/careers. That last sentence is the whole shape of this
source: our queue tracks 15,102 BambooHR boards — the largest queue of any
provider here — and BambooHR does not lead on postings. Small companies have few
open roles. Board count measures reach; posting count measures volume; predicting
one from the other is the mistake sources is at pains
to describe.
Try it#
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "source=bamboohr" \
--data-urlencode "time_frame=7d" \
--data-urlencode "limit=2"{
"data": [
{
"id": "bamboohr:lakeside-dental_142",
"title": "Dental Assistant",
"company": {
"slug": "lakeside-dental",
"name": "Lakeside Dental",
"domain": "lakesidedental.com",
"website": "https://lakesidedental.com",
"logo": "https://media.licdn.com/dms/image/…",
"industry": "Hospital & Health Care",
"employee_count": 22
},
"location": {
"remote": false,
"countries": ["United States"],
"cities": ["Boise"],
"regions": ["Idaho"],
"raw": ["Boise, Idaho, United States"]
},
"employment_type": ["FULL_TIME"],
"work_arrangement": "On-site",
"seniority": "Entry level",
"salary": {
"min": 16,
"max": 20,
"currency": "USD",
"unit": "HOUR",
"annual_min": 33280,
"annual_max": 41600,
"summary": "$16.00-$20.00"
},
"skills": ["chairside assisting", "radiography"],
"taxonomies": ["Healthcare"],
"apply_url": "https://lakesidedental.bamboohr.com/careers/142",
"url": "https://lakesidedental.bamboohr.com/careers/142",
"source": "bamboohr",
"posted_at": "2026-07-09T00:00:00.000Z",
"expires_at": null,
"updated_at": "2026-07-13T17:45:33.000Z"
}
],
"total": 2,
"limit": 2,
"offset": 0
}min: 16 with unit: "HOUR" is an hourly wage, not a catastrophically low
salary. Hourly postings are common on this source, and reading min/max without
reading unit is how you end up with a chart claiming people earn $16 a year.
How BambooHR boards actually work#
Two calls, no auth:
# 1. List — ids and titles only
curl -H "accept: application/json" "https://<company>.bamboohr.com/careers/list"
# 2. Detail — the description and everything else
curl -H "accept: application/json" "https://<company>.bamboohr.com/careers/<id>/detail"<company> is the subdomain. The list returns { result: [{ id, jobOpeningName }] }
— genuinely nothing else, so the detail call is mandatory for every posting. The
detail returns { result: { jobOpening: { … } } }, and that's where
description, datePosted, atsLocation, employmentStatusLabel,
departmentLabel, compensation, locationType and minimumExperience live.
Both endpoints need an explicit accept: application/json header. Without it
you're negotiating for HTML.
The accept header and the two-level result nesting are the entire API surface.
It's the simplest platform on this site — the interesting parts are all in the
fields.
What bites you#
`locationType` is a numeric code, not a word
It's "0", "1" or "2" — strings containing digits. Passing that to anything
expecting Remote / Hybrid / On-site yields null on every posting, and
nothing errors. The mapping is 0 → On-site, 1 → Hybrid, 2 → Remote.
atsLocation is often null while location is fully populated. They're two
different objects with different field names — atsLocation has
city/state/province/country, location has city/state/addressCountry.
Read only atsLocation and a chunk of postings lose their geography entirely
despite the data being right there under a different key. Fall back to the second
object.
The "no vacancies" placeholder is a job. A board with nothing open returns a row whose title is a variant of can't find a vacancy that matches. It has an id. It has a detail page. It will go straight into your index as an open position at that company unless you filter it out by title.
compensation is free text. $16.00-$20.00, or DOE, or an empty string, or
a sentence. It parses often enough to be worth parsing and never reliably enough
to trust without a parser that rejects junk.
Ids are per-company numerics. 142 is not unique — every board has a job 142.
A key of bamboohr:142 collides across 15,102 boards, which is as bad as it
sounds. Ours is scoped: bamboohr:<company>_<id>.
minimumExperience is a decent seniority signal — Entry-level, Mid-level,
Senior, Manager/Supervisor — populated on close to every posting on the
detail. It's BambooHR's own vocabulary, so it needs mapping rather than passing
through.
Or use ours#
This is the platform where the scraping is trivial and the scale is the problem. 15,102 boards, two calls each, one of them per posting. Not hard — just relentless, and none of it is the interesting part of whatever you're building.
You get the detail N+1 already paid, ids scoped so Lakeside Dental's job 142
doesn't overwrite someone else's, the numeric locationType decoded, the
atsLocation/location fallback so postings keep their cities, placeholder rows
filtered out, hourly compensation parsed and normalised to comparable annual
figures, and the company join — which matters
disproportionately here, because a 22-person dental practice is exactly the kind
of employer you have no other record of.
BambooHR is sharded across the heavy refresh tier, attempted every third cycle — 15,102 boards times two calls is why. A new posting can take a few hours to appear. Coverage has the tiers.
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key