All platforms

Kronos Workforce Ready Jobs API.

Pull structured job listings from legacy UKG Workforce Ready boards on mykronos.com, where an anonymous REST endpoint returns pay ranges, categories, and full descriptions with no login or browser automation.

Get API access
Kronos Workforce Ready
Live
<3haverage discovery time
1hrefresh interval
Companies using Kronos Workforce Ready
Arnot HealthHarris CenterArc Baltimore
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 Kronos Workforce Ready.

Data fields
  • Structured Pay Ranges
  • Job Categories & Industries
  • Employment Type & Degree
  • Full Job Descriptions
  • Map Coordinates
  • Remote & Quick-Apply Flags
Use cases
  1. 01Job Board Aggregation
  2. 02Salary Benchmarking
  3. 03Local & Regional Hiring Trends
  4. 04Multi-Tenant Board Monitoring
Trusted by
Arnot HealthHarris CenterArc Baltimore
DIY GUIDE

How to scrape Kronos Workforce Ready.

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

RESTintermediate~100ms between requests; 3 concurrent detail fetches (recommended)No auth

Resolve the pod host and numeric tenant

Kronos Workforce Ready boards live on multi-label pod hosts like prd01-hcm01.prd.mykronos.com, with the tenant as a numeric ID inside /ta/{tenant}.careers. Reject the www marketing host and sibling saashr.com hosts, which do not serve this board.

Step 1: Resolve the pod host and numeric tenant
import re

board_url = "https://prd01-hcm01.prd.mykronos.com/ta/6012355.careers"

# Host must be a multi-label pod under mykronos.com (>= 2 labels before it),
# and the tenant is a 5-12 digit numeric ID.
m = re.match(
    r"^https://((?:[a-z0-9-]+\.){2,}mykronos\.com)/ta/(\d{5,12})\.careers$",
    board_url,
)
if not m:
    raise ValueError("Not a valid Kronos Workforce Ready board URL")

origin = "https://" + m.group(1)
tenant = m.group(2)

Fetch a page of job requisitions

Call the anonymous job-requisitions endpoint. The tenant segment is prefixed with a URL-encoded pipe (%7C), and the AJAX headers below match what the board sends. Note that the misleadingly named offset parameter is a one-based page number, not a row offset.

Step 2: Fetch a page of job requisitions
import requests

PAGE_SIZE = 100
HEADERS = {
    "Accept": "application/json, text/javascript, */*; q=0.01",
    "Referer": f"{origin}/",
    "X-Requested-With": "XMLHttpRequest",
}


def fetch_page(page: int) -> dict:
    # %7C is the encoded pipe before the tenant; keep it verbatim.
    url = f"{origin}/ta/rest/ui/recruitment/companies/%7C{tenant}/job-requisitions"
    params = {
        "offset": page,   # one-based PAGE number, not a row offset
        "size": PAGE_SIZE,
        "sort": "desc",
        "ein_id": "",
        "lang": "en-US",
    }
    resp = requests.get(url, params=params, headers=HEADERS, timeout=15)
    resp.raise_for_status()
    return resp.json()


first = fetch_page(1)
total = first["_paging"]["total"]
print(f"Tenant {tenant} has {total} open jobs")

Walk pagination via _paging.total

The response carries an authoritative _paging.total. Compute the page count as ceil(total / 100) and request pages 1..N, pacing requests to stay polite. Stop once the total is consumed rather than probing for an empty page.

Step 3: Walk pagination via _paging.total
import math
import time


def fetch_all_jobs() -> list[dict]:
    first = fetch_page(1)
    total = first["_paging"]["total"]
    jobs = list(first["job_requisitions"])

    total_pages = math.ceil(total / PAGE_SIZE)
    for page in range(2, total_pages + 1):
        payload = fetch_page(page)
        jobs.extend(payload["job_requisitions"])
        time.sleep(0.1)  # ~100ms between requests

    return jobs

Map fields and build the public apply URL

Each row carries the native numeric id, title, location, and pay range. The public job URL is the board URL with a ShowJob query param, which doubles as the apply URL.

Step 4: Map fields and build the public apply URL
def parse_job(job: dict) -> dict:
    job_id = str(job["id"])
    loc = job.get("location") or {}
    return {
        "external_id": job_id,
        "title": (job.get("job_title") or "").strip(),
        "apply_url": f"{origin}/ta/{tenant}.careers?ShowJob={job_id}&lang=en-US",
        "city": loc.get("city"),
        "state": loc.get("state"),
        "country": loc.get("country"),
        "salary_min": job.get("base_pay_from"),
        "salary_max": job.get("base_pay_to"),
        "salary_period": job.get("base_pay_frequency"),
        "categories": job.get("job_categories"),
        "is_remote": job.get("is_remote_job"),
    }


rows = [parse_job(j) for j in fetch_all_jobs()]

Fetch full detail JSON per job

The listing rows omit the full body, so request the per-job detail endpoint with showMap=1. Assemble the description from job_preview, job_description, and job_requirement. Treat a 404 or 410 as a removed job rather than a hard error.

Step 5: Fetch full detail JSON per job
def fetch_detail(job_id: str) -> dict | None:
    url = (
        f"{origin}/ta/rest/ui/recruitment/companies/%7C{tenant}"
        f"/job-requisitions/{job_id}"
    )
    resp = requests.get(
        url,
        params={"showMap": 1, "lang": "en-US"},
        headers=HEADERS,
        timeout=15,
    )
    if resp.status_code in (404, 410):
        return None  # job was removed from the board
    resp.raise_for_status()
    job = resp.json()

    parts = [job.get("job_preview"), job.get("job_description"), job.get("job_requirement")]
    job["full_description"] = "\n\n".join(p for p in parts if p)
    return job
Common issues
highRequests to www.mykronos.com or a saashr.com host return no board

Only multi-label pod hosts (e.g. prd01-hcm01.prd.mykronos.com) serve the anonymous REST board. The www host is marketing, and the sibling SaaShr product lives on secure*.saashr.com and must be scraped as a separate provider.

highPagination skips or refetches rows when offset is treated as a row index

The offset parameter is a one-based page number, not a row offset. Start at offset=1 and iterate pages 1..ceil(total/100); never send offset=0 or offset=100.

mediumDropping or double-encoding the %7C pipe breaks the path

The tenant segment is prefixed with a URL-encoded pipe (companies/%7C{tenant}). Keep it verbatim and prevent your HTTP client from re-encoding the % into %25.

mediumAnonymous access rejected with 401 or 403

Some tenants gate the board. Always send the Accept, Referer, and X-Requested-With: XMLHttpRequest headers, and treat a persistent 401/403 as an auth-gated tenant rather than retrying.

medium429 responses under heavy crawling

Pace requests to roughly one every 100ms and cap concurrent detail fetches at about 3 to avoid rate limiting across a tenant's pages.

Best practices
  1. 1Restrict scraping to multi-label mykronos.com pod hosts; skip www and saashr.com siblings
  2. 2Treat the API's offset as a one-based page number, not a row offset
  3. 3Page until _paging.total is fully consumed (ceil(total / 100) pages)
  4. 4Send Accept, Referer, and X-Requested-With: XMLHttpRequest on every request
  5. 5Pace requests to ~100ms and cap concurrent detail fetches at 3
  6. 6Treat detail 404/410 as job removal and 401/403 as an auth-gated tenant
Or skip the complexity

One endpoint. All Kronos Workforce Ready jobs. No scraping, no sessions, no maintenance.

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

Access Kronos Workforce Ready
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