- criticalEvery request returns HTTP 401 without a Referer header
- 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.
- highA 401 is mistaken for an auth prompt
- 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.
- mediumRequests get 403 or 429 responses
- 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.
- mediumWorkspace type appears in a non-English display value
- The workspaceType field is localized per tenant. Match on the stable workspaceTypeId enum key ('on_site', 'remote', 'hybrid') instead of the display string.
- mediumJob description looks truncated
- 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.
- lowsiteId parsed as the wrong type or location over-split
- 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.
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.
What's in every response.
Data fields, real-world applications, and the companies already running on HiBob.
Data fields
- Full HTML Job Descriptions
- Department & Employment Type
- Workspace Type Codes
- Site & Country Locations
- Published Dates
- Company Name & Branding
Use cases
- 01HRIS-Native Job Monitoring
- 02Mid-Market Talent Sourcing
- 03Hiring Signal Tracking
- 04Careers Page Validation
Trusted by
- IREN
- Stuart
- Ecologi
- Synpulse
DIY GUIDE
How to scrape HiBob.
Step-by-step guide to extracting jobs from HiBob-powered career pages—endpoints, authentication, and working code.
Step 1: Resolve the tenant and set the Referer header
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)Step 2: Fetch every job ad in one request
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}")Step 3: Map fields and stitch the description sections
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"),
})Step 4: Resolve the company name (best-effort)
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)Step 5: Assemble a resilient scraper
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") Common issues
Best practices
- 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
Or skip the complexity
One endpoint. All HiBob jobs. No scraping, no sessions, no maintenance.
Get API accesscURL
curl "https://connect.jobo.world/api/jobs?sources=hibob" \
-H "X-Api-Key: YOUR_KEY"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.
Ready to integrate Access HiBob
Access HiBob
job data today.
One API call. Structured data. No scraping infrastructure to build or maintain — start with the $5 free starting balance and scale as you grow.
99.9%API uptime
<200msAvg response
50M+Jobs processed