HealthBoxHR Jobs API.
Aggregate live vacancies from UK health and social care employers running hosted HealthBoxHR careers feeds. Every company board and application page is server-rendered HTML at a predictable app.hbhr.io URL, so a plain GET-and-parse loop captures titles, salaries, employment types and closing dates with no login or headless browser.
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.
What's in every response.
Data fields, real-world applications, and the companies already running on HealthBoxHR.
- Full Job Descriptions
- Employment Type & Work Style
- Salary & Experience Level
- Location, County & Postcode
- Application Open & Close Dates
- Native Application URLs
- 01Health & Social Care Hiring Feeds
- 02Care Provider Vacancy Tracking
- 03Charity & Nonprofit Recruitment Data
- 04Sector Talent Market Monitoring
How to scrape HealthBoxHR.
Step-by-step guide to extracting jobs from HealthBoxHR-powered career pages—endpoints, authentication, and working code.
import re
import requests
from urllib.parse import urlparse
HOSTS = ("app.hbhr.io", "app.healthboxhr.com")
COMPANY_ID = re.compile(r"^/jobs-feed/web/company/([a-f0-9]{13})/?$", re.I)
def resolve_feed(board_url):
parts = urlparse(board_url)
host = parts.netloc.lower()
assert host in HOSTS, "not a HealthBoxHR host"
m = COMPANY_ID.match(parts.path)
assert m, "not a company-feed URL"
company_id = m.group(1).lower() # 13-hex tenant key, kept verbatim
return f"https://{host}/jobs-feed/web/company/{company_id}", company_idfrom bs4 import BeautifulSoup
HEADERS = {"Accept": "text/html"}
FINGERPRINT_FORM = "form[action*='/job-application/external/store/'] input[name='hash']"
def is_provider_document(soup):
author = soup.select_one("meta[name='author']")
if author and (author.get("content") or "").strip().lower() == "healthbox software":
return True
return soup.select_one(FINGERPRINT_FORM) is not None
def fetch_feed(feed_url):
html = requests.get(feed_url, headers=HEADERS, timeout=30).text
soup = BeautifulSoup(html, "html.parser")
assert is_provider_document(soup), "missing HealthBoxHR document fingerprint"
return soupfrom urllib.parse import urljoin
JOB_ID = re.compile(r"/job-application/external/([a-f0-9]{8}(?:-[a-f0-9]{8}){3})", re.I)
def parse_listings(soup, feed_url, company_id):
jobs = []
for card in soup.select(".card"):
anchor = (card.select_one(".card-header h3 a[href*='/job-application/external/']")
or card.select_one("a[href*='/job-application/external/']"))
if not anchor:
continue
url = urljoin(feed_url, anchor.get("href", ""))
m = JOB_ID.search(url)
title = anchor.get_text(strip=True)
if not m or not title:
continue
location = card.select_one(".card-body.vacancy-item > h5")
summary = card.select_one(".card-body.vacancy-item .mt-3")
jobs.append({
"external_id": m.group(1).lower(), # segmented-hex job key
"company_id": company_id, # carry tenant into detail
"detail_url": url.split("?")[0],
"title": title,
"location": location.get_text(strip=True) if location else None,
"summary": summary.get_text(strip=True) if summary else None,
})
if not jobs and "There are no job vacancies found." in soup.get_text():
return [] # genuinely empty feed, not a parse failure
return jobsdef fetch_detail(job):
resp = requests.get(job["detail_url"], headers=HEADERS, timeout=30)
if resp.status_code in (404, 410):
return None # canonical application was removed
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
form_hash = soup.select_one(FINGERPRINT_FORM)
value = form_hash.get("value", "").strip().lower() if form_hash else None
if value != job["external_id"]:
raise ValueError("application form hash did not match the native job ID")
return soupdef parse_detail(soup, job):
root = soup.select_one(".job-vacancy-details")
assert root is not None, "missing vacancy-details root"
fields = {}
for div in root.select(":scope > .row > div"):
label = div.select_one("h5.card-title")
if not label:
continue
key = label.get_text(strip=True)
if key in fields: # duplicate labels signal a layout change
raise ValueError(f"duplicate field label: {key}")
text = div.select_one("p.card-text")
html = div.select_one("div.card-text")
fields[key] = {
"text": text.get_text(strip=True) if text else None,
"html": html.decode_contents().strip() if html else None,
}
title = soup.select_one("h3")
doc_title = soup.select_one("title")
company = None
if doc_title and "|" in doc_title.get_text():
company = doc_title.get_text().rsplit("|", 1)[-1].strip()
def value(name):
return fields.get(name, {}).get("text")
return {
"external_id": job["external_id"],
"title": title.get_text(strip=True) if title else None,
"description": fields.get("Description", {}).get("html"),
"employment_type": value("Employment Type"),
"work_style": value("Work Style"),
"salary": value("Salary"),
"experience": value("Experience"),
"location": value("Location"),
"posted_at": value("Application Start Date"),
"closes_at": value("Application End Date"),
"company": company or job.get("company"),
}The external-application URL carries only the segmented-hex job hash and never the 13-hex company ID, so it cannot seed a scrape or mint a tenant. Always start from the /jobs-feed/web/company/{id} feed and carry the company ID into each detail fetch.
Confirm the HealthBoxHR document fingerprint first (meta author 'HealthBox Software' or an external-application form hash input), and on detail pages assert the form hash equals the native job ID before mapping any fields.
A company with no published jobs still renders a valid document with the literal 'There are no job vacancies found.' message. Treat that as a genuinely empty feed and only flag a parse error when neither cards nor that message are present.
A themed detail page that repeats an h5.card-title label indicates a layout change, not clean data. Detect duplicate labels and fail the parse rather than silently keeping the last value.
HealthBoxHR serves 404/410 once an external application is withdrawn. Treat those status codes as a removal signal for that job and drop it from the active set instead of retrying as a hard failure.
- 1Start every scrape from the 13-hex company feed and carry the company ID into each detail fetch, since the application page never exposes it.
- 2Confirm the provider document fingerprint (meta author 'HealthBox Software' or the application form hash) before parsing either page.
- 3Cross-check the application form hash equals the native job ID so you never map a wrong or shared-host document.
- 4Key jobs on the segmented-hex external ID and companies on the 13-hex feed ID; keep both verbatim, never hashed or remapped.
- 5Distinguish a genuinely empty feed ('There are no job vacancies found.') from a missing card layout that needs re-inspection.
- 6Self-throttle to ~150ms between requests and cap concurrent detail fetches at ~3, matching the native scraper's pacing.
One endpoint. All HealthBoxHR jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=healthboxhr" \
-H "X-Api-Key: YOUR_KEY" Access HealthBoxHR
job data today.
One API call. Structured data. No scraping infrastructure to build or maintain — start with the free tier and scale as you grow.