All platforms

Hireful (SiteHub) Jobs API.

Pull UK vacancies with structured pay, benefits, and location data from Hireful-powered SiteHub career sites through one anonymous JSON collection API — no login or keys.

Get API access
Hireful (SiteHub)
Live
<3haverage discovery time
1hrefresh interval
Companies using Hireful (SiteHub)
Carter JonasSouth StaffordshireSt Elizabeth's
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 Hireful (SiteHub).

Data fields
  • Structured Pay & Benefits Text
  • Department & Role Type
  • Contract Type & Hours
  • Full Job Descriptions
  • Location, Region & Postcode
  • Apply URL & Closing Date
Use cases
  1. 01UK Recruitment Monitoring
  2. 02Job Board Aggregation
  3. 03Salary & Benefits Analysis
  4. 04Employer Vacancy Tracking
Trusted by
Carter JonasSouth StaffordshireSt Elizabeth's
DIY GUIDE

How to scrape Hireful (SiteHub).

Step-by-step guide to extracting jobs from Hireful (SiteHub)-powered career pages—endpoints, authentication, and working code.

HybridintermediateNo published limit; one request at a time with a ~50ms delayNo auth

Discover the SiteHub collection ID

Hireful career sites are SiteHub frontends. Fetch the board HTML once and read the URL-encoded data-collection-bind object named "Jobs" — its 24-hex id is the tenant collection identifier used by every subsequent API call.

Step 1: Discover the SiteHub collection ID
import html as html_lib
import json
import re
from urllib.parse import unquote

import requests

BOARD_URL = "https://jobs.carterjonas.co.uk/"
BINDING = re.compile(r'data-collection-bind\s*=\s*["\']([^"\']+)["\']', re.I)

def discover_collection_id(board_url: str) -> str:
    page = requests.get(board_url, timeout=15).text
    if "api.sitehub.io" not in page:
        raise ValueError("Not a supported Hireful SiteHub board")
    for raw in BINDING.findall(page):
        try:
            binding = json.loads(unquote(html_lib.unescape(raw)))
        except json.JSONDecodeError:
            continue  # a board has many bindings; skip non-JSON ones
        cid = binding.get("id", "")
        if binding.get("name", "").lower() == "jobs" and re.fullmatch(r"[a-f0-9]{24}", cid):
            return cid
    raise ValueError("No Jobs collection id found on board")

collection_id = discover_collection_id(BOARD_URL)
print(collection_id)

Fetch published jobs from the collection API

Query the anonymous SiteHub items endpoint for the collection. Keep filter[columns.web-published]=1 to skip drafts; the response carries an authoritative total plus every job's full columns object.

Step 2: Fetch published jobs from the collection API
PAGE_SIZE = 100

def fetch_page(collection_id: str, offset: int = 0) -> dict:
    url = f"https://api.sitehub.io/collection/{collection_id}/items"
    params = {
        "order": "updatedAt_DESC",
        "limit": PAGE_SIZE,
        "offset": offset,
        "distinct": "",
        "paginate": "true",
        "filter[columns.web-published]": "1",
    }
    resp = requests.get(url, params=params, timeout=15)
    resp.raise_for_status()
    return resp.json()

page = fetch_page(collection_id)
print(page["total"], "published jobs")

Normalize each item into a job record

Use columns.job-id as the stable external ID — never the /job/{slug} path, which is a mutable CMS slug. The apply URL is the absolute anchor inside columns.apply-url-new, and the description is the joined columns.content[] HTML plus columns.description.

Step 3: Normalize each item into a job record
from urllib.parse import urljoin

HREF = re.compile(r'href\s*=\s*["\']([^"\']+)["\']', re.I)

def parse_item(item: dict, board_url: str) -> dict:
    cols = item.get("columns") or {}
    m = HREF.search(cols.get("apply-url-new") or "")
    apply_url = html_lib.unescape(m.group(1)).strip() if m else None
    slug = (cols.get("slug") or "").strip()
    sections = [s["content"].strip() for s in (cols.get("content") or []) if (s.get("content") or "").strip()]
    if (cols.get("description") or "").strip():
        sections.append(cols["description"].strip())
    location = ", ".join(v for v in (cols.get("location"), cols.get("region"), cols.get("postcode")) if v)
    return {
        "external_id": (cols.get("job-id") or "").strip(),  # native Hireful key
        "title": (cols.get("title") or "").strip(),
        "listing_url": urljoin(board_url, f"job/{slug}"),
        "apply_url": apply_url,
        "location": location or None,
        "department": cols.get("department"),
        "salary_text": cols.get("salary"),
        "closes_at": cols.get("closing-date"),
        "description": "\n".join(sections),
    }

Page through the full board

Advance the offset by the number of rows returned and stop only when you reach the API's authoritative total. A short page before the total means the board changed mid-scrape — restart rather than emit a partial snapshot.

Step 4: Page through the full board
import time

def scrape_board(board_url: str) -> list[dict]:
    collection_id = discover_collection_id(board_url)
    jobs, offset = [], 0
    while True:
        page = fetch_page(collection_id, offset)
        rows = page.get("collection") or []
        total = page.get("total", 0)
        if not rows:
            break
        jobs.extend(parse_item(row, board_url) for row in rows)
        offset += len(rows)
        if offset >= total:  # authoritative total reached
            break
        time.sleep(0.05)  # be polite between requests
    return jobs

print(len(scrape_board(BOARD_URL)), "jobs scraped")
Common issues
highOnly specific tenant hosts are recognized as Hireful

The provider validates a fixed set of evidenced SiteHub career-site hosts. A generic api.sitehub.io request or the hireful.com marketing domain is not enough to classify a company — confirm the board host is one of the known Hireful sites before scraping.

highBoard HTML exposes no jobs collection ID

Bootstrap fails if the page has no api.sitehub.io reference and no data-collection-bind object named "Jobs". Verify the site is actually a SiteHub frontend; sites on a different CMS won't expose a collection ID and can't be scraped this way.

mediumSome postings have empty descriptions

A posting can put its advert in an attached DOCX, leaving columns.content and columns.description empty. Don't fabricate text or reuse tenant boilerplate — emit the job with an empty description and mark the snapshot incomplete so it can't authorize removals.

mediumUsing the URL slug as the job ID

The public /job/{slug} path contains a mutable SiteHub CMS slug, not the native key. Always persist columns.job-id as the external ID so records stay stable when a slug changes.

mediumPagination stops before the authoritative total

Requesting past the total or receiving an empty page before reaching it signals the board changed mid-scrape. Reconcile offset + received against the API's total and restart the run rather than emitting a partial snapshot.

Best practices
  1. 1Bootstrap once per board to get the collection ID, then read everything from api.sitehub.io
  2. 2Always keep filter[columns.web-published]=1 to skip unpublished drafts
  3. 3Use columns.job-id as the stable external ID; treat /job/{slug} as a display URL only
  4. 4Page with limit=100 and stop at the API's authoritative total, not on the first empty page
  5. 5Send one request at a time with a short (~50ms) delay; no auth, cookies, or headers are needed
  6. 6Mark postings with empty content as incomplete instead of inventing a description
Or skip the complexity

One endpoint. All Hireful (SiteHub) jobs. No scraping, no sessions, no maintenance.

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

Access Hireful (SiteHub)
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