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.
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).
- Structured Pay & Benefits Text
- Department & Role Type
- Contract Type & Hours
- Full Job Descriptions
- Location, Region & Postcode
- Apply URL & Closing Date
- 01UK Recruitment Monitoring
- 02Job Board Aggregation
- 03Salary & Benefits Analysis
- 04Employer Vacancy Tracking
How to scrape Hireful (SiteHub).
Step-by-step guide to extracting jobs from Hireful (SiteHub)-powered career pages—endpoints, authentication, and working code.
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)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")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),
}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")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.
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.
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.
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.
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.
- 1Bootstrap once per board to get the collection ID, then read everything from api.sitehub.io
- 2Always keep filter[columns.web-published]=1 to skip unpublished drafts
- 3Use columns.job-id as the stable external ID; treat /job/{slug} as a display URL only
- 4Page with limit=100 and stop at the API's authoritative total, not on the first empty page
- 5Send one request at a time with a short (~50ms) delay; no auth, cookies, or headers are needed
- 6Mark postings with empty content as incomplete instead of inventing a description
One endpoint. All Hireful (SiteHub) jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=hireful (sitehub)" \
-H "X-Api-Key: YOUR_KEY" 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.