Fusion recruitment platform Jobs API.
Pull vacancies from Fusion-powered UK career sites — server-rendered boards run by logistics, retail-supply, and public-sector employers — with schema.org JobPosting JSON-LD on every detail page.
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 Fusion recruitment platform.
- Full Job Descriptions
- Salary Text
- Employment Type
- Job Sector & Industry
- Location Details
- Posted & Closing Dates
- 01UK Job Market Tracking
- 02Logistics & Supply-Chain Hiring Signals
- 03Public-Sector Vacancy Monitoring
- 04Recruitment Market Research
How to scrape Fusion recruitment platform.
Step-by-step guide to extracting jobs from Fusion recruitment platform-powered career pages—endpoints, authentication, and working code.
import requests
# Host + route are tenant-specific; store the exact pair per deployment.
ORIGIN = "https://jobs.bunzlchs.com"
ROUTE = "Bunzl"
BOARD_URL = f"{ORIGIN}/{ROUTE}/Jobs"
HEADERS = {
"Accept": "text/html,application/xhtml+xml",
"Accept-Language": "en-GB,en;q=0.9",
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
),
}
session = requests.Session() # carries the ASP.NET_SessionId cookie into pagination
resp = session.get(BOARD_URL, headers=HEADERS, timeout=20)
resp.raise_for_status()
html = resp.textfrom bs4 import BeautifulSoup
from urllib.parse import urljoin
def parse_listings(html: str) -> list[dict]:
soup = BeautifulSoup(html, "html.parser")
jobs = []
for card in soup.select(".job-summary"):
anchor = card.select_one("h3 a[href*='/JobDescription/']")
if not anchor:
continue
url = urljoin(BOARD_URL, anchor.get("href", ""))
token = url.rstrip("/").split("/JobDescription/")[-1]
# Label elements are each followed by their value element.
fields = {}
for label in card.select(".job-summary-table-label"):
key = label.get_text(strip=True).rstrip(":")
value_el = label.find_next_sibling()
if key and value_el:
fields[key] = value_el.get_text(" ", strip=True)
jobs.append({
"id": token, # opaque public token — use verbatim, never decode
"title": anchor.get_text(" ", strip=True),
"listing_url": url,
"apply_url": f"{ORIGIN}/{ROUTE}/ApplyNow/{token}",
"reference": fields.get("Ref No"),
"sector": fields.get("Job Sector"),
"employment_type": fields.get("Employment Type"),
"salary": fields.get("Salary"),
"hours": fields.get("Hours of Work"),
"location": fields.get("Location"),
"expires": fields.get("Expiry Date"),
})
return jobs
listings = parse_listings(html)
print(f"Page 1: {len(listings)} jobs")import re
def total_pages(html: str) -> int:
# setJobsCount(...) and navigateJobList(...) markers carry the authoritative state.
pages = [int(n) for n in re.findall(r"navigateJobList\(\s*['\"]?(\d+)", html)]
return max(pages) if pages else 1
def fetch_page(session, page: int) -> str:
resp = session.post(
f"{ORIGIN}/{ROUTE}/NavigateJobList",
data={"intPageNumber": page, "arJobLocationIds": "", "arJobSectorIds": ""},
headers={
**HEADERS,
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"X-Requested-With": "XMLHttpRequest",
"Referer": BOARD_URL,
},
timeout=20,
)
resp.raise_for_status()
return resp.text # HTML fragment, not JSON
all_jobs = list(listings)
seen = {j["id"] for j in all_jobs}
for page in range(2, total_pages(html) + 1):
page_jobs = [j for j in parse_listings(fetch_page(session, page)) if j["id"] not in seen]
if not page_jobs: # defensive: halt if a page yields nothing new
break
seen.update(j["id"] for j in page_jobs)
all_jobs.extend(page_jobs)import json
def fetch_details(session, listing: dict) -> dict:
resp = session.get(listing["listing_url"], headers=HEADERS, timeout=20)
if resp.status_code in (404, 410):
return {**listing, "removed": True}
resp.raise_for_status()
soup = BeautifulSoup(resp.text, "html.parser")
posting = None
for tag in soup.select("script[type='application/ld+json']"):
if "JobPosting" in tag.text:
posting = json.loads(tag.text)
break
if not posting:
raise ValueError("No JobPosting JSON-LD on detail page")
location = (posting.get("jobLocation", {})
.get("address", {})
.get("addressLocality"))
return {
**listing,
"description": posting.get("description"),
"employment_type": posting.get("employmentType", listing["employment_type"]),
"industry": posting.get("industry"),
"company": posting.get("hiringOrganization", {}).get("name"),
"posted_at": posting.get("datePosted"),
"closes_at": posting.get("validThrough"),
"location": location or listing["location"],
}
detailed = [fetch_details(session, j) for j in all_jobs[:3]]GET the board page first inside a persistent requests.Session so the ASP.NET_SessionId cookie is captured, and send X-Requested-With: XMLHttpRequest plus the board Referer on every NavigateJobList POST.
Store the exact host + route per deployment (jobs.bunzlchs.com/Bunzl, jobs.uk.yusen-logistics.com/Yusen, careers.peakdistrict.gov.uk/PeakDistrict) and rebuild listing/apply URLs from them; a URL for one tenant will not work for another.
Parse the fragment with an HTML parser exactly like page 1 — it contains the next .job-summary cards. Do not call resp.json().
Use the token exactly as it appears in the /JobDescription/{token} href for both the detail and ApplyNow URLs; keep Ref No only as display metadata and never decode or substitute the token.
Require the JobPosting block with a title and a substantive (80+ character) description before accepting a detail; skip and re-queue pages that fail this check.
- 1Use a persistent requests.Session so the ASP.NET_SessionId cookie carries into NavigateJobList pagination.
- 2Send browser-like headers (Accept, en-GB Accept-Language, a Chrome User-Agent) — the boards expect a real browser.
- 3Read the setJobsCount(...) and navigateJobList(...) markers for the authoritative job count and page range, and stop paginating when a page yields no new tokens.
- 4Prefer the JobPosting JSON-LD on detail pages for clean title, description, dates, and location over scraping the visible markup.
- 5Keep requests polite: roughly 150ms between calls and no more than ~3 concurrent detail fetches.
- 6Register and test each new tenant host/route before crawling it — a Fusion fingerprint alone doesn't guarantee a stable board.
One endpoint. All Fusion recruitment platform jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=fusion recruitment platform" \
-H "X-Api-Key: YOUR_KEY" Access Fusion recruitment platform
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.