- 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.
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.
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
- 01Enterprise Job Monitoring
- 02Global Talent Market Analysis
- 03Multi-Brand Career Site Aggregation
- 04Large-Scale Job Data Feeds
Trusted by
- GE Aerospace
- HelloFresh
- DHL
- Thermo Fisher Scientific
- Mars
- Microsoft
How to scrape Phenom (Phenompeople).
Step-by-step guide to extracting jobs from Phenom (Phenompeople)-powered career pages—endpoints, authentication, and working code.
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}")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})")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']}")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', ''))}")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)}")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")- 1Verify a site is actually Phenom (look for POST /widgets or cdn.phenompeople.com) before trusting the host shape
- 2Send free-text filters in the top-level keywords field, not under selected_fields
- 3Paginate with the from offset and stop when from + hits reaches totalHits
- 4Throttle to ~1 request / 2s single-threaded and keep detail fetches at ~3 concurrent to avoid 403/429
- 5Treat an empty jobs array as 'no results' only when totalHits is 0; otherwise retry
- 6Cache results and refresh daily — enterprise boards update in batches, not continuously
One endpoint. All Phenom (Phenompeople) jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=phenom (phenompeople)" \
-H "X-Api-Key: YOUR_KEY"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.
Access Phenom (Phenompeople)
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.