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.
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).
- Full Job Descriptions
- Multi-Location Data
- Posting & Expiration Dates
- Requisition IDs & Titles
- Job Category & Type
- Shift & Travel Details
- 01Enterprise Job Aggregation
- 02Global Labor Market Analysis
- 03Job Board Syndication
- 04Hiring Trend Monitoring
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")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.
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.
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".
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.
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.
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.
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.
- 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" 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.