# 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: https://hyperjobs.io/ats/bamboohr

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](/docs/reference/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"
```

```json title="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

<Warning title="`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.
</Warning>

**`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](/docs/objects/company) — 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](/docs/reference/coverage) has the tiers.

[Quickstart](/docs/quickstart) · [Pricing](/pricing)
