All platforms

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.

Get API access
HiBob
Live
<3haverage discovery time
1hrefresh interval
Companies using HiBob
IRENStuartEcologiSynpulse
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 HiBob.

Data fields
  • Full HTML Job Descriptions
  • Department & Employment Type
  • Workspace Type Codes
  • Site & Country Locations
  • Published Dates
  • Company Name & Branding
Use cases
  1. 01HRIS-Native Job Monitoring
  2. 02Mid-Market Talent Sourcing
  3. 03Hiring Signal Tracking
  4. 04Careers Page Validation
Trusted by
IRENStuartEcologiSynpulse
DIY GUIDE

How to scrape HiBob.

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

RESTbeginnerUndocumented; a tenant Referer header is mandatory. Back off on HTTP 403 or 429No auth

Resolve the tenant and set the Referer header

Every HiBob careers site lives at https://{tenant}.careers.hibob.com/, where the tenant is the first host label (e.g. 'iren', or a stable hex-suffixed 'hibob-fa0ad69d0cb34a'). The backend enforces a Referer check on that subdomain, so requests without a matching Referer return HTTP 401.

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)

Fetch every job ad in one request

A single GET on /api/job-ad returns the full listing for the tenant, including HTML descriptions — HiBob has no pagination. The jobs live under the jobAdDetails array.

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}")

Map fields and stitch the description sections

HiBob splits the job body into four HTML fields (description, responsibilities, requirements, benefits); concatenate them, giving the follow-up sections headings. Match workspace type on the stable workspaceTypeId enum ('on_site' | 'remote' | 'hybrid'), not the localized display label, and skip ads with an empty id.

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"),
    })

Resolve the company name (best-effort)

The human-readable company name comes from /api/career-site, which also requires the Referer header. Treat it as optional: if it fails or the field is missing, listings should still flow with a null company name.

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)

Assemble a resilient scraper

Combine the pieces and classify HTTP errors honestly: because the Referer is always sent, a 401 means the tenant does not exist (not an auth prompt), while 403 and 429 signal blocking or rate limiting. Dedupe on the job id to guard against repeated ads.

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
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.

Best practices
  1. 1Always send the tenant Referer header; without it every call returns 401
  2. 2Treat a 401 as a nonexistent tenant, not an auth prompt — never attempt to log in
  3. 3Pull everything from /api/job-ad in one request; there is no pagination
  4. 4Match workspace type on the workspaceTypeId enum, not the localized display label
  5. 5Stitch the four description sections and honor per-tenant sectionLabels overrides
  6. 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 access
cURL
curl "https://connect.jobo.world/api/jobs?sources=hibob" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

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.

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