All platforms

Cornerstone OnDemand (CSOD) Jobs API.

Pull enterprise job listings from Cornerstone OnDemand (CSOD) career sites with a single token-authenticated REST call that returns full descriptions, multiple locations, and posting dates in one response.

Get API access
Cornerstone OnDemand (CSOD)
Live
80K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Cornerstone OnDemand (CSOD)
HenkelBitdefenderState of IllinoisState of Louisiana
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 Cornerstone OnDemand (CSOD).

Data fields
  • Full Job Descriptions
  • Multi-Location Data
  • Posting & Expiration Dates
  • Requisition IDs & Titles
  • Job Category & Type
  • Shift & Travel Details
Use cases
  1. 01Enterprise Job Aggregation
  2. 02Global Labor Market Analysis
  3. 03Job Board Syndication
  4. 04Hiring Trend Monitoring
Trusted by
HenkelBitdefenderState of IllinoisState of Louisiana
DIY GUIDE

How to scrape Cornerstone OnDemand (CSOD).

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

RESTadvancedNo published limit; space requests ~300ms and back off on 429Auth required

Identify the Cornerstone URL pattern

Modern CSOD career sites follow a fixed URL shape. Parse the company code (the ?c= query param, which overrides the subdomain) and the numeric site ID from the /careersite/{id} path segment.

Step 1: Identify the Cornerstone URL pattern
# Modern CSOD URL format:
# https://{company}.csod.com/ux/ats/careersite/{siteId}/home?c={company}

# Examples:
# https://henkel.csod.com/ux/ats/careersite/1/home?c=henkel
# https://thekids.csod.com/ux/ats/careersite/4/home?c=thekids

company = "henkel"
site_id = 1
company_url = f"https://{company}.csod.com/ux/ats/careersite/{site_id}/home?c={company}"
print(f"Target URL: {company_url}")

Extract the JWT token and API region

CSOD embeds a Bearer token and the regional API host in the page bootstrap JSON. Load the career site page and pull both out with regex — the token authenticates every API call and the cloud host tells you which region to hit.

Step 2: Extract the JWT token and API region
import re
import requests

response = requests.get(company_url)
html = response.text

# CSOD embeds the token and API region in the page bootstrap JSON
# as "token":"eyJ..." and "cloud":"https://...".
token_match = re.search(r'"token"\s*:\s*"(eyJ[^"]+)"', html)
token = token_match.group(1) if token_match else None

cloud_match = re.search(r'"cloud"\s*:\s*"([^"]+)"', html)
cloud_endpoint = cloud_match.group(1).rstrip("/") if cloud_match else None

if not token:
    raise Exception("Failed to extract JWT token")

# If the page did not expose a region, probe the known hosts (see Step 3).
print(f"Token length: {len(token)}")
print(f"Cloud endpoint: {cloud_endpoint}")

Call the job search API

POST the extracted token to {region}/rec-job-search/external/jobs. If the page did not reveal a region, fall back to probing the known CSOD hosts and keep the first that returns status "Success".

Step 3: Call the job search API
import requests

KNOWN_API_REGIONS = [
    "https://eu-fra.api.csod.com",
    "https://uk.api.csod.com",
    "https://api.csod.com",
    "https://us.api.csod.com",
]

payload = {
    "careerSiteId": site_id,
    "careerSitePageId": 1,
    "pageNumber": 1,
    "pageSize": 25,
    "cultureId": 1,
    "searchText": "",
    "cultureName": "en-US",
    "states": [],
    "countryCodes": [],
    "cities": [],
    "placeID": "",
    "radius": None,
    "postingsWithinDays": None,
    "customFieldCheckboxKeys": [],
    "customFieldDropdowns": [],
    "customFieldRadios": [],
}

headers = {
    "Authorization": f"Bearer {token}",
    "Content-Type": "application/json",
    "Origin": f"https://{company}.csod.com",
    "Referer": f"https://{company}.csod.com/",
    "Csod-Accept-Language": "en-US",
}

def search_jobs(region):
    resp = requests.post(f"{region}/rec-job-search/external/jobs",
                         headers=headers, json=payload)
    return resp

regions = [cloud_endpoint] if cloud_endpoint else KNOWN_API_REGIONS
for region in regions:
    resp = search_jobs(region)
    data = resp.json() if resp.ok else {}
    if data.get("status") == "Success":
        cloud_endpoint = region
        break

print(f"Found {data.get('data', {}).get('totalCount', 0)} total jobs")

Parse job details from the response

Each requisition carries the full external description plus one or more locations, so no per-job detail call is needed. Build the canonical requisition URL from the same components you parsed in Step 1.

Step 4: Parse job details from the response
jobs = []
for req in data.get("data", {}).get("requisitions", []):
    job = {
        "id": req.get("requisitionId"),
        "title": req.get("displayJobTitle") or req.get("postingTitle"),
        "description": req.get("externalDescription"),  # full text, inline
        "locations": [
            {
                "city": loc.get("city"),
                "state": loc.get("state"),
                "country": loc.get("country"),
            }
            for loc in req.get("locations", [])
        ],
        "posted_date": req.get("postingEffectiveDate"),
        "expiration_date": req.get("postingExpirationDate"),
        "detail_url": f"https://{company}.csod.com/ux/ats/careersite/{site_id}/home/requisition/{req.get('requisitionId')}?c={company}",
    }
    jobs.append(job)

print(f"Parsed {len(jobs)} jobs")
print(f"First job title: {jobs[0]['title'] if jobs else 'No jobs found'}")

Handle pagination

CSOD returns totalCount alongside a fixed page size. Increment pageNumber until you have collected totalCount jobs (or a page comes back empty), pausing briefly between calls to stay polite.

Step 5: Handle pagination
import time

all_jobs = []
page_number = 1
page_size = 25

while True:
    payload["pageNumber"] = page_number
    payload["pageSize"] = page_size

    response = requests.post(f"{cloud_endpoint}/rec-job-search/external/jobs",
                             headers=headers, json=payload)
    data = response.json()
    requisitions = data.get("data", {}).get("requisitions", [])

    if not requisitions:
        break

    all_jobs.extend(requisitions)
    total_count = data.get("data", {}).get("totalCount", 0)
    print(f"Page {page_number}: {len(requisitions)} jobs (total: {len(all_jobs)}/{total_count})")

    if len(all_jobs) >= total_count or len(requisitions) < page_size:
        break

    page_number += 1
    time.sleep(0.3)  # pace requests

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

Refresh the token on expiry

CSOD JWTs are short-lived (roughly an hour). When the API returns 401, re-fetch the career site page to mint a fresh token and region, then retry the request.

Step 6: Refresh the token on expiry
import re
import requests

def get_fresh_token(company_url):
    """Re-fetch a JWT token and cloud region from the career site page."""
    html = requests.get(company_url).text
    token_match = re.search(r'"token"\s*:\s*"(eyJ[^"]+)"', html)
    cloud_match = re.search(r'"cloud"\s*:\s*"([^"]+)"', html)
    if not token_match:
        raise Exception("Failed to extract JWT token")
    cloud = cloud_match.group(1).rstrip("/") if cloud_match else None
    return token_match.group(1), cloud

def fetch_jobs_with_retry(company, site_id, payload, max_retries=2):
    company_url = f"https://{company}.csod.com/ux/ats/careersite/{site_id}/home?c={company}"
    for attempt in range(max_retries + 1):
        token, cloud_endpoint = get_fresh_token(company_url)
        headers = {
            "Authorization": f"Bearer {token}",
            "Content-Type": "application/json",
        }
        response = requests.post(f"{cloud_endpoint}/rec-job-search/external/jobs",
                                 headers=headers, json=payload)
        if response.status_code == 401:
            print(f"Token expired, refreshing... (attempt {attempt + 1})")
            continue
        response.raise_for_status()
        return response.json()
    raise Exception("Failed to fetch jobs after token refresh")
Common issues
highJWT token expired mid-scrape

CSOD Bearer tokens live for roughly an hour. On a 401, re-fetch the career site page, extract a new "token", and retry. Cache the token per tenant so you only reload the page when it actually expires.

mediumDescriptions come back as "Job Description Here"

Some tenants author content into a legacy field, so the listings API's externalDescription is just a placeholder. Detect that exact string, then GET the requisition page and read the schema.org JobPosting JSON-LD block for the real ad.

mediumRegional API host unknown

The API region varies by tenant. Read it from the "cloud" value in the page bootstrap JSON; if that is missing, probe the known hosts (eu-fra.api.csod.com, uk.api.csod.com, api.csod.com, us.api.csod.com) and keep the first that returns status "Success".

mediumLegacy career site URLs are not scrapable

Only modern /ux/ats/careersite/ URLs expose the public token and API. Legacy hosts (e.g. cornerstone.csod.com) redirect to authentication and should be skipped or handled via an HTML fallback.

mediumBrowser requests blocked by CORS

Run requests server-side and send Origin and Referer headers matching the tenant's csod.com domain. Direct browser calls hit CORS restrictions the API expects to originate from the career site.

lowIntermittent 429 or 504 responses

Space requests roughly 300ms apart and keep concurrency low. Treat 429/403 as rate limiting and 504 as a transient gateway timeout — back off and retry rather than dropping the tenant.

lowWrong site ID or culture returns no jobs

Site ID comes from the /careersite/{id} path and defaults to 1 when absent; cultureId defaults to 1 (en-US). Confirm both from the URL and page context if a tenant returns an empty requisitions array.

Best practices
  1. 1Read the API region from the page's "cloud" value; fall back to probing the known CSOD hosts.
  2. 2Cache the JWT per tenant and only refresh it when the API returns 401.
  3. 3Send requests server-side with matching Origin and Referer headers to avoid CORS blocks.
  4. 4Space requests ~300ms apart and back off on 429, 403, or 504 responses.
  5. 5When externalDescription reads "Job Description Here", pull the full ad from the requisition page's JSON-LD.
  6. 6Parse the company from the ?c= param (it overrides the subdomain) and the site ID from the /careersite/{id} path.
Or skip the complexity

One endpoint. All Cornerstone OnDemand (CSOD) jobs. No scraping, no sessions, no maintenance.

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

Access Cornerstone OnDemand (CSOD)
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