- highThe etrec106gf.json / etrec107gf.json routes return a server-rendered HTML session or error page instead of JSON, usually after the guest USESSION has lapsed.
- Detect a body that begins with '<' and treat it as a failure, not an empty board. Re-fetch the .open page for a fresh USESSION and retry the JSON call.
- highHitting the JSON endpoints without the mhrParams header (and X-Requested-With: XMLHttpRequest) returns an error page rather than vacancy data.
- Send the WVID/USESSION/LANG string in both the query and an mhrParams header, appending RESULTS_PP/TOTAL_REC/REC_FROM/REC_TO for every page after the first.
- mediumUSESSION is volatile guest state, not a durable key; reusing a stale token across a long crawl or persisting it in job identity breaks later requests.
- Bootstrap a fresh USESSION from the .open page immediately before each JSON burst, and never store it in an external ID or metadata.
- mediumTenants publish either the ETREC105GF or ETREC179GF search route while detail lives on ETREC107GF; swapping one route for another yields the wrong board or a 404.
- Preserve the exact tenant instance, WVID and route observed on the host, and only exchange the trailing .open route for the matching .json endpoint.
- mediumPagination runs against an authoritative total_rec, so stopping early or when the total shifts mid-crawl produces a partial or inconsistent snapshot.
- Continue until rec_to equals total_rec, and discard the snapshot if total_rec changes between pages before restarting.
MHR iTrent Jobs API.
Extract every live vacancy from iTrent-powered career sites — the UK HR and payroll platform behind many councils, universities, and public-sector employers — as structured JSON with salaries, locations, and closing dates.
What's in every response.
Data fields, real-world applications, and the companies already running on MHR iTrent.
Data fields
- Full Job Descriptions
- Salary Text & Pay Bands
- Locations & Regions
- Contract Basis & Hours
- Business Reference Codes
- Posted & Closing Dates
Use cases
- 01UK Public-Sector Job Boards
- 02Council & University Vacancy Feeds
- 03Salary Benchmarking
- 04Recruitment Market Analytics
Trusted by
- UWE Bristol
- Rotherham Council
- Reading Borough Council
How to scrape MHR iTrent.
Step-by-step guide to extracting jobs from MHR iTrent-powered career pages—endpoints, authentication, and working code.
import re
import time
import requests
# A live iTrent board: host label and /{instance}_webrecruitment/ must match.
# Use the tenant's exact route (ETREC105GF or ETREC179GF).
BOARD = "https://ce0164li.webitrent.com/ce0164li_webrecruitment/wrd/run/ETREC105GF.open"
WVID, LANG = "8433573cTb", "USA"
http = requests.Session()
http.headers["User-Agent"] = "Mozilla/5.0"
def bootstrap_session(open_url, params):
"""The .open page embeds a fresh USESSION token in its response."""
resp = http.get(open_url, params=params, timeout=30)
resp.raise_for_status()
match = re.search(r"[?&]USESSION=([A-F0-9]{16,})", resp.text, re.IGNORECASE)
if not match:
raise RuntimeError("iTrent bootstrap did not expose a USESSION - retry")
return match.group(1), resp.url
usession, referer = bootstrap_session(BOARD, {"WVID": WVID, "LANG": LANG})def endpoint(board_url, name):
base = board_url.rsplit("/", 1)[0] # .../wrd/run
return f"{base}/{name}"
LISTINGS = endpoint(BOARD, "etrec106gf.json")
def fetch_listings(page=None):
session_qs = f"WVID={WVID}&USESSION={usession}&LANG={LANG}"
mhr_params = session_qs
if page: # (rec_from, rec_to, results_pp, total_rec) — only after page 1
mhr_params += f"&RESULTS_PP={page[2]}&TOTAL_REC={page[3]}&REC_FROM={page[0]}&REC_TO={page[1]}"
resp = http.get(
LISTINGS,
params={"WVID": WVID, "USESSION": usession, "LANG": LANG},
headers={
"Accept": "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest",
"Referer": referer,
"mhrParams": mhr_params, # required XHR header, else you get HTML
},
timeout=30,
)
resp.raise_for_status()
if resp.text.lstrip().startswith("<"):
raise RuntimeError("iTrent served HTML instead of JSON - refresh USESSION")
return resp.json()results, page = [], None
while True:
payload = fetch_listings(page)
search = payload["search"]
results.extend(payload["results"])
if search["rec_to"] >= search["total_rec"]:
break
nxt_from = search["rec_to"] + 1
nxt_to = min(search["total_rec"], search["rec_to"] + search["results_pp"])
page = (nxt_from, nxt_to, search["results_pp"], search["total_rec"])
time.sleep(2) # single in-flight request, ~2s apart
print(f"{len(results)} of {search['total_rec']} vacancies collected")DETAIL = endpoint(BOARD, "etrec107gf.json")
def fetch_detail(vacancy_id):
# USESSION is volatile — establish a new one per detail via the .open route.
sess, ref = bootstrap_session(
endpoint(BOARD, "ETREC107GF.open"),
{"WVID": WVID, "LANG": LANG, "VACANCY_ID": vacancy_id})
resp = http.get(
DETAIL,
params={"WVID": WVID, "USESSION": sess, "LANG": LANG, "VACANCY_ID": vacancy_id},
headers={
"Accept": "application/json, text/plain, */*",
"X-Requested-With": "XMLHttpRequest",
"Referer": ref,
},
timeout=30,
)
resp.raise_for_status()
profile = resp.json() # JSON array with exactly one object
if len(profile) != 1 or profile[0].get("vacancy_id") != vacancy_id:
raise RuntimeError("iTrent detail did not return the requested vacancy")
job = profile[0]
return {
"vacancy_id": job["vacancy_id"], # persistent job key
"business_ref": job.get("vacancy_ref"),
"title": job["job_titles"],
"description": job["job_description"], # HTML; expect 80+ chars of text
"location": job.get("location"),
"salary": job.get("salary"),
"hours": job.get("con_hrs"),
"posted": job.get("vacancy_d"), # yyyyMMdd
"closes": job.get("app_close_d"), # yyyyMMdd
"apply_url": job.get("apply_url"),
}
for job in results:
detail = fetch_detail(job["vacancy_id"])
time.sleep(2)- 1Bootstrap a fresh USESSION from the .open page right before each JSON call, and never persist it in identity or metadata.
- 2Send WVID/USESSION/LANG in both the query string and the mhrParams header, plus X-Requested-With: XMLHttpRequest.
- 3Preserve the tenant instance, WVID, and exact ETREC105GF/179GF route; only swap the trailing route to reach the JSON endpoints.
- 4Treat any response body starting with '<' as a session/error failure and retry with a new USESSION, not as an empty board.
- 5Page until rec_to equals total_rec, and restart the snapshot if total_rec changes between requests.
- 6Throttle to a single in-flight request roughly 2 seconds apart and back off on HTTP 403/429.
One endpoint. All MHR iTrent jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=mhr itrent" \
-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 MHR iTrent
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.