HrSmart Jobs API.
Pull structured postings from Deltek's HrSmart career sites, where every detail field carries a stable HTML id — so requisition codes, locations, and descriptions extract cleanly without brittle label matching.
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 HrSmart.
- Full HTML Job Descriptions
- Requisition & Reference Codes
- Multi-Site Locations
- Department & Job Category
- Education & Experience Levels
- Employment Type & Open Date
- 01Higher-ed & healthcare job tracking
- 02Enterprise talent-market monitoring
- 03Careers-page job aggregation
- 04Requisition-level data enrichment
How to scrape HrSmart.
Step-by-step guide to extracting jobs from HrSmart-powered career pages—endpoints, authentication, and working code.
import requests
from bs4 import BeautifulSoup
tenant = "photobox" # leading subdomain of {tenant}.mua.hrdepartment.com
base_url = f"https://{tenant}.mua.hrdepartment.com"
def listing_url(page: int, page_size: int = 100) -> str:
return (
f"{base_url}/hr/ats/JobSearch/viewAll"
f"/jobSearchPaginationExternal_pageSize:{page_size}"
f"/jobSearchPaginationExternal_page:{page}"
)import re
JOB_ID = re.compile(r"/hr/ats/Posting/view/(\d+)")
def parse_rows(html: str) -> list[dict]:
soup = BeautifulSoup(html, "html.parser")
jobs = []
for row in soup.select("#jobSearchResultsGrid_table tbody tr"):
anchor = row.select_one("a[href*='/hr/ats/Posting/view/']")
if not anchor:
continue
m = JOB_ID.search(anchor.get("href", ""))
if not m:
continue
job_id = m.group(1)
cells = row.find_all("td")
wide = len(cells) >= 5
jobs.append({
"external_id": job_id,
"title": anchor.get_text(strip=True),
# Canonicalize — HrSmart sometimes appends /0/13 for share links.
"url": f"{base_url}/hr/ats/Posting/view/{job_id}",
"reference_code": cells[0].get_text(strip=True) if wide else None,
"employment_type": cells[2].get_text(strip=True) if wide else None,
"location": cells[3].get_text(strip=True) if wide else None,
"posted": cells[4].get_text(strip=True) if wide else None,
})
return jobsCOUNT_RE = re.compile(r"\bof\s+(\d+)\b", re.I)
def total_count(html: str) -> int | None:
soup = BeautifulSoup(html, "html.parser")
el = soup.select_one(".pagination_displaying")
if not el:
return None
m = COUNT_RE.search(el.get_text(" ", strip=True))
return int(m.group(1)) if m else None
def scrape_all(max_pages: int = 50) -> list[dict]:
seen, out, total = set(), [], None
for page in range(1, max_pages + 1):
html = requests.get(listing_url(page), timeout=15).text
if not html:
break
new = [j for j in parse_rows(html) if j["external_id"] not in seen]
if page == 1:
total = total_count(html)
if not new: # no new records -> stop
break
for j in new:
seen.add(j["external_id"])
out.append(j)
if total is not None and len(out) >= total:
break
return outdef field(soup, el_id):
el = soup.find(id=el_id)
return el.get_text(" ", strip=True) if el else None
def parse_detail(html: str) -> dict:
soup = BeautifulSoup(html, "html.parser")
desc = soup.find(id="job_details_ats_requisition_description")
location = field(soup, "job_details_hua_location_id")
sites = [s.split("(")[0].strip() for s in (location or "").split("\n") if s.strip()]
return {
"title": field(soup, "job_details_ats_requisition_title"),
"description_html": desc.decode_contents() if desc else None,
"locations": sites or ([location] if location else []),
"employment_type": field(soup, "job_details_hua_job_type_id"),
"department": field(soup, "job_details_hua_org_level_id"),
"category": field(soup, "job_details_ats_requisition_category_id"),
"education_level": field(soup, "job_details_ats_education_level_id"),
"experience_level": field(soup, "job_details_ats_requisition_level_id"),
"open_date": field(soup, "job_details_f_open_date_0"),
"reference_code": field(soup, "job_details_ats_requisition_code"),
}import time
def fetch_detail(url: str) -> dict | None:
resp = requests.get(url, timeout=15)
if resp.status_code in (404, 410):
return None # posting removed
if resp.status_code in (403, 429):
raise RuntimeError("Blocked / rate limited — back off and retry")
resp.raise_for_status()
time.sleep(0.5) # ~500ms between requests; <=3 concurrent
return parse_detail(resp.text)Only *.mua.hrdepartment.com subdomains are HrSmart boards — the bare www.hrdepartment.com root and other ATS hosts (Lever, Greenhouse) are not. Derive the tenant from the leading subdomain and confirm the host matches {tenant}.mua.hrdepartment.com before scraping.
The #jobSearchResultsGrid_table tbody tr selector returns nothing when the tenant has no open jobs or the markup shifts. Treat an empty grid as 'no jobs' rather than an error, and stop paging as soon as a page adds no new posting ids.
The board self-pages through URL segments and will keep returning a page forever. Read the total from the '... of N' text in .pagination_displaying, break when a page yields no new ids, and cap the loop at ~50 pages.
Field labels and date order vary by tenant locale. Read detail fields by their stable id="job_details_*" attributes (never by label text), and parse dates as dd/MM/yyyy with d/M/yyyy and MM/dd/yyyy fallbacks.
#job_details_hua_location_id can list several sites separated by <br> tags, with a '(Principal)' or '(Primary)' parenthetical on the main one. Split on the line breaks and strip the parenthetical before geocoding each site.
- 1Derive the tenant slug from the leading subdomain of {tenant}.mua.hrdepartment.com
- 2Read detail fields by their stable id="job_details_*" attributes, never by localized label text
- 3Stop paging when a page adds no new posting ids, and cross-check against the '... of N' total
- 4Keep to at most 3 concurrent detail fetches with ~500ms between requests
- 5Split multi-site locations on <br> and strip '(Principal)'/'(Primary)' suffixes
- 6Treat a 404/410 on a posting page as a removed job, not a hard failure
One endpoint. All HrSmart jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=hrsmart" \
-H "X-Api-Key: YOUR_KEY" Access HrSmart
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.