- 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.
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.
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
- 01Enterprise Job Aggregation
- 02Global Labor Market Analysis
- 03Job Board Syndication
- 04Hiring Trend Monitoring
Trusted by
- Henkel
- Bitdefender
- State of Illinois
- State of Louisiana
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.
# 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}")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}")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")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'}")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")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")- 1Read the API region from the page's "cloud" value; fall back to probing the known CSOD hosts.
- 2Cache the JWT per tenant and only refresh it when the API returns 401.
- 3Send requests server-side with matching Origin and Referer headers to avoid CORS blocks.
- 4Space requests ~300ms apart and back off on 429, 403, or 504 responses.
- 5When externalDescription reads "Job Description Here", pull the full ad from the requisition page's JSON-LD.
- 6Parse the company from the ?c= param (it overrides the subdomain) and the site ID from the /careersite/{id} path.
One endpoint. All Cornerstone OnDemand (CSOD) jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=cornerstone ondemand (csod)" \
-H "X-Api-Key: YOUR_KEY"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.
Access Cornerstone OnDemand (CSOD)
job data today.
One API call. Structured data. No scraping infrastructure to build or maintain — start with the $5 free starting balance and scale as you grow.