All platforms

ClearCompany HRMDirect Jobs API.

Pull open roles from ClearCompany's HRMDirect career boards using their public RSS requisition feed, then hydrate each posting from server-rendered detail pages for full descriptions, locations, and departments.

Get API access
ClearCompany HRMDirect
Live
<3haverage discovery time
1hrefresh interval
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.

What's in every response.

Data fields, real-world applications, and the companies already running on ClearCompany HRMDirect.

Data fields
  • Full Job Descriptions
  • Location (City & State)
  • Department Tags
  • Requisition IDs
  • Posted Dates
  • Direct Apply URLs
Use cases
  1. 01Job Board Aggregation
  2. 02ATS Market Mapping
  3. 03Recruitment Analytics
  4. 04Careers Page Monitoring
  5. 05Talent Sourcing Feeds
DIY GUIDE

How to scrape ClearCompany HRMDirect.

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

HybridintermediateNo published server limit; self-throttle to ~3 concurrent requests, ~400ms apartNo auth

Resolve the tenant career board

Every HRMDirect board is served from a per-company subdomain of hrmdirect.com. Older links are frequently captured as http:// even though the site silently upgrades, so normalize the host to https:// before building any request URL.

Step 1: Resolve the tenant career board
import requests

def board_base_url(tenant: str) -> str:
    # e.g. "ecisolutions" -> "https://ecisolutions.hrmdirect.com"
    # Always request over https, even if the source link used http://.
    return f"https://{tenant}.hrmdirect.com"

tenant = "ecisolutions"
base_url = board_base_url(tenant)

Pull the requisition list from the public RSS feed

HRMDirect advertises a public RSS 2.0 feed on every board. The all-filter query enumerates the same open requisitions as the HTML board without coupling discovery to fragile table markup. Extract the numeric req id from each item's link and skip any item whose req is missing or non-numeric.

Step 2: Pull the requisition list from the public RSS feed
import xml.etree.ElementTree as ET
from urllib.parse import urlparse, parse_qs

RSS_PATH = "/employment/rss.php?search=true&dept=-1&city=-1&state=-1"

def list_requisitions(base_url: str) -> list[dict]:
    resp = requests.get(base_url + RSS_PATH, timeout=30)
    resp.raise_for_status()
    root = ET.fromstring(resp.content)
    if root.tag.lower() != "rss":
        raise ValueError("Response was not an RSS envelope")

    listings, seen = [], set()
    for item in root.findall("./channel/item"):
        link = (item.findtext("link") or "").strip()
        req = parse_qs(urlparse(link).query).get("req", [""])[0]
        if not req.isdigit() or req in seen:
            continue  # skip malformed links and duplicate reqs
        seen.add(req)
        listings.append({
            "external_id": req,
            "listing_url": f"{base_url}/employment/view.php?req={req}",
            "title": (item.findtext("title") or "").strip(),
            "posted_at": item.findtext("pubDate"),
        })
    return listings

Fetch each requisition's detail page

RSS item descriptions are truncated or empty, so the full posting only exists on the server-rendered detail page at /employment/view.php?req={id}. Throttle these fetches (the board self-limits at ~3 concurrent, ~400ms apart) and treat a 404 as a removed job rather than a transient error.

Step 3: Fetch each requisition's detail page
import time

def fetch_detail_html(base_url: str, req: str) -> str | None:
    url = f"{base_url}/employment/view.php?req={req}"
    resp = requests.get(url, timeout=30)
    if resp.status_code == 404:
        return None  # requisition removed
    if resp.status_code in (403, 429):
        raise RuntimeError(f"Rate limited / blocked (HTTP {resp.status_code})")
    resp.raise_for_status()
    time.sleep(0.4)  # pace requests to stay under the board's limits
    return resp.text

Parse title, description, and view fields

The detail page carries the title (div.reqResult h2, falling back to any h2 or the <title> tag minus its ' - Careers At {Company}' suffix), the full description in div.jobDesc, a two-column table.viewFields block holding Location and Department, and the apply link in a.applyOnline / div.applyDiv.

Step 4: Parse title, description, and view fields
from bs4 import BeautifulSoup

def parse_detail(base_url: str, req: str, html: str) -> dict:
    soup = BeautifulSoup(html, "html.parser")
    detail_url = f"{base_url}/employment/view.php?req={req}"

    h2 = soup.select_one("div.reqResult h2") or soup.select_one("h2")
    title = h2.get_text(strip=True) if h2 else None
    if not title and soup.title:
        title = soup.title.get_text(strip=True).split(" - Careers At")[0].strip()

    desc_el = soup.select_one("div.jobDesc")
    description = desc_el.get_text(" ", strip=True) if desc_el else ""

    fields = {}
    for row in soup.select("table.viewFields tr"):
        name = row.select_one("td.viewFieldName")
        value = row.select_one("td.viewFieldValue")
        if name and value:
            fields[name.get_text(strip=True).rstrip(":")] = value.get_text(strip=True)

    apply_el = soup.select_one("a.applyOnline[href], div.applyDiv a[href]")
    apply_url = apply_el["href"] if apply_el else detail_url

    return {
        "external_id": req,
        "listing_url": detail_url,
        "apply_url": apply_url,
        "title": title,
        "description": description,
        "location": fields.get("Location"),
        "department": fields.get("Department"),
    }
Common issues
highRSS item descriptions are truncated or blank, so the feed alone never yields the full posting body.

Use /employment/rss.php only for discovery, then fetch the /employment/view.php?req={id} detail page for the full description, location, and department.

mediumBoards are often linked over http:// and silently upgrade to https://, causing redirect churn and canonical URL mismatches.

Normalize the board host to https:// and force every detail and apply URL to https before requesting.

lowTenants with zero open requisitions still return HTTP 200 with a valid but item-less RSS envelope.

Treat an empty <item> list as 'no open roles' rather than a parse failure; only error when the envelope itself is malformed or not RSS.

mediumAggressive concurrency triggers HTTP 403 (blocked) or 429 (rate limited) on both the feed and detail pages.

Keep detail fetches to about 3 concurrent requests spaced ~400ms apart, and back off when a 429 is returned.

lowSome RSS <link> values lack a numeric req query parameter, producing broken detail URLs if used directly.

Skip any item whose req parameter is missing or non-digit before building the /employment/view.php URL.

Best practices
  1. 1Discover with the RSS feed, then hydrate each req from its detail page — never trust RSS descriptions for full text.
  2. 2Normalize every board host and detail URL to https:// before requesting.
  3. 3Cap detail fetches at ~3 concurrent with ~400ms spacing to avoid 403/429 responses.
  4. 4Validate that each detail URL resolves to /employment/view.php with a matching numeric req before fetching.
  5. 5Treat detail-page 404s as removal signals, not transient failures.
  6. 6Cache each tenant's req set between runs and only re-fetch details for new or changed requisitions.
Or skip the complexity

One endpoint. All ClearCompany HRMDirect jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=clearcompany hrmdirect" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

Access ClearCompany HRMDirect
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.

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