- criticalAn unknown board GUID returns HTTP 200 with an empty body
- HireTrue answers /job-board?externalId={guid} with a 200 and no body when the GUID is not a board. Treat an empty body, a non-positive primaryKey, or an echoed externalId that differs from the one you sent as 'board not found' — never as a board with zero jobs, which would expire the whole tenant.
- highThe detail endpoint rejects the requisition GUID with HTTP 400
- Requisition details are keyed by the numeric primaryKey from the listings row, not by the externalId GUID. Passing the GUID produces a 400 number-format error. Keep the primary key as the record identifier and use the GUID only for the canonical board and apply URLs.
- mediumOnly a ~300 character preview of the description is available
- The listings row carries a vendor-truncated descriptionPreview. Fetch /job-board/requisitions/{primaryKey} and concatenate descriptionSection1 through descriptionSection5 — a single section is often blank, so joining all five is what produces the complete advert.
- mediumA requisition still resolves after it left the board
- The detail endpoint is board-agnostic and keeps returning an unpublished requisition, so a successful detail response is not proof the job is still listed. Use the board's own requisition collection as the authority for what is open, and reserve HTTP 404/410 plus the exact 400 body 'Incorrect result size: expected 1, actual 0' for genuine removal.
HireTrue Jobs API.
Pull complete requisition collections from HireTrue's ce3 candidate-experience boards — counties, state agencies and private employers — through the same anonymous JSON API the Angular front end calls.
What's in every response.
Data fields, real-world applications, and the companies already running on HireTrue.
Data fields
- Complete Requisition Collections
- Five-Section Job Descriptions
- Salary and Salary Type
- Department, Division & Facility
- Open and Close Timestamps
- Requisition Numbers
Use cases
- 01Public-Sector Job Aggregation
- 02County & State Agency Feeds
- 03Government Hiring Trends
- 04ATS Data Pipelines
Trusted by
- State of Missouri
- York County
- Franklin County
- Alabama State Department of Education
- Navajo Housing Authority
How to scrape HireTrue.
Step-by-step guide to extracting jobs from HireTrue-powered career pages—endpoints, authentication, and working code.
import re
from urllib.parse import urlparse
HOST = "hiretrue-prod.com"
APP_ROOT = "/hiretrue/ce3"
API_ROOT = "/hiretrue/api/ce3"
GUID = re.compile(
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
re.IGNORECASE,
)
ROUTES = {"job-board", "job-board-welcome", "job-application"}
def parse_route(url: str) -> tuple[str, str | None] | None:
parsed = urlparse(url)
if parsed.netloc.lower().removeprefix("www.") != HOST:
return None
parts = parsed.path.strip("/").split("/")
if len(parts) < 4 or parts[0] != "hiretrue" or parts[1] != "ce3":
return None
if parts[2] not in ROUTES or not GUID.match(parts[3]):
return None
requisition = parts[4].lower() if len(parts) == 5 and GUID.match(parts[4]) else None
return parts[3].lower(), requisition
board_id, requisition_id = parse_route(
"https://hiretrue-prod.com/hiretrue/ce3/job-board/"
"a9a97c37-cc24-43d0-a7c3-6a931bc18276?jb=1"
)
print(board_id)import requests
def board_api_url(board_id: str) -> str:
return f"https://{HOST}{API_ROOT}/job-board?externalId={board_id}"
def resolve_board(session: requests.Session, board_id: str) -> dict:
response = session.get(
board_api_url(board_id),
headers={"Accept": "application/json"},
timeout=30,
)
response.raise_for_status()
# An unrecognised GUID returns 200 with an EMPTY body. That is "board not
# found", never "board with no jobs".
if not response.text.strip():
raise RuntimeError(f"HireTrue does not recognise board {board_id}")
board = response.json()
if int(board.get("primaryKey") or 0) <= 0:
raise RuntimeError(f"HireTrue does not recognise board {board_id}")
if (board.get("externalId") or "").lower() != board_id:
raise RuntimeError("HireTrue answered with a different board than requested")
if board.get("permissioned") or board.get("requiresInvitation"):
raise RuntimeError("HireTrue board is gated behind a password or invitation")
return board
session = requests.Session()
board = resolve_board(session, "a9a97c37-cc24-43d0-a7c3-6a931bc18276")
print(board["name"], board["primaryKey"], "internal:", board.get("internal"))from datetime import datetime, timezone
def epoch_ms(value) -> str | None:
if not value or value <= 0:
return None
return datetime.fromtimestamp(value / 1000, tz=timezone.utc).isoformat()
def fetch_requisitions(session: requests.Session, board: dict) -> list[dict]:
response = session.get(
f"https://{HOST}{API_ROOT}/job-board/requisitions",
params={"jobBoardPrimaryKey": board["primaryKey"]},
headers={"Accept": "application/json"},
timeout=60,
)
response.raise_for_status()
rows = response.json()
if not isinstance(rows, list):
raise RuntimeError("HireTrue requisitions API omitted its collection")
return rows
def map_row(row: dict, board_id: str, board: dict) -> dict | None:
requisition_id = (row.get("externalId") or "").lower()
if int(row.get("primaryKey") or 0) <= 0 or not GUID.match(requisition_id):
return None
title = (row.get("title") or row.get("position") or "").strip()
if not title:
return None
return {
"primary_key": str(row["primaryKey"]),
"requisition_id": requisition_id,
"requisition_number": row.get("requisitionNumber"),
"title": title,
"company": board.get("companyName"),
"location": row.get("location"),
"department": row.get("department"),
"division": row.get("division"),
"position_type": row.get("positionType"),
"opened_at": epoch_ms(row.get("openTimestamp")),
"closes_at": epoch_ms(row.get("closeTimestamp")),
"listing_url": f"https://{HOST}{APP_ROOT}/job-board/{board_id}/{requisition_id}",
"apply_url": f"https://{HOST}{APP_ROOT}/job-application/{board_id}/{requisition_id}",
}
rows = fetch_requisitions(session, board)
listings = [m for m in (map_row(r, board_id, board) for r in rows) if m]
print(f"{len(listings)} requisitions mapped of {len(rows)} returned")MISSING = "Incorrect result size: expected 1, actual 0"
def fetch_detail(session: requests.Session, listing: dict) -> dict | None:
response = session.get(
f"https://{HOST}{API_ROOT}/job-board/requisitions/{listing['primary_key']}",
headers={"Accept": "application/json"},
timeout=30,
)
if response.status_code in (404, 410):
return None # canonical removal
# The endpoint answers an unknown key with 400 and this exact message. Any
# other 400 (for example "Invalid UUID string") is a transport bug, not removal.
if response.status_code == 400 and MISSING in response.text:
return None
response.raise_for_status()
detail = response.json()
sections = [
(detail.get(f"descriptionSection{n}") or "").strip()
for n in range(1, 6)
]
description = "\n".join(s for s in sections if s)
if not description:
raise RuntimeError("HireTrue requisition omitted every description section")
return {
**listing,
"description_html": description,
"salary": detail.get("salary"),
"salary_type": detail.get("salaryType"),
"scheduled_hours": (detail.get("scheduledStartHour"), detail.get("scheduledEndHour")),
"posted_at": epoch_ms(detail.get("openDate")) or listing["opened_at"],
}
for listing in listings[:3]:
job = fetch_detail(session, listing)
if job:
print(job["title"], "-", job["salary"])- 1Take both GUIDs from the path and ignore the ?jb=1 flag entirely
- 2Resolve the board first and require it to echo back the externalId you requested
- 3Refuse an empty board response instead of interpreting it as an empty board
- 4Skip boards whose permissioned or requiresInvitation flag is set — those need credentials
- 5Fetch requisition details by numeric primaryKey and keep the GUID for canonical URLs
- 6Read published state from the board collection, not from whether a detail call succeeds
One endpoint. All HireTrue jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=hiretrue" \
-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 HireTrue
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.