All platforms

UltiPro (UKG) Jobs API.

UKG's enterprise HR and talent management platform with hybrid API/HTML job board architecture.

UltiPro (UKG)
Live
100K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using UltiPro (UKG)
United Natural Foods (UNFI)Macmillan PublishersTechnoServeAmerican Chemical SocietyRegal Theatres
Developer tools

Try the API.

Test Jobs, Feed, and Auto-Apply 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 UltiPro (UKG).

Data fields
  • HCM integration
  • Payroll
  • Talent management
  • Analytics
  • Compliance
  • Multi-location support
Use cases
  1. 01Enterprise job aggregation
  2. 02Healthcare recruiting
  3. 03Manufacturing talent sourcing
  4. 04Multi-company job discovery
Trusted by
United Natural Foods (UNFI)Macmillan PublishersTechnoServeAmerican Chemical SocietyRegal TheatresCNL Financial GroupHope Network
DIY GUIDE

How to scrape UltiPro (UKG).

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

HybridintermediateUnknown - implement delays between requestsNo auth

Extract URL components

UltiPro job boards have specific URL patterns with company codes and board IDs. Extract these components from your target company's career page URL.

Step 1: Extract URL components
import re

# UltiPro URL patterns:
# https://recruiting.ultipro.com/{COMPANY_CODE}/JobBoard/{BOARD_ID}/
# https://recruiting2.ultipro.com/{COMPANY_CODE}/JobBoard/{BOARD_ID}/

company_url = "https://recruiting.ultipro.com/HOP1003HOPN/JobBoard/b3a1c5d7-6c5e-46f6-8478-cd649884f0ef"

# Extract company code
company_code_match = re.search(r'recruiting\d*\.ultipro\.(?:com|ca)/([^/]+)', company_url)
company_code = company_code_match.group(1) if company_code_match else None

# Extract board ID
board_id_match = re.search(r'JobBoard/([^/]+)', company_url)
board_id = board_id_match.group(1) if board_id_match else None

print(f"Company Code: {company_code}")  # HOP1003HOPN
print(f"Board ID: {board_id}")  # UUID

Fetch job listings via API

Use the LoadSearchResults POST endpoint to retrieve job metadata. The API returns brief descriptions (100-200 characters) but not full job descriptions.

Step 2: Fetch job listings via API
import requests

api_url = f"https://recruiting.ultipro.com/{company_code}/JobBoard/{board_id}/JobBoardView/LoadSearchResults"

headers = {
    "Content-Type": "application/json; charset=UTF-8",
    "X-Requested-With": "XMLHttpRequest",
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
}

payload = {
    "opportunitySearch": {
        "Top": 50,
        "Skip": 0,
        "QueryString": "",
        "OrderBy": [{
            "Value": "postedDateDesc",
            "PropertyName": "PostedDate",
            "Ascending": False
        }],
        "Filters": [
            {"t": "TermsSearchFilterDto", "fieldName": 4, "extra": None, "values": []},
            {"t": "TermsSearchFilterDto", "fieldName": 5, "extra": None, "values": []},
            {"t": "TermsSearchFilterDto", "fieldName": 6, "extra": None, "values": []},
            {"t": "TermsSearchFilterDto", "fieldName": 37, "extra": None, "values": []}
        ]
    },
    "matchCriteria": {
        "PreferredJobs": [],
        "Educations": [],
        "LicenseAndCertifications": [],
        "Skills": [],
        "hasNoLicenses": False,
        "SkippedSkills": []
    }
}

response = requests.post(api_url, json=payload, headers=headers)
data = response.json()
print(f"Found {data.get('totalCount', 0)} total jobs")

Parse the API response

Extract job metadata from the response. Note that only brief descriptions are included - full descriptions require fetching individual job detail pages.

Step 3: Parse the API response
jobs = []
for job in data.get("opportunities", []):
    location_info = None
    if job.get("Locations") and len(job["Locations"]) > 0:
        addr = job["Locations"][0].get("Address", {})
        location_info = {
            "city": addr.get("City"),
            "state": addr.get("State", {}).get("Code"),
            "postal_code": addr.get("PostalCode"),
            "country": addr.get("Country", {}).get("Code"),
        }

    # JobLocationType: 1=On-site, 2=Hybrid, 3=Remote
    location_type_map = {1: "On-site", 2: "Hybrid", 3: "Remote"}

    jobs.append({
        "id": job.get("Id"),
        "title": job.get("Title"),
        "requisition_number": job.get("RequisitionNumber"),
        "full_time": job.get("FullTime"),
        "job_category": job.get("JobCategoryName"),
        "location": location_info,
        "posted_date": job.get("PostedDate"),
        "brief_description": job.get("BriefDescription", "")[:200],
        "location_type": location_type_map.get(job.get("JobLocationType"), "Unknown"),
    })

print(f"Parsed {len(jobs)} jobs")

Handle pagination

UltiPro uses Skip/Top pagination. Continue fetching until all jobs are retrieved based on the totalCount field.

Step 4: Handle pagination
import time

all_jobs = []
skip = 0
page_size = 50

while True:
    payload["opportunitySearch"]["Skip"] = skip
    payload["opportunitySearch"]["Top"] = page_size

    response = requests.post(api_url, json=payload, headers=headers)
    data = response.json()

    opportunities = data.get("opportunities", [])
    if not opportunities:
        break

    all_jobs.extend(opportunities)
    total_count = data.get("totalCount", 0)

    print(f"Fetched {len(opportunities)} jobs (total: {len(all_jobs)}/{total_count})")

    if len(all_jobs) >= total_count:
        break

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

print(f"Retrieved {len(all_jobs)} total jobs")

Fetch full job details from HTML

For complete job descriptions, fetch the server-side rendered OpportunityDetail page and parse the HTML content.

Step 5: Fetch full job details from HTML
from bs4 import BeautifulSoup

def fetch_job_details(company_code: str, board_id: str, job_id: str) -> dict:
    detail_url = (
        f"https://recruiting.ultipro.com/{company_code}/JobBoard/{board_id}"
        f"/OpportunityDetail?opportunityId={job_id}"
    )

    response = requests.get(detail_url)
    soup = BeautifulSoup(response.text, "html.parser")

    # Find the full description - selectors may vary by company
    description_selectors = [
        ".job-description",
        ".description",
        ".opportunity-description",
        "#job-description",
        "[class*='description']",
    ]

    full_description = None
    for selector in description_selectors:
        element = soup.select_one(selector)
        if element:
            full_description = element.get_text(strip=True)
            break

    # Extract requirements if available
    requirements = None
    req_element = soup.select_one(".requirements, .qualifications")
    if req_element:
        requirements = req_element.get_text(strip=True)

    return {
        "full_description": full_description,
        "requirements": requirements,
        "html_length": len(response.text),
    }

# Example usage
if all_jobs:
    job_id = all_jobs[0].get("Id")
    details = fetch_job_details(company_code, board_id, job_id)
    print(f"Description length: {len(details.get('full_description', ''))}")

Detect the correct subdomain

UltiPro uses multiple subdomains (recruiting.ultipro.com, recruiting2.ultipro.com, recruiting.ultipro.ca). Always detect and use the correct subdomain from the original company URL.

Step 6: Detect the correct subdomain
import re

def parse_ultipro_url(url: str) -> dict:
    """Extract all components from an UltiPro job board URL."""
    patterns = {
        "base_url": r'(https://recruiting\d*\.ultipro\.(?:com|ca))',
        "company_code": r'recruiting\d*\.ultipro\.(?:com|ca)/([^/]+)',
        "board_id": r'JobBoard/([^/]+)',
    }

    result = {"original_url": url}

    # Extract base URL (includes subdomain)
    base_match = re.search(patterns["base_url"], url)
    result["base_url"] = base_match.group(1) if base_match else None

    # Extract company code
    code_match = re.search(patterns["company_code"], url)
    result["company_code"] = code_match.group(1) if code_match else None

    # Extract board ID
    board_match = re.search(patterns["board_id"], url)
    result["board_id"] = board_match.group(1) if board_match else None

    return result

# Test with different subdomains
urls = [
    "https://recruiting.ultipro.com/HOP1003HOPN/JobBoard/b3a1c5d7-6c5e-46f6-8478-cd649884f0ef",
    "https://recruiting2.ultipro.com/HEN1009HPCC/JobBoard/abc123",
    "https://recruiting.ultipro.ca/BFL5000BFLCA/JobBoard/xyz789",
]

for url in urls:
    parsed = parse_ultipro_url(url)
    print(f"Base: {parsed['base_url']}, Code: {parsed['company_code']}")
Common issues
criticalBoard ID is required and cannot be guessed

Each UltiPro job board has a unique UUID board ID. This must be extracted from the company's careers page URL. Store board IDs for each company you scrape.

highAPI returns only brief descriptions (100-200 chars)

The LoadSearchResults API returns truncated BriefDescription fields. For full job descriptions, you must fetch individual OpportunityDetail HTML pages. Plan for extra HTTP requests.

mediumMultiple subdomains exist

UltiPro uses recruiting.ultipro.com, recruiting2.ultipro.com, and recruiting.ultipro.ca. Detect the correct subdomain from the company's actual URL to avoid 404 errors.

mediumAnti-forgery token may be required

Some UltiPro instances require X-RequestVerificationToken header. If you receive 403 errors, extract this token from cookies or meta tags on the initial page load.

lowEmpty job boards return totalCount: 0

Some companies may have zero active postings. Handle the empty response gracefully and check totalCount before attempting pagination.

mediumRate limiting behavior is unknown

Implement delays between requests (1-2 seconds) and use exponential backoff for error responses. The API does not document official rate limits.

Best practices
  1. 1Extract company code and board ID from the actual company URL before making API calls
  2. 2Use the LoadSearchResults API for efficient bulk job discovery
  3. 3Add 1-2 second delays between requests to avoid potential rate limiting
  4. 4Fetch full job descriptions from HTML only when needed (not for all jobs)
  5. 5Handle multiple subdomains (recruiting, recruiting2, recruiting.ultipro.ca)
  6. 6Store extracted board IDs to avoid re-discovery on subsequent scrapes
Or skip the complexity

One endpoint. All UltiPro (UKG) jobs. No scraping, no sessions, no maintenance.

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

Access UltiPro (UKG)
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