BambooHR Jobs Scraper — Company Job Boards avatar

BambooHR Jobs Scraper — Company Job Boards

Pricing

from $1.45 / 1,000 posting saveds

Go to Apify Store
BambooHR Jobs Scraper — Company Job Boards

BambooHR Jobs Scraper — Company Job Boards

Every open role on any company's BambooHR careers site as structured data: title, department, location with country, on-site, hybrid or remote, employment type, posted date, pay where stated, full text on request. Incremental mode charges only for what changed. No personal data.

Pricing

from $1.45 / 1,000 posting saveds

Rating

0.0

(0)

Developer

Adderley Data

Adderley Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 hours ago

Last modified

Share

What does BambooHR Jobs Scraper do?

BambooHR Jobs Scraper reads every open role on any company's public BambooHR careers site and returns it as structured data you can load straight into a spreadsheet, a database or a model. Companies that hire through BambooHR publish their roles at <name>.bamboohr.com/careers; if a careers page's "Apply" button leads there, or its job list is the one BambooHR lets a company embed in its own website, this Actor reads it, given the company's BambooHR name.

Give it the company's BambooHR name (acme) or any acme.bamboohr.com link, and for each job opening you get the title, the department, the location with its country, whether the role is on-site, hybrid or remote, the employment type, the pay where the company states it, the date it was posted and a link to the opening — and, if you ask for it, the full text of the opening.

What makes it different:

  • Two kinds of request, both counted. One request reads a board's whole list of openings. Then one request per opening you save reads that opening's own record, because BambooHR's list carries no dates and no pay. Both go to the public JSON behind BambooHR's own careers pages, at the Actor's default of 90 requests a minute. No HTML parsing, nothing to break when a careers page is redesigned.
  • On-site, hybrid or remote, as the company set it. BambooHR asks for a location setting on every opening — On Site, Hybrid or Remote — and workArrangement is that setting, not a guess from the title or the text.
  • Pay as numbers where the company states it. BambooHR's pay field is free text. Where it holds an amount ($70,000 - $100,000 per year, $34 per hour, €85,000), the numbers come back as min, max, currency and period, with the company's own words kept in salary.raw. Where it holds words only (TBD) but the opening's text states a range, that range is read from the text. Where no amount is stated anywhere, the numbers are null; nothing is estimated.
  • Incremental mode. Put the Actor on a schedule and each run returns only openings that are new, changed, back again or gone. Unchanged openings are skipped, not charged, and cost no request for their records.
  • One stable schema. Every row has every field, every time. Unknown is null, never a missing key, so nothing downstream breaks on a sparse opening. The schema is versioned (job.v1) and every Adderley Data jobs Actor uses it, so a BambooHR board, a Lever board and a job board sit in the same table.
  • No personal data. BambooHR attaches its application form to every opening — name, email, phone, address, profile links, the company's own questions and the demographic questions. This Actor never reads the form and has nowhere to put what it asks for. Contact details inside an opening's text are redacted by default.
  • Bounded cost. You set a maximum number of results; the run stops there, and no record is requested for an opening that will not be saved. It also stops at the spending limit you set on the run in Apify.

What BambooHR data can you extract?

FieldWhat it holds
idStable across runs: source:market:sourceJobId. Use it as your primary key.
titleJob title as listed.
company.nameThe hiring company, where the listing names one.
advertiser.nameThe business that placed the listing — often a recruitment agency. Never a person.
location.rawLocation text as listed. Several locations are joined with |.
location.suburbSuburb, when the listing states one.
location.cityCity or area, when the listing states one.
location.regionState or region, e.g. VIC.
location.postcodePostcode, when the source provides it.
location.countryISO 3166-1 alpha-2 country code.
workArrangementon_site, hybrid, remote or unknown.
employmentTypesNormalised: full_time, part_time, contract, casual, temporary, internship, volunteer.
salary.rawThe salary text exactly as shown, or null when the listing shows none.
salary.minLower bound as a number, when the text contains one.
salary.maxUpper bound as a number. Equal to min for a single figure.
salary.currencyISO 4217. Taken from the text, otherwise the market default.
salary.periodhour, day, week, month or year; null when the text does not say.
salary.includesSupertrue / false when the text says so ("plus super", "inc. super"); otherwise null.
classificationsThe source's category and subcategory pairs.
teaserThe short summary shown on the results page.
bulletPointsSelling points shown on the results page.
postedAtWhen the listing was posted, ISO 8601 UTC.
updatedAtThe source's own last-modified time, ISO 8601 UTC. Published by ATS and API sources; null where the site does not show one.
expiresAtExpiry, ISO 8601 UTC, where the source states one.
isPromotedtrue for paid placements. A listing shown both promoted and organic is returned once.
urlLink to the listing.
descriptionNull unless requested. text, optional sanitised html, and contactsRedacted.
changeTypeIncremental runs: NEW, UPDATED, REAPPEARED, EXPIRED (or UNCHANGED if you ask for those). Otherwise null.
firstSeenAtIncremental runs: when this monitor first saw the listing.
contentHashSHA-256 over the fields that define a change. Compare it to detect edits yourself.
scrapedAtWhen this row was produced, ISO 8601 UTC.
sourceSource key, e.g. seek.
marketMarket key, e.g. au, nz.
sourceJobIdThe source's own identifier for the listing.
company.sourceCompanyIdThe source's identifier for the company, when exposed.
company.urlThe company's page on the source site, when exposed.
advertiser.sourceAdvertiserIdThe source's identifier for the advertiser.
schemaVersionAlways job.v1. Breaking changes ship as job.v2 in a new Actor version, never silently.

How BambooHR's fields fill the schema:

  • company.name and company.sourceCompanyId are the board's BambooHR name, e.g. example-freight: BambooHR's job records do not carry the company's display name. company.url is the careers site, e.g. https://example-freight.bamboohr.com/careers.
  • url is the opening's public page, https://<name>.bamboohr.com/careers/<id>.
  • location.raw is the office as the company entered it, city then state (Melbourne, Victoria), or for a remote opening Remote with the place BambooHR records for it (Remote (Canada)). postcode and an office's country come from the opening's own record. location.country is an ISO code, from the country BambooHR names or from a US state, Canadian province or Australian state — never from a city.
  • workArrangement is BambooHR's location setting: on_site, hybrid or remote.
  • classifications holds the department as category. BambooHR has no second level, so subcategory is null.
  • employmentTypes comes from the words of the company's employment status, which each company names for itself: "Full-Time", "Fulltime" and "Full-Time / Permanent" give full_time; "Part Time" gives part_time; "Fixed Term" and "Contractor" give contract; "Casual", "Temporary", "Intern" and "Volunteer" give theirs. A status that names no type, such as "Salaried", gives an empty list.
  • salary is read from BambooHR's pay field as above. A dollar sign is the dollar of the opening's country — USD, CAD, AUD or NZD — and where the country is not known, currency is null. A reading that contradicts itself — a range wider than tenfold, an hourly rate in the thousands, a yearly salary under a thousand, or one figure read from words that state two joined by a word the parser does not know as "to" — keeps the words and drops the numbers.
  • postedAt is the date BambooHR shows as the posting date, at midnight UTC. updatedAt and expiresAt are always null: BambooHR publishes neither.
  • market is always global: a BambooHR board is the company's, not a country's.

How much does it cost to scrape BambooHR job boards?

You pay per opening saved to your dataset — $1.75 per 1,000 openings on Apify's Starter plan — plus $0.005 each time a run starts. There is no monthly rental.

Apify planPricePer listing
Free$1.75 per 1,000 listings$0.00175
Starter (Bronze)$1.75 per 1,000 listings$0.00175
Scale (Silver)$1.60 per 1,000 listings$0.00160
Business (Gold)$1.45 per 1,000 listings$0.00145

Plus $0.005 per run start. Compute and proxy are included in these prices.

What you runCost (USD, Starter plan)
100 listings, one run$0.18
1,000 listings, one run$1.75
10,000 listings, one run$17.50
50,000 listings, one run$87.50
A daily incremental monitor finding about 150 new or changed listings a day, for a month$8.03

Descriptions cost nothing extra here: the text arrives with the opening's record, which is requested for every saved opening anyway. Use incremental mode for anything you run more than once — after the first run you pay only for what changed.

How to scrape a BambooHR careers site

  1. Find the company's BambooHR name. It is the first part of the address in https://acme.bamboohr.com/careers: here, acme. A link to one opening (acme.bamboohr.com/careers/<id>), an older link (acme.bamboohr.com/jobs/view.php?id=<id>) or the address of the embedded job list works too.
  2. Open the Actor in Apify Console and go to the Input tab. Paste one name or link per line into Job boards. Up to 500 boards per run.
  3. Optionally filter: Title keywords, Locations, Departments. Filters are applied to the board's list, so an opening they leave out costs no request.
  4. Set Maximum results. This is also your cost cap.
  5. Press Start. When the run finishes, open the Output tab and export as JSON, CSV, Excel, XML or HTML, or read the dataset through the Apify API.

An opening that matches more than one board or filter is returned once.

Input

FieldTypeDefaultWhat it does
boardsarray—One entry per company: its BambooHR name (in https://acme.bamboohr.com/careers it is "acme"), or any link on that address — the careers site, one of its job openings, an older /jobs/ link or the job list a company embeds in its own website. A company's own website does not say which BambooHR name is behind it, so the bamboohr.com name is needed. Up to 500 boards per run; each is one request for its list of openings, plus one request per opening saved.
keywordsarray—Keep openings whose title contains every word of any keyword, in any order — "engineer data" matches "Senior Data Engineer". Leave empty for all titles.
locationsarray—Keep openings whose city, state or province contains this text ("Melbourne", "Ontario"), in this country ("Canada", "Australia", "AU") where the board's list states it — for a remote opening, or through a US state, Canadian province or Australian state — or with this location type ("Remote", "Hybrid", "On-site"). An office entered with a city and no state is found by its city. Leave empty for all locations.
departmentsarray—Keep openings whose department contains this text, e.g. "Engineering". Leave empty for all departments.
maxResultsinteger100The run stops once this many openings are saved. You are charged per opening saved, so this is also your cost cap. Each opening saved is also one request for its details.
includeDescriptionbooleanfalseOn: the opening's full text comes back with each row, from the request already made for the opening's details, so it costs no extra requests. Off: listing fields only.
descriptionFormattext, text_and_html"text"Plain text, or plain text plus sanitised HTML.
redactContactsbooleantrueOn by default: email addresses and phone numbers inside description text are replaced with [redacted]. This Actor never outputs recruiter names or contact fields.
incrementalbooleanfalseRemember what earlier runs saw and save only openings that are new, changed or gone. Unchanged openings are skipped, not charged and cost no request for their details. Put the Actor on a schedule with this on.
stateKeystring—Optional name for this monitor, e.g. "competitor-engineering". Runs with the same key share memory. Left empty, a key is derived from the boards and filters themselves.
emitExpiredbooleantrueIncremental mode only. When a complete run no longer finds an opening it saw before, save one row with changeType EXPIRED.
emitUnchangedbooleanfalseIncremental mode only. Saves (and charges for) every opening, labelled UNCHANGED where nothing moved.
proxyConfigurationobject{"useApifyProxy":true}Apify Proxy, automatic group, is the default and is what this Actor is tested with.
maxConcurrencyinteger4Parallel requests. The default is deliberately modest.
maxRequestsPerMinuteinteger90An upper bound on request rate across the whole run.

A typical input:

{
"boards": [
"estella"
],
"maxResults": 100
}

The prefilled board, estella, is one company's public BambooHR careers site, used here only as an example of a public BambooHR board. This Actor is not affiliated with that company. The prefill saves 20 openings, so a trial run makes 21 requests: one for the list and one for each opening's record.

Output

One row per opening. This is a synthetic example in the exact shape the Actor returns:

{
"schemaVersion": "job.v1",
"id": "bamboohr:global:215",
"source": "bamboohr",
"market": "global",
"sourceJobId": "215",
"url": "https://example-freight.bamboohr.com/careers/215",
"title": "Data Analyst",
"company": {
"name": "example-freight",
"sourceCompanyId": "example-freight",
"url": "https://example-freight.bamboohr.com/careers"
},
"advertiser": {
"name": "example-freight",
"sourceAdvertiserId": "example-freight"
},
"location": {
"raw": "Melbourne, Victoria",
"suburb": null,
"city": "Melbourne",
"region": "Victoria",
"postcode": "3121",
"country": "AU"
},
"workArrangement": "hybrid",
"employmentTypes": [
"full_time"
],
"salary": {
"raw": "$95,000 - $110,000 per year",
"min": 95000,
"max": 110000,
"currency": "AUD",
"period": "year",
"includesSuper": null
},
"classifications": [
{
"category": "Data",
"subcategory": null
}
],
"teaser": null,
"bulletPoints": [],
"postedAt": "2026-09-21T00:00:00.000Z",
"updatedAt": null,
"expiresAt": null,
"isPromoted": false,
"description": null,
"changeType": "NEW",
"firstSeenAt": "2026-09-21T19:30:12.000Z",
"contentHash": "7ae2db707b8be7867c27cbe64d279dd8bf138bcd52814b5e6effabcb0aba88ef",
"scrapedAt": "2026-09-21T19:30:12.000Z"
}

The Output tab has two table views: Overview (the fields most people want, flattened) and Changes (for incremental runs).

Incremental mode: monitor new roles across companies

Turn on Incremental mode and run the same input on a schedule — hourly, daily, weekly. The Actor keeps a small record of what it has seen and every row tells you what happened:

changeTypeMeaning
NEWFirst time this monitor has seen the opening
UPDATEDSeen before, and its title, department, employment type, place or location setting has changed
REAPPEAREDWas reported as expired and is back
EXPIREDSeen before and no longer on the board. One row, once
UNCHANGEDOnly if you turn on Also save unchanged openings

How it behaves, so there are no surprises:

  • The first run returns everything as NEW. From the second run you pay only for the difference.
  • Change detection runs on the board's list — each opening's title, department, employment type, place and location setting — before any opening's record is requested. That is what keeps an unchanged opening free: no charge and no request. The cost is that an edit only to an opening's text, pay or posted date is not reported as UPDATED. When an opening is saved for any other reason, its row carries the current text and pay.
  • EXPIRED is only ever reported by a complete run. If a run hits your result cap or your spending limit, or a board cannot be read, nothing is declared expired — an opening on a board the run never read is not gone. In an EXPIRED row, company.name is the board name.
  • Each board's list is read in one response, so a run compares whole boards, never pages read seconds apart.
  • Runs share memory when they share a State key. Leave it empty and the key is derived from the boards and filters themselves, so the same input always continues the same monitor. Name it (competitor-engineering) if you want to change filters later without starting again.
  • An opening not seen for 45 days is forgotten.

Descriptions and contact details

Full descriptions are off by default. Turn on Include full descriptions and each row carries description.text (and sanitised description.html if you choose that format): the opening's text as BambooHR's careers page shows it. Because the text comes with the opening's record, which is requested for every saved opening anyway, it costs no extra requests.

Openings sometimes contain a recruiter's email address or phone number. With Redact contact details on — the default — those are replaced with [redacted] and description.contactsRedacted is true; so is a personal profile address (linkedin.com/in/…). Neither description.text nor the optional HTML carries the address behind a link: the HTML keeps each link's words and drops its address, and drops images. The Actor never returns recruiter names or contact details as fields, under any setting: BambooHR's job records name no recruiter or hiring lead, the Actor never reads the application form attached to each opening, and the schema has nowhere to put a person. If your use case is contacting individuals, this is the wrong tool.

What people use it for

  • Competitor and market hiring signals. Which companies are opening which roles, in which departments and places, on-site or remote, and how often — a daily monitor across a list of boards is one scheduled run.
  • Pay-transparency datasets. Stated pay across companies and roles, as numbers with their currency and period, and the company's own words kept beside them.
  • Job aggregators and alert products. A clean feed of new openings from a curated list of employers, deduplicated and labelled by change.
  • Sales and partnership research at company level. Growth signals from hiring, without collecting anything about the individuals involved.
  • Research and teaching. A clean, repeatable dataset with a documented schema.

Using the API

Run it from code with the Apify client, using your own API token:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('adderleydata/bamboohr-jobs-scraper').call({"boards":["estella"],"maxResults":100});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, items[0]?.salary);
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("adderleydata/bamboohr-jobs-scraper").call(run_input={"boards":["estella"],"maxResults":100})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item["title"], item["location"]["raw"], item["workArrangement"], item["postedAt"])

Schedules, webhooks and the Make, Zapier, n8n and Google Sheets integrations all work the way they do for any Apify Actor. The Actor runs with limited permissions and is priced per event, so AI agents can call it through Apify's MCP server as well.

The Actor reads the public JSON that BambooHR's own careers pages read — with no login, no key and no rate tricks — at a modest default rate, and returns facts about job openings. It does not log in, does not solve CAPTCHAs, does not submit applications and does not collect personal information.

What you do with the data is your responsibility. Each company's job text is its own copyright — analyse it, do not republish it. If your project touches personal information, privacy law applies to you wherever you are. This is general information, not legal advice.

Questions

Where do I find a company's BambooHR name? Open any job on the company's careers site. If the address is acme.bamboohr.com/careers/<id>, the name is acme; paste the whole link or just the name.

The company's jobs are on its own website. Many companies show BambooHR's job list inside their own site. Their own address does not say which BambooHR name is behind it, so the Actor asks for the bamboohr.com name instead of guessing. Each job in that list links to <name>.bamboohr.com/careers/<id>; that <name> is the one to use.

A board I gave came back as "not found". BambooHR sends a name it does not know to its own home page, and the run log says the board does not exist. A name whose BambooHR account has expired is reported as expired: the company has no public careers site on BambooHR any more. Either way the other boards in the run are unaffected. A missing board is asked for once, not retried, and a run in which every board is missing finishes with an empty dataset and a message naming each one.

Why does each saved opening cost a request? BambooHR's list of openings carries titles, departments, places and employment status only. The posted date, the pay and the office's postcode and country are in each opening's own record, so the Actor asks for that record once for every opening it saves — and never for an opening a filter, the result cap or incremental mode leaves out. At the default of 90 requests a minute, 100 new openings take a little over a minute.

Can I filter by the date an opening was posted? Not inside the run: the list has no dates, so a date filter could only be applied after paying a request for every opening. Every row carries postedAt; filter on it in your dataset or with a dataset view.

Why does a country filter miss some offices? Filters work on the board's list, which states a country for a remote opening and, through the state, for offices in the United States, Canada and Australia. An office entered with a city and no state states no country on the list, so filter it by its city; its row still carries the country from the opening's own record.

Why is company.name the board name? BambooHR's job records do not include the company's display name. The board name is what BambooHR uses to identify the company, and it is the same on every run.

Does it need a BambooHR login or API key? No. The careers pages and the JSON behind them are public.

Why is employmentTypes sometimes empty? Each company names its own employment statuses in BambooHR. A status whose words name no type — "Salaried", say — gives an empty list rather than a guess.

What if an opening is withdrawn during the run? If an opening disappears in the seconds between the board's list and the request for its record, it is saved with what the list said — title, department, place, location setting and link — and the run log says so.

Can I get recruiter emails or phone numbers? No, by design.

How current is the data? It is read from BambooHR while your run is in progress. postedAt is the posting date BambooHR shows; scrapedAt records when the row was produced.

The field I need is not there. Open an issue on the Issues tab. Fields are added to the schema without breaking existing ones.

Support

Use the Issues tab on this page. We read it every day. Include the run ID and what you expected to see.

Other Adderley Data Actors

Every Actor in a vertical returns the same fields, so adding a source needs no new code on your side.

About

Made by Adderley Data, Melbourne — https://adderleydata.com. Not affiliated with, endorsed by or sponsored by BambooHR. BambooHR is a trade mark of its owner and is used here only to describe what this Actor reads.