- 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.
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.
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
- 01SMB Job Aggregation
- 02Local Employer Monitoring
- 03Careers Page Extraction
- 04ATS Data Pipelines
Trusted by
- First Manufacturing
- Midwest Towing
- Van Buskirk Companies
- Acura Honda Omaha
How to scrape HireClick.
Step-by-step guide to extracting jobs from HireClick-powered career pages—endpoints, authentication, and working code.
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))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)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")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")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"])- 1Derive the tenant from the first DNS label only, and reject hosts that are not exactly {tenant}.hireclick.com
- 2Bootstrap the organization GUID from input#hdnOrgGuid on /jobboard/ before touching the API
- 3Send X-Requested-With: XMLHttpRequest plus a board Referer on the API call
- 4Decode the response twice when the outer JSON document is a string
- 5Treat the single JobList response as the complete board — there is no cursor to follow
- 6Accept only 404 and 410 on a canonical detail URL as evidence that a posting was removed
One endpoint. All HireClick jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=hireclick" \
-H "X-Api-Key: YOUR_KEY"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.
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.