All platforms

BambooHR Jobs API

Query jobs from BambooHR career boards through one API — and how its list/detail endpoints and numeric locationType codes actually behave.

?source=bamboohrsource reference

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#

BASH
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"
Illustrative — the shape, not a live response
{
  "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:

BASH
# 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 signalEntry-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.

Quickstart · Pricing

Every source above normalises onto the same schema, with the hiring company already joined onto each posting.

Get an API key