All platforms

Cornerstone OnDemand (CSOD) Jobs API.

Retrieve complete enterprise job listings — full descriptions included — from Cornerstone OnDemand career sites with a single authenticated API call per page.

Get API access
Cornerstone OnDemand (CSOD)
Live
90K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Companies using Cornerstone OnDemand (CSOD)
HenkelBoschBitdefender
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 Listings
  • Posting & Expiration Dates
  • Job Category & Type
  • Shift & Travel Details
  • Stable Requisition IDs
Use cases
  1. 01Enterprise job monitoring
  2. 02Multi-region talent sourcing
  3. 03Large company career tracking
  4. 04Global recruitment analysis
Trusted by
HenkelBoschBitdefender
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.

RESTintermediateNo published limit; add 1-2s delays and keep concurrency lowAuth required

Parse the CSOD URL to extract company and site ID

CSOD modern career sites encode the company as a subdomain (or ?c= query param) and the site ID in the path. Extract both so you can build API calls and job URLs.

Step 1: Parse the CSOD URL to extract company and site ID
import re
from urllib.parse import urlparse, parse_qs

def parse_csod_url(url: str) -> dict:
    """
    Parse CSOD URL to extract company and site ID.
    Format: https://{company}.csod.com/ux/ats/careersite/{siteId}/home?c={company}
    """
    parsed = urlparse(url)

    # Extract company from subdomain
    company = parsed.hostname.split('.')[0]

    # Extract site ID from path
    match = re.search(r'/careersite/(d+)', parsed.path)
    site_id = int(match.group(1)) if match else 1

    # Extract from query param as fallback
    query_params = parse_qs(parsed.query)
    company = query_params.get('c', [company])[0]

    return {
        "company": company,
        "site_id": site_id,
        "base_url": f"https://{company}.csod.com"
    }

# Example usage
url = "https://henkel.csod.com/ux/ats/careersite/1/home?c=henkel"
config = parse_csod_url(url)
print(f"Company: {config['company']}, Site ID: {config['site_id']}")

Extract the JWT token and regional endpoint from the page

CSOD embeds a JWT bearer token and the region-specific API host in the career site's bootstrap JavaScript. Load the HTML and pull both from the page context.

Step 2: Extract the JWT token and regional endpoint from the page
import requests
import re

def extract_csod_context(company: str) -> dict:
    """Extract JWT token and API endpoint from CSOD career site."""
    url = f"https://{company}.csod.com/ux/ats/careersite/1/home?c={company}"

    response = requests.get(url, timeout=30)
    response.raise_for_status()
    html = response.text

    # Extract JWT token from embedded JavaScript
    token_match = re.search(r'csod\.context\.token\s*=\s*["']([^"']+)["']', html)
    token = token_match.group(1) if token_match else None

    # Extract regional API endpoint
    cloud_match = re.search(r'csod\.context\.endpoints\.cloud\s*=\s*["']([^"']+)["']', html)
    cloud_endpoint = cloud_match.group(1).rstrip('/') if cloud_match else None

    if not token or not cloud_endpoint:
        raise ValueError("Failed to extract CSOD context from page")

    return {
        "token": token,
        "cloud_endpoint": cloud_endpoint,
        "company": company
    }

# Example usage
context = extract_csod_context("henkel")
print(f"Token length: {len(context['token'])}")
print(f"API endpoint: {context['cloud_endpoint']}")

Call the job search API with authentication

POST to the regional job search endpoint with the JWT bearer token. A single request returns full job information, including complete descriptions.

Step 3: Call the job search API with authentication
import requests

def fetch_csod_jobs(context: dict, page: int = 1, page_size: int = 25) -> dict:
    """Fetch jobs from CSOD API using extracted token."""
    api_url = f"{context['cloud_endpoint']}/rec-job-search/external/jobs"

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

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

    response = requests.post(api_url, json=payload, headers=headers, timeout=30)
    response.raise_for_status()

    return response.json()

# Example usage
data = fetch_csod_jobs(context, page=1, page_size=25)
print(f"Total jobs: {data['data']['totalCount']}")
print(f"Jobs on this page: {len(data['data']['requisitions'])}")

Parse job data from the API response

Map each requisition into a normalized job object. The listings response already carries the full description, so no separate detail call is required for most tenants.

Step 4: Parse job data from the API response
def parse_csod_job(requisition: dict, company: str, site_id: int = 1) -> dict:
    """Parse a CSOD requisition into a standardized job object."""
    return {
        "id": requisition.get("requisitionId"),
        "title": requisition.get("displayJobTitle"),
        "description": requisition.get("externalDescription", ""),
        "locations": [
            {
                "city": loc.get("city"),
                "state": loc.get("state"),
                "country": loc.get("country"),
            }
            for loc in requisition.get("locations", [])
        ],
        "posted_date": requisition.get("postingEffectiveDate"),
        "expiration_date": requisition.get("postingExpirationDate"),
        "url": f"https://{company}.csod.com/ux/ats/careersite/{site_id}/home/requisition/{requisition['requisitionId']}?c={company}",
    }

# Parse all jobs from the response
jobs = [
    parse_csod_job(req, context["company"])
    for req in data["data"]["requisitions"]
]

for job in jobs[:3]:
    print(f"- {job['title']} ({job['locations'][0]['city'] if job['locations'] else 'Remote'})")
    print(f"  Description length: {len(job['description'])} chars")

Handle pagination to fetch all jobs

CSOD uses 1-based page numbers. Loop until the requisitions array is empty or you have collected totalCount jobs, refreshing the token on 401 and pausing between requests.

Step 5: Handle pagination to fetch all jobs
import time

def fetch_all_csod_jobs(context: dict, page_size: int = 25, delay: float = 1.5) -> list:
    """Fetch all jobs from CSOD with pagination handling."""
    all_jobs = []
    page = 1
    total_count = None

    while True:
        print(f"Fetching page {page}...")

        try:
            data = fetch_csod_jobs(context, page=page, page_size=page_size)
        except requests.HTTPError as e:
            if e.response.status_code == 401:
                print("Token expired, refreshing...")
                context = extract_csod_context(context["company"])
                data = fetch_csod_jobs(context, page=page, page_size=page_size)
            else:
                raise

        requisitions = data.get("data", {}).get("requisitions", [])

        if not requisitions:
            break

        # Parse jobs from this page
        for req in requisitions:
            all_jobs.append(parse_csod_job(req, context["company"]))

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

        # Check if we've fetched all jobs
        if len(all_jobs) >= total_count:
            break

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

    return all_jobs

# Fetch all jobs
all_jobs = fetch_all_csod_jobs(context)
print(f"Total jobs fetched: {len(all_jobs)}")
Common issues
highJWT token expired (401 Unauthorized)

CSOD tokens are short-lived (roughly one hour). Re-fetch the career site page to extract a fresh token whenever the API returns 401, then retry the request.

mediumRegional API endpoint varies by tenant

The cloud host differs by region (uk.api.csod.com, eu-fra.api.csod.com, api.csod.com, us.api.csod.com). Read it from csod.context.endpoints.cloud instead of hardcoding; if extraction fails, probe the known regions with a 1-result request and keep the one that returns status Success.

mediumCORS blocks requests from the browser

The API expects Origin and Referer headers matching the career site domain. Run requests server-side to sidestep CORS entirely — that is the recommended approach for production scraping.

mediumLegacy URLs redirect to a login page

Only modern /ux/ats/careersite/ URLs are publicly accessible. Older subdomains without that path redirect to authentication, so validate the URL format before attempting to scrape.

lowPlaceholder description in the listings feed

Some tenants author content into a separate field and leave externalDescription as boilerplate like 'Job Description Here'. When that happens, fetch the requisition page and read the schema.org JobPosting JSON-LD block for the real description.

medium504 Gateway Timeout under load

The API occasionally returns 504 during peak traffic. Retry with exponential backoff (start at ~5s and double each attempt) and treat 403/429 as rate limiting.

Best practices
  1. 1Read the API host from csod.context.endpoints.cloud — never hardcode a regional URL
  2. 2Cache the token per tenant and refresh it on 401 (tokens last about an hour)
  3. 3Keep concurrency low and add 1-2 second delays between requests
  4. 4Fall back to the requisition page's JSON-LD when the listing description is a placeholder
  5. 5Run requests server-side to avoid browser CORS restrictions entirely
  6. 6Retry 504 responses with exponential backoff for resilient bulk pulls
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