All platforms

Phenom (Phenompeople) Jobs API.

Pull structured job listings from enterprise career sites built on Phenom's talent-experience platform, straight from the JSON /widgets endpoint instead of parsing HTML.

Get API access
Phenom (Phenompeople)
Live
120K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Phenom (Phenompeople)
GE AerospaceHelloFreshDHLThermo Fisher ScientificMars
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 Phenom (Phenompeople).

Data fields
  • Full Job Titles & Req IDs
  • Structured Location Data
  • Category & Department
  • Employment Type & Posted Date
  • Description Teasers
  • ML-Extracted Skill Tags
Use cases
  1. 01Enterprise Job Monitoring
  2. 02Global Talent Market Analysis
  3. 03Multi-Brand Career Site Aggregation
  4. 04Large-Scale Job Data Feeds
Trusted by
GE AerospaceHelloFreshDHLThermo Fisher ScientificMarsMicrosoft
DIY GUIDE

How to scrape Phenom (Phenompeople).

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

HybridintermediateNo published limit; safe cadence is ~1 request / 2s single-threaded (bursts return 403/429; some tenants sit behind Cloudflare)No auth

Discover the company refNum

Each Phenom career site carries a company-specific refNum embedded in the page's inline JSON. Extract it once and reuse it for every API call. On single-tenant career domains an empty refNum also works, because the domain itself scopes the tenant.

Step 1: Discover the company refNum
import requests
from bs4 import BeautifulSoup

def get_refnum(domain: str) -> str | None:
    url = f"https://{domain}/global/en/search-results"
    response = requests.get(url, timeout=10)
    soup = BeautifulSoup(response.text, "html.parser")

    # Look for refNum in script tags or data attributes
    for script in soup.find_all("script"):
        if script.string and "refNum" in script.string:
            # Extract refNum using string parsing
            import re
            match = re.search(r'"refNum":"([^"]+)"', script.string)
            if match:
                return match.group(1)
    return None

refnum = get_refnum("careers.geaerospace.com")
print(f"Found refNum: {refnum}")

Fetch job listings from the widgets API

POST to the /widgets endpoint with ddoKey 'refineSearch' to retrieve paginated listings. Each job carries a teaser description plus location, category and posted-date metadata.

Step 2: Fetch job listings from the widgets API
import requests

def fetch_jobs(domain: str, refnum: str, page: int = 0, size: int = 20) -> dict:
    url = f"https://{domain}/widgets"
    payload = {
        "lang": "en_global",
        "deviceType": "desktop",
        "country": "global",
        "pageName": "search-results",
        "size": size,
        "from": page * size,
        "jobs": True,
        "counts": True,
        "all_fields": ["category", "country", "city", "type"],
        "clearAll": False,
        "jdsource": "facets",
        "isSliderEnable": False,
        "pageId": "page20",
        "siteType": "external",
        "keywords": "",
        "global": True,
        "selected_fields": {},
        "sort": {"order": "desc", "field": "postedDate"},
        "locationData": {},
        "refNum": refnum,
        "ddoKey": "refineSearch"
    }

    response = requests.post(
        url,
        json=payload,
        headers={"Content-Type": "application/json"},
        timeout=15
    )
    return response.json()

data = fetch_jobs("careers.geaerospace.com", "GAOGAYGLOBAL", page=0)
jobs = data.get("refineSearch", {}).get("data", {}).get("jobs", [])
total = data.get("refineSearch", {}).get("totalHits", 0)
print(f"Found {len(jobs)} jobs (total: {total})")

Parse job listings data

Read the fields you need from each job object — title, location, category, and the jobSeqNo used to build detail-page URLs. Prefer jobSeqNo as the stable external ID, falling back to jobId.

Step 3: Parse job listings data
for job in jobs:
    parsed = {
        "job_id": job.get("jobId"),
        "req_id": job.get("reqId"),
        "title": job.get("title"),
        "location": job.get("location"),
        "category": job.get("category"),
        "type": job.get("type"),
        "posted_date": job.get("postedDate"),
        "apply_url": job.get("applyUrl"),
        "teaser": job.get("descriptionTeaser", "")[:200],
        "job_seq_no": job.get("jobSeqNo"),
        "is_multi_location": job.get("isMultiLocation", False),
    }
    print(f"{parsed['title']} - {parsed['location']}")

Fetch full job descriptions from HTML

The listings API returns only a descriptionTeaser. For the full description, request the server-side-rendered job detail page, using the jobSeqNo to build the URL, and parse it with defensive selectors.

Step 4: Fetch full job descriptions from HTML
import requests
from bs4 import BeautifulSoup
from urllib.parse import quote

def fetch_job_details(domain: str, job_seq_no: str, title: str) -> dict:
    title_slug = quote(title.lower().replace(" ", "-"))
    url = f"https://{domain}/global/en/job/{job_seq_no}/{title_slug}"

    response = requests.get(url, timeout=10)
    soup = BeautifulSoup(response.text, "html.parser")

    # Extract job details from HTML
    title_elem = soup.select_one("h1.job-title, h1")
    location_elem = soup.select_one(".job-location, [class*='location']")
    desc_elem = soup.select_one(".job-description, [class*='description']")
    apply_elem = soup.select_one("a[href*='apply']")

    return {
        "title": title_elem.get_text(strip=True) if title_elem else None,
        "location": location_elem.get_text(strip=True) if location_elem else None,
        "description": desc_elem.get_text(strip=True) if desc_elem else None,
        "apply_url": apply_elem.get("href") if apply_elem else None,
        "url": url,
    }

# Fetch details for first job
if jobs:
    first_job = jobs[0]
    details = fetch_job_details(
        "careers.geaerospace.com",
        first_job["jobSeqNo"],
        first_job["title"]
    )
    print(f"Full description length: {len(details.get('description', ''))}")

Handle pagination and rate limiting

Page through all jobs with the 'from' offset until you reach totalHits, and space requests out to stay under Phenom's block thresholds (a single-threaded ~1 request / 2s cadence is safe).

Step 5: Handle pagination and rate limiting
import time
import requests

def fetch_all_jobs(domain: str, refnum: str, delay: float = 0.5) -> list:
    all_jobs = []
    page = 0
    size = 20

    while True:
        data = fetch_jobs(domain, refnum, page=page, size=size)
        result = data.get("refineSearch", {})
        jobs = result.get("data", {}).get("jobs", [])
        total = result.get("totalHits", 0)

        if not jobs:
            break

        all_jobs.extend(jobs)
        print(f"Page {page}: fetched {len(jobs)} jobs (total: {len(all_jobs)}/{total})")

        if len(all_jobs) >= total:
            break

        page += 1
        time.sleep(delay)  # Rate limiting

    return all_jobs

all_jobs = fetch_all_jobs("careers.geaerospace.com", "GAOGAYGLOBAL")
print(f"Total jobs collected: {len(all_jobs)}")

Alternative: Discover jobs via sitemap

As a fallback to the API, walk the sitemap index to enumerate every job URL. Each child sitemap holds up to 500 job URLs with last-modified dates — useful when the widgets endpoint is blocked.

Step 6: Alternative: Discover jobs via sitemap
import requests
import xml.etree.ElementTree as ET

def discover_jobs_from_sitemap(domain: str) -> list:
    sitemap_url = f"https://{domain}/global/en/sitemap_index.xml"
    response = requests.get(sitemap_url, timeout=10)

    root = ET.fromstring(response.content)
    namespace = {"sm": "http://www.sitemaps.org/schemas/sitemap/0.9"}

    job_urls = []
    # Parse sitemap index for individual sitemaps
    for sitemap in root.findall(".//sm:loc", namespace):
        sitemap_response = requests.get(sitemap.text, timeout=10)
        sitemap_root = ET.fromstring(sitemap_response.content)

        # Extract job URLs from each sitemap
        for url in sitemap_root.findall(".//sm:loc", namespace):
            if "/job/" in url.text:
                job_urls.append(url.text)

    return job_urls

job_urls = discover_jobs_from_sitemap("careers.geaerospace.com")
print(f"Discovered {len(job_urls)} job URLs from sitemaps")
Common issues
mediumHostname alone doesn't prove a site is Phenom

Phenom powers white-label domains (careers.{brand}.com, jobs.{brand}.com) that look identical to unrelated career sites. Confirm Phenom before scraping by checking the page source for the POST /widgets endpoint or cdn.phenompeople.com widget scripts.

mediumFree-text keywords in the wrong field return zero jobs

Send search terms in the top-level 'keywords' field. Placing a searchParameter under selected_fields is accepted with HTTP 200 but silently returns no jobs.

highMissing or wrong refNum yields empty results on multi-brand deployments

Extract the refNum from the page's inline JSON and reuse it. On single-tenant career domains you can leave refNum empty because the domain scopes the tenant, but shared multi-brand deployments need the exact code.

highAPI only returns descriptionTeaser, not full descriptions

Use a hybrid flow: take job IDs from the listings API, then either scrape the job detail HTML page for the full body or re-query /widgets with the jobId as a keyword to pull the matching record.

mediumAn empty jobs array can be a transient failure, not 'no jobs'

Treat an empty page as a real zero only when refineSearch.totalHits is 0. Otherwise retry — an empty jobs list with a non-zero total usually signals a transient or parse error.

mediumAggressive request rates get blocked (403/429) or hit Cloudflare

Throttle to roughly one request every 2 seconds single-threaded and cap detail fetches at ~3 concurrent. Some tenants add Cloudflare, which may require session cookies or browser automation for initial access.

Best practices
  1. 1Verify a site is actually Phenom (look for POST /widgets or cdn.phenompeople.com) before trusting the host shape
  2. 2Send free-text filters in the top-level keywords field, not under selected_fields
  3. 3Paginate with the from offset and stop when from + hits reaches totalHits
  4. 4Throttle to ~1 request / 2s single-threaded and keep detail fetches at ~3 concurrent to avoid 403/429
  5. 5Treat an empty jobs array as 'no results' only when totalHits is 0; otherwise retry
  6. 6Cache results and refresh daily — enterprise boards update in batches, not continuously
Or skip the complexity

One endpoint. All Phenom (Phenompeople) jobs. No scraping, no sessions, no maintenance.

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

Access Phenom (Phenompeople)
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