All platforms

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.

Get API access
Fusion recruitment platform
Live
<3haverage discovery time
1hrefresh interval
Companies using Fusion recruitment platform
Bunzl Cleaning & Hygiene SuppliesYusen LogisticsPeak District National Park Authority
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.

What's in every response.

Data fields, real-world applications, and the companies already running on Fusion recruitment platform.

Data fields
  • Full Job Descriptions
  • Salary Text
  • Employment Type
  • Job Sector & Industry
  • Location Details
  • Posted & Closing Dates
Use cases
  1. 01UK Job Market Tracking
  2. 02Logistics & Supply-Chain Hiring Signals
  3. 03Public-Sector Vacancy Monitoring
  4. 04Recruitment Market Research
Trusted by
Bunzl Cleaning & Hygiene SuppliesYusen LogisticsPeak District National Park Authority
DIY GUIDE

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.

HTMLadvancedNo published limit; ~150ms between requests and max ~3 concurrent detail fetches recommendedNo auth

Load the board's first page

Fusion has no shared API host — every employer runs on its own host and URL route (e.g. jobs.bunzlchs.com/Bunzl). GET the board page inside a persistent session so the ASP.NET_SessionId cookie some deployments set is captured for later pagination.

Step 1: Load the board's first page
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.text

Parse the .job-summary cards

Each vacancy is a .job-summary block: the title/link sits in an h3 anchor pointing at /JobDescription/{token}, and a table of label/value rows carries the reference, sector, salary, hours, location, and expiry.

Step 2: Parse the .job-summary cards
from 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")

Page through NavigateJobList

Extra pages come from a POST to /{route}/NavigateJobList with the same session. It returns an HTML fragment (not JSON) containing the next .job-summary cards, so parse it the same way and stop when a page adds no new tokens.

Step 3: Page through NavigateJobList
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)

Read JobPosting JSON-LD on detail pages

Each /JobDescription/{token} page is server HTML embedding a schema.org JobPosting object. Prefer it for a clean title, description, dates, industry, and location; treat 404/410 as a removed job.

Step 4: Read JobPosting JSON-LD on detail pages
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]]
Common issues
highPagination fails or returns an empty fragment on session-bound deployments (e.g. Bunzl).

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.

highThere is no single global Fusion API host — each employer runs on its own host and route.

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.

mediumThe NavigateJobList endpoint responds with HTML, not JSON.

Parse the fragment with an HTML parser exactly like page 1 — it contains the next .job-summary cards. Do not call resp.json().

mediumThe job token is opaque and is not the human-readable Ref No.

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.

mediumA detail page occasionally omits the JobPosting JSON-LD or carries a very thin description.

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.

Best practices
  1. 1Use a persistent requests.Session so the ASP.NET_SessionId cookie carries into NavigateJobList pagination.
  2. 2Send browser-like headers (Accept, en-GB Accept-Language, a Chrome User-Agent) — the boards expect a real browser.
  3. 3Read the setJobsCount(...) and navigateJobList(...) markers for the authoritative job count and page range, and stop paginating when a page yields no new tokens.
  4. 4Prefer the JobPosting JSON-LD on detail pages for clean title, description, dates, and location over scraping the visible markup.
  5. 5Keep requests polite: roughly 150ms between calls and no more than ~3 concurrent detail fetches.
  6. 6Register and test each new tenant host/route before crawling it — a Fusion fingerprint alone doesn't guarantee a stable board.
Or skip the complexity

One endpoint. All Fusion recruitment platform jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=fusion recruitment platform" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

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.

99.9%API uptime
<200msAvg response
50M+Jobs processed