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.
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).
- Full Job Titles & Req IDs
- Structured Location Data
- Category & Department
- Employment Type & Posted Date
- Description Teasers
- ML-Extracted Skill Tags
- 01Enterprise Job Monitoring
- 02Global Talent Market Analysis
- 03Multi-Brand Career Site Aggregation
- 04Large-Scale Job Data Feeds
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")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.
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.
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.
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.
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.
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.
- 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" 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.