HireClick Jobs API.

Collect every vacancy from a HireClick tenant board in one call — the first-party JobBoard API returns the whole job list at once, and each detail page carries JobPosting JSON-LD for the full advert.

Get API access

What's in every response.

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

Data fields

  • Complete Vacancy List In One Call
  • Full Job Descriptions
  • JobPosting JSON-LD
  • Employment Type
  • City-Level Locations
  • Native Numeric Job IDs

Use cases

  1. 01SMB Job Aggregation
  2. 02Local Employer Monitoring
  3. 03Careers Page Extraction
  4. 04ATS Data Pipelines

Trusted by

  • First Manufacturing
  • Midwest Towing
  • Van Buskirk Companies
  • Acura Honda Omaha
DIY GUIDE

How to scrape HireClick.

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

API type
Hybrid
Difficulty
intermediate
Rate limit
No published limit; ~200ms between requests and at most 3 concurrent detail fetches
Authentication
No auth

Identify the tenant from the subdomain

Every HireClick employer gets its own {tenant}.hireclick.com host, and the canonical board is always /jobboard/ on that host. The tenant is the first DNS label; reject the vendor's own marketing hosts and anything that is not exactly three labels ending in hireclick.com.

Step 1: Identify the tenant from the subdomain
import re
from urllib.parse import urlparse

RESERVED = {"admin", "api", "app", "mail", "secure", "support", "www"}
TENANT = re.compile(r"^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$")

def tenant_from_url(url: str) -> str | None:
    host = (urlparse(url).netloc or "").lower().rstrip(".")
    labels = host.split(".")
    # Exactly {tenant}.hireclick.com — a longer host is a lookalike, not a board.
    if len(labels) != 3 or labels[1:] != ["hireclick", "com"]:
        return None
    tenant = labels[0]
    if tenant in RESERVED or not TENANT.match(tenant):
        return None
    return tenant

def board_url(tenant: str) -> str:
    return f"https://{tenant}.hireclick.com/jobboard/"

tenant = tenant_from_url("https://1stmanufacturing.hireclick.com/jb/cnc-machinist/view/249707")
print(board_url(tenant))

Bootstrap the organization GUID from the board page

The board is a server-rendered page whose hidden input#hdnOrgGuid holds the organization GUID that the API is keyed by. There is no way to derive that GUID from the tenant name, so fetch the board first; if the input is missing, the page is not a real HireClick board and the run should stop rather than reporting an empty inventory.

Step 2: Bootstrap the organization GUID from the board page
import requests
from bs4 import BeautifulSoup

API_PATH = "/api/Controllers/JobBoard/GetSettingsAndJobs"

def fetch_org_guid(session: requests.Session, tenant: str) -> str:
    response = session.get(
        board_url(tenant),
        headers={"Accept": "text/html,application/xhtml+xml"},
        timeout=30,
    )
    response.raise_for_status()

    # The board must reference its own API path — that is the first-party proof.
    if API_PATH.lower() not in response.text.lower():
        raise RuntimeError(f"{tenant} did not serve a HireClick board")

    soup = BeautifulSoup(response.text, "html.parser")
    field = soup.select_one("input#hdnOrgGuid")
    guid = (field.get("value") if field else "") or ""
    if not guid.strip():
        raise RuntimeError("HireClick board omitted its organization GUID")
    return guid.strip().upper()

session = requests.Session()
org_guid = fetch_org_guid(session, "1stmanufacturing")
print(org_guid)

Call GetSettingsAndJobs once for the whole board

The API returns the entire JobList collection in a single response — the paging controls on the board only slice that array in the browser, so there is no cursor to follow. Send the XHR header, and note that the payload is often a JSON string containing JSON, which has to be decoded twice.

Step 3: Call GetSettingsAndJobs once for the whole board
import json

def fetch_job_list(session: requests.Session, tenant: str, org_guid: str) -> list[dict]:
    response = session.get(
        f"https://{tenant}.hireclick.com{API_PATH}",
        params={"orgGUID": org_guid},
        headers={
            "Accept": "application/json,text/javascript;q=0.9,*/*;q=0.8",
            "X-Requested-With": "XMLHttpRequest",
            "Referer": board_url(tenant),
        },
        timeout=30,
    )
    response.raise_for_status()

    payload = response.json()
    # HireClick frequently double-encodes: the outer document is a JSON string
    # whose contents are the real object.
    if isinstance(payload, str):
        payload = json.loads(payload)

    jobs = payload.get("JobList")
    if jobs is None:
        raise RuntimeError("HireClick response omitted its JobList collection")
    return jobs

job_list = fetch_job_list(session, "1stmanufacturing", org_guid)
print(f"{len(job_list)} vacancies on the board")

Map rows and drop cross-tenant links

Each row exposes JobTitle, JobURL, JobDescriptionShort, JobCity, and JobType. Canonical detail links are /jb/{slug}/view/{numericId} on the same tenant host — resolve the JobURL, re-check the tenant, and skip the /jb/generalApplication row, which is a speculative application form rather than a vacancy.

Step 4: Map rows and drop cross-tenant links
from urllib.parse import urljoin, urlparse

DETAIL = re.compile(r"^/jb/[^/]+/view/([1-9][0-9]{0,18})$")

def map_row(row: dict, tenant: str) -> dict | None:
    raw = (row.get("JobURL") or "").strip()
    if not raw:
        return None
    absolute = urljoin(board_url(tenant), raw)
    parsed = urlparse(absolute)
    if tenant_from_url(absolute) != tenant:
        return None  # a link that leaves the tenant is never this board's job
    match = DETAIL.match(parsed.path)
    if not match:
        return None  # skips /jb/generalApplication and any other non-vacancy route

    canonical = f"{parsed.scheme}://{parsed.netloc}{parsed.path}"
    return {
        "id": match.group(1),
        "title": (row.get("JobTitle") or "").strip() or None,
        "listing_url": canonical,
        "apply_url": canonical,
        "summary": (row.get("JobDescriptionShort") or "").strip() or None,
        "city": (row.get("JobCity") or "").strip() or None,
        "employment_type": (row.get("JobType") or "").strip() or None,
    }

listings = [m for m in (map_row(r, "1stmanufacturing") for r in job_list) if m]
print(f"{len(listings)} mapped of {len(job_list)} received")

Hydrate each posting from its JSON-LD

The API only carries a short summary, so the full advert comes from the detail page, which publishes a JobPosting JSON-LD block with the description, dates, employment type, hiring organization, and location. A 404 or 410 on that canonical URL is genuine removal evidence; treat anything else as a transport failure.

Step 5: Hydrate each posting from its JSON-LD
def fetch_detail(session: requests.Session, listing: dict) -> dict | None:
    response = session.get(
        listing["listing_url"],
        headers={"Accept": "text/html,application/xhtml+xml"},
        timeout=30,
    )
    if response.status_code in (404, 410):
        return None  # the only responses that prove the posting is gone
    response.raise_for_status()

    soup = BeautifulSoup(response.text, "html.parser")
    for tag in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(tag.string or "")
        except (json.JSONDecodeError, TypeError):
            continue
        for node in (data if isinstance(data, list) else [data]):
            if isinstance(node, dict) and node.get("@type") == "JobPosting":
                return {
                    **listing,
                    "title": node.get("title") or listing["title"],
                    "description_html": node.get("description"),
                    "posted_at": node.get("datePosted"),
                    "valid_through": node.get("validThrough"),
                    "employment_type": node.get("employmentType") or listing["employment_type"],
                    "company": (node.get("hiringOrganization") or {}).get("name"),
                }
    raise RuntimeError("HireClick detail page carried no JobPosting JSON-LD")

for listing in listings[:3]:
    job = fetch_detail(session, listing)
    if job:
        print(job["title"], "-", job["company"])
Common issues
highThe JobBoard API response fails to parse as an object
GetSettingsAndJobs commonly returns a JSON string whose contents are the real payload, so a single json.loads leaves you holding a string. Decode again when the parsed value is a string, then read JobList off the inner object.
highThere is no way to guess the orgGUID from the tenant name
The API is keyed by an opaque organization GUID that appears only in the board page's hidden input#hdnOrgGuid. Always fetch /jobboard/ first and read the GUID from there; a board that does not publish it is not a live HireClick tenant and should fail the run rather than report zero jobs.
mediumThe generic application link is picked up as a vacancy
Boards publish a /jb/generalApplication route for speculative applications alongside real postings. Accept only detail URLs matching /jb/{slug}/view/{numericId} on the same tenant host, which filters that route out and also rejects cross-tenant links.
mediumOnly a truncated description is available
The listing row exposes JobDescriptionShort, not the advert. Fetch each canonical /jb/{slug}/view/{id} page and read the JobPosting JSON-LD block, which is where the complete description, posting date, and hiring organization live.
Best practices
  1. 1Derive the tenant from the first DNS label only, and reject hosts that are not exactly {tenant}.hireclick.com
  2. 2Bootstrap the organization GUID from input#hdnOrgGuid on /jobboard/ before touching the API
  3. 3Send X-Requested-With: XMLHttpRequest plus a board Referer on the API call
  4. 4Decode the response twice when the outer JSON document is a string
  5. 5Treat the single JobList response as the complete board — there is no cursor to follow
  6. 6Accept only 404 and 410 on a canonical detail URL as evidence that a posting was removed
Or skip the complexity

One endpoint. All HireClick jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=hireclick" \
  -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 HireClick
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