HiBob Jobs API.
Pull an entire careers site's open roles from one tenant-scoped JSON endpoint that ships structured departments, workspace types, and section-split job descriptions — no API key required.
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 HiBob.
- Full HTML Job Descriptions
- Department & Employment Type
- Workspace Type Codes
- Site & Country Locations
- Published Dates
- Company Name & Branding
- 01HRIS-Native Job Monitoring
- 02Mid-Market Talent Sourcing
- 03Hiring Signal Tracking
- 04Careers Page Validation
How to scrape HiBob.
Step-by-step guide to extracting jobs from HiBob-powered career pages—endpoints, authentication, and working code.
import requests
def build_headers(tenant: str) -> dict:
"""HiBob's careers backend enforces a Referer/Origin check on the tenant
subdomain. Requests without a matching Referer return HTTP 401, so always
send it — this applies to both the job-ad and career-site endpoints."""
base = f"https://{tenant}.careers.hibob.com/"
return {
"Referer": base,
"Accept": "application/json",
}
# The tenant is the first host label of the careers URL, e.g.
# "iren" in https://iren.careers.hibob.com/ (keep any -hex suffix intact).
tenant = "iren"
headers = build_headers(tenant)import requests
def fetch_job_ads(tenant: str, headers: dict) -> list[dict]:
"""One call returns every open role for the tenant — there is no pagination."""
url = f"https://{tenant}.careers.hibob.com/api/job-ad"
resp = requests.get(url, headers=headers, timeout=15)
resp.raise_for_status()
return resp.json().get("jobAdDetails", []) or []
ads = fetch_job_ads(tenant, headers)
print(f"Found {len(ads)} jobs for {tenant}")def build_description(ad: dict) -> str:
"""Concatenate the four HTML sections HiBob splits the body into. Per-tenant
sectionLabels override the default headings when present."""
labels = ad.get("sectionLabels") or {}
sections = [
(None, ad.get("description")), # lead section: no heading
(labels.get("responsibilities") or "Responsibilities", ad.get("responsibilities")),
(labels.get("requirements") or "Requirements", ad.get("requirements")),
(labels.get("benefits") or "Benefits", ad.get("benefits")),
]
html = ""
for label, body in sections:
if not body:
continue
if label:
html += f"<h3>{label}</h3>"
html += body
return html
def build_location(ad: dict) -> str:
"""The 'site' field is free-form (e.g. 'Remote — EMEA'); don't split it.
Combine it with the structured 'country' field for display."""
site = (ad.get("site") or "").strip()
country = (ad.get("country") or "").strip()
if site and country:
return f"{site}, {country}"
return site or country
for ad in ads:
if not ad.get("id"):
continue # skip malformed entries
print({
"external_id": ad["id"],
"title": (ad.get("title") or "").strip(),
"url": f"https://{tenant}.careers.hibob.com/jobs/{ad['id']}",
"department": ad.get("department"),
"employment_type": ad.get("employmentType"),
"workspace_type": ad.get("workspaceTypeId"), # on_site | remote | hybrid
"location": build_location(ad),
"published_at": ad.get("publishedAt"),
})import requests
def fetch_company_name(tenant: str, headers: dict) -> str | None:
"""Best-effort branding lookup. Non-fatal: listings should not fail because
the branding endpoint hiccuped."""
url = f"https://{tenant}.careers.hibob.com/api/career-site"
try:
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
return (resp.json().get("companyName") or "").strip() or None
except requests.RequestException:
return None
company_name = fetch_company_name(tenant, headers)import requests
def scrape_hibob(tenant: str) -> list[dict]:
"""Full HiBob careers scraper: one listing call plus best-effort branding."""
headers = build_headers(tenant)
company_name = fetch_company_name(tenant, headers)
url = f"https://{tenant}.careers.hibob.com/api/job-ad"
try:
resp = requests.get(url, headers=headers, timeout=15)
resp.raise_for_status()
except requests.HTTPError as e:
status = e.response.status_code
if status == 401:
# We always send Referer, so a 401 means the tenant does not exist.
print(f"Tenant '{tenant}' not found on HiBob")
elif status in (403, 429):
print(f"Blocked or rate limited for '{tenant}' — back off and retry")
return []
ads = resp.json().get("jobAdDetails", []) or []
results, seen = [], set()
for ad in ads:
job_id = ad.get("id")
if not job_id or job_id in seen:
continue
seen.add(job_id)
results.append({
"external_id": job_id,
"title": (ad.get("title") or "").strip(),
"company": company_name,
"url": f"https://{tenant}.careers.hibob.com/jobs/{job_id}",
"department": ad.get("department"),
"employment_type": ad.get("employmentType"),
"workspace_type": ad.get("workspaceTypeId"),
"location": build_location(ad),
"description": build_description(ad),
"published_at": ad.get("publishedAt"),
})
return results
jobs = scrape_hibob("iren")
print(f"Scraped {len(jobs)} jobs")The careers backend enforces a Referer/Origin check on the tenant subdomain. Send Referer: https://{tenant}.careers.hibob.com/ (and Accept: application/json) on both /api/job-ad and /api/career-site or the server rejects the call.
Because the Referer is always sent, a 401 from production traffic means the tenant does not exist — classify it as not-found, not auth-required. Do not attempt to log in or attach credentials.
HiBob returns 403 when blocking and 429 when rate limiting. Back off, add a delay between tenants, and retry later rather than hammering the endpoint.
The workspaceType field is localized per tenant. Match on the stable workspaceTypeId enum key ('on_site', 'remote', 'hybrid') instead of the display string.
HiBob splits the body across four fields — description, responsibilities, requirements, and benefits. Concatenate all four, and honor per-tenant sectionLabels that override the section headings; any of the sections can be null.
siteId arrives as a JSON number (e.g. 2575378), not a string. The 'site' field is free-form text, so keep it whole for display and rely on the structured 'country' field for the country value.
- 1Always send the tenant Referer header; without it every call returns 401
- 2Treat a 401 as a nonexistent tenant, not an auth prompt — never attempt to log in
- 3Pull everything from /api/job-ad in one request; there is no pagination
- 4Match workspace type on the workspaceTypeId enum, not the localized display label
- 5Stitch the four description sections and honor per-tenant sectionLabels overrides
- 6Cache results and back off on 403/429 — careers boards refresh daily, not per request
One endpoint. All HiBob jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=hibob" \
-H "X-Api-Key: YOUR_KEY" Access HiBob
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.