HealthcareSource Hiring Jobs API.

HealthcareSource Hiring serves senior-care and hospital boards at {tenant}.hcshiring.com. A public JSON API returns paginated jobs and full descriptions once you read the build version off the board page.

Get API access

What's in every response.

Data fields, real-world applications, and the companies already running on HealthcareSource Hiring.

Data fields

  • Full Job Descriptions
  • Authoritative Job Totals
  • Street, City, State & ZIP
  • Organization Names
  • Opening & Expiry Dates
  • Labor Request IDs

Use cases

  1. 01Senior Care Job Aggregation
  2. 02Healthcare Recruitment Feeds
  3. 03Clinical Hiring Trackers
  4. 04ATS Data Pipelines

Trusted by

  • Accura
  • American Lutheran
  • Americare
  • Aberdeen Health and Rehab
DIY GUIDE

How to scrape HealthcareSource Hiring.

Step-by-step guide to extracting jobs from HealthcareSource Hiring-powered career pages—endpoints, authentication, and working code.

API type
REST
Difficulty
intermediate
Rate limit
No published limit; ~75ms between requests, up to 4 concurrent detail fetches
Authentication
No auth

Resolve the tenant board

Each employer gets a subdomain of hcshiring.com with the board at /jobs. Job pages are /jobs/{id}, where the identifier is an opaque URL-safe string of 16 to 80 characters rather than a number — never coerce it to an integer.

Step 1: Resolve the tenant board
import re
from urllib.parse import urlparse, unquote

SUFFIX = ".hcshiring.com"
TENANT = re.compile("^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$", re.IGNORECASE)
JOB_ID = re.compile("^[A-Za-z0-9_-]{16,80}$")

def parse_hcs(url: str) -> dict | None:
    parsed = urlparse(url)
    host = parsed.netloc.lower().rstrip(".")
    if not host.endswith(SUFFIX):
        return None

    tenant = host[: -len(SUFFIX)]
    if tenant in ("www", "cdn", "") or "." in tenant or not TENANT.match(tenant):
        return None

    segments = [s for s in parsed.path.split("/") if s]
    job_id = None
    if segments:
        if segments[0] != "jobs" or len(segments) > 2:
            return None
        if len(segments) == 2:
            job_id = unquote(segments[1])
            if not JOB_ID.match(job_id):
                return None

    return {"tenant": tenant, "job_id": job_id,
            "board_url": f"https://{tenant}.hcshiring.com/jobs"}

print(parse_hcs("https://aberdeenhealthandrehab.hcshiring.com/jobs/BlYGs4AHMkmUlmkarS36BA"))

Read the build version off the board

Every API path is prefixed with the deployed build version, so you cannot call the API without first loading the board page and reading the version marker it publishes in its bootstrap script. Cache it per tenant and re-resolve it when a call starts returning 404.

Step 2: Read the build version off the board
import requests

VERSION = re.compile("var[ ]+V[ ]*=[ ]*[{][ ]*version:[ ]*['\"]([a-zA-Z0-9]+)['\"]")

def resolve_version(session, tenant: str) -> str:
    board_url = f"https://{tenant}.hcshiring.com/jobs"
    resp = session.get(board_url, headers={"Accept": "text/html"}, timeout=30)
    resp.raise_for_status()

    match = VERSION.search(resp.text)
    if not match:
        raise RuntimeError("board omitted its API version marker")
    return match.group(1)

session = requests.Session()
version = resolve_version(session, "accura")
print("api version:", version)

Page the jobs endpoint

The versioned jobs endpoint returns a jobs array plus a meta object carrying totalJobs and totalPages. Walk pages until you reach totalPages; each row already includes the title, a summary, the organization name and the split address fields.

Step 3: Page the jobs endpoint
import time

def fetch_page(session, tenant: str, version: str, page: int) -> dict:
    url = f"https://{tenant}.hcshiring.com/{version}/api/jobs?page={page}"
    resp = session.get(url, headers={"Accept": "application/json"}, timeout=30)
    resp.raise_for_status()
    return resp.json()

def join_address(row: dict) -> str | None:
    parts = [row.get(key) for key in ("street", "city", "state", "zip")]
    text = ", ".join(p.strip() for p in parts if isinstance(p, str) and p.strip())
    return text or None

def fetch_all(session, tenant: str, version: str) -> list[dict]:
    page, jobs = 1, []
    while True:
        payload = fetch_page(session, tenant, version, page)
        rows = payload.get("jobs") or []
        meta = payload.get("meta") or {}
        total_pages = meta.get("totalPages") or 1

        for row in rows:
            job_id, title = row.get("id"), row.get("title")
            if not job_id or not title:
                continue
            jobs.append({
                "id": job_id,
                "title": title.strip(),
                "summary": row.get("summary"),
                "company": row.get("organization"),
                "location": join_address(row),
                "posted_at": row.get("lastOpening"),
                "closes_at": row.get("validThrough"),
                "url": f"https://{tenant}.hcshiring.com/jobs/{job_id}",
            })

        if page >= total_pages or not rows:
            return jobs
        page += 1
        time.sleep(0.075)

listings = fetch_all(session, "accura", version)
print(f"{len(listings)} open jobs")

Fetch the full description

Descriptions come from the versioned jobDescriptions endpoint, keyed by the same opaque job ID. The response nests the job summary fields under a job object and the body under description; verify the returned id echoes the one you requested before merging.

Step 4: Fetch the full description
def fetch_detail(session, tenant: str, version: str, job_id: str) -> dict | None:
    url = (f"https://{tenant}.hcshiring.com/{version}"
           f"/api/jobDescriptions?id={job_id}")
    resp = session.get(url, headers={"Accept": "application/json"}, timeout=30)
    if resp.status_code in (404, 410):
        return None  # job removed
    resp.raise_for_status()

    payload = resp.json() or {}
    detail = payload.get("jobDescriptions") or {}
    job = detail.get("job") or {}
    if detail.get("id") != job_id or not job:
        raise RuntimeError("HealthcareSource returned a different job than requested")

    return {
        "id": job_id,
        "title": job.get("title"),
        "description_html": detail.get("description"),
        "company": job.get("organization"),
        "location": join_address(job),
        "posted_at": job.get("lastOpening"),
        "closes_at": job.get("validThrough"),
        "labor_request_id": detail.get("laborRequest"),
        "url": f"https://{tenant}.hcshiring.com/jobs/{job_id}",
    }

for row in listings[:3]:
    print(fetch_detail(session, "accura", version, row["id"]))
    time.sleep(0.075)

Recover from a version rollover

The version segment changes whenever HealthcareSource deploys, and stale values start returning 404 mid-crawl. Wrap API calls so a 404 re-reads the board once and retries with the fresh version instead of failing the whole tenant.

Step 5: Recover from a version rollover
class VersionCache:
    def __init__(self, session):
        self.session = session
        self.versions = {}

    def get(self, tenant: str) -> str:
        if tenant not in self.versions:
            self.versions[tenant] = resolve_version(self.session, tenant)
        return self.versions[tenant]

    def refresh(self, tenant: str) -> str:
        self.versions.pop(tenant, None)
        return self.get(tenant)

def call_with_retry(cache: VersionCache, tenant: str, build_url) -> dict:
    version = cache.get(tenant)
    resp = cache.session.get(build_url(version),
                             headers={"Accept": "application/json"}, timeout=30)
    if resp.status_code == 404:
        # The build rolled over mid-crawl — re-read the board and retry once.
        version = cache.refresh(tenant)
        resp = cache.session.get(build_url(version),
                                 headers={"Accept": "application/json"}, timeout=30)
    resp.raise_for_status()
    return resp.json()

cache = VersionCache(session)
payload = call_with_retry(
    cache, "accura",
    lambda v: f"https://accura.hcshiring.com/{v}/api/jobs?page=1")
Common issues
criticalEvery API call returns 404
The API is namespaced under the deployed build version, so /api/jobs alone does not exist. Load the board page first and read the version marker out of its bootstrap script, then place that value between the host and /api in every request path.
highA crawl fails halfway through with 404s
A deploy rotates the version segment while your crawl is running, invalidating a cached value. Treat a 404 on an API path as a signal to re-read the board once and retry with the fresh version rather than aborting the whole tenant.
highJob IDs are mangled or collide
Identifiers are opaque URL-safe strings such as BlYGs4AHMkmUlmkarS36BA, not numbers. Parsing them as integers, lowercasing them or trimming them truncates real IDs and merges distinct jobs. Store them verbatim and URL-encode when building paths.
lowLocations come through as fragments
The address is split across street, city, state and zip fields, all of them optional. Join whichever are present into one string instead of reading city alone, otherwise multi-site operators end up with jobs that all look identically located.
Best practices
  1. 1Read the build version off the board page and cache it per tenant
  2. 2Re-resolve the version and retry once when an API path returns 404
  3. 3Drive pagination off meta.totalPages and stop on an empty page
  4. 4Store the opaque job ID verbatim and URL-encode it in request paths
  5. 5Verify the detail response's id echoes the job you requested
  6. 6Join street, city, state and ZIP into one location string
Or skip the complexity

One endpoint. All HealthcareSource Hiring jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=healthcaresource hiring" \
  -H "X-Api-Key: YOUR_KEY"
Developer tools

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.

Ready to integrate

Access HealthcareSource Hiring
job data today.

One API call. Structured data. No scraping infrastructure to build or maintain — start with the $5 free starting balance and scale as you grow.

99.9%API uptime
<200msAvg response
50M+Jobs processed