All platforms

Cornerstone OnDemand Jobs API

Query jobs from Cornerstone OnDemand boards through one API with ?source=cornerstone — plus how Cornerstone's scraped-JWT handshake really behaves.

?source=cornerstonesource reference

Cornerstone OnDemand is a talent-management suite built around learning and compliance as much as hiring, which is the kind of thing large, regulated employers buy. Its career sites live on csod.com, and there are 12,463 Cornerstone postings in this dataset — a number that moves hourly, so read it from /v1/meta rather than trusting this sentence.

Cornerstone is unusual in a good way: its descriptions arrive in the listing, so there's no detail call per posting. It's unusual in a less good way too, which is the handshake.

Try it#

BASH
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "source=cornerstone" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=2"
Illustrative — the shape, not a live response
{
  "data": [
    {
      "id": "cornerstone:req-88104",
      "title": "Registered Nurse, Intensive Care",
      "company": {
        "slug": "st-aubin-health",
        "name": "St Aubin Health System",
        "domain": "staubinhealth.org",
        "website": "https://staubinhealth.org",
        "logo": "https://media.licdn.com/dms/image/…",
        "industry": "Hospitals and Health Care",
        "employee_count": 12400
      },
      "location": {
        "remote": false,
        "countries": ["United States"],
        "cities": ["Portland"],
        "regions": ["Oregon"],
        "raw": ["Portland, Oregon, United States"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": "On-site",
      "seniority": null,
      "salary": null,
      "skills": ["icu", "acls"],
      "taxonomies": ["Healthcare"],
      "apply_url": "https://staubin.csod.com/ux/ats/careersite/4/job/88104?c=staubin",
      "url": "https://staubin.csod.com/ux/ats/careersite/4/job/88104?c=staubin",
      "source": "cornerstone",
      "posted_at": "2026-07-10T00:00:00.000Z",
      "expires_at": "2026-08-09T00:00:00.000Z",
      "updated_at": "2026-07-14T08:33:19.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}

How Cornerstone boards actually work#

Two steps, and the first one exists only to get a token:

BASH
# 1. GET the career site HTML — scrape the JWT and the regional API host out of it
curl "https://<tenant>.csod.com/ux/ats/careersite/<siteId>/home?c=<slug>"
 
# 2. POST the search, carrying that JWT — descriptions included
curl -X POST "https://na.api.csod.com/rec-job-search/external/jobs" \
  -H "Authorization: Bearer <token>" \
  -H "content-type: application/json" \
  -d '{"careerSiteId":4,"careerSitePageId":4,"pageNumber":1,"pageSize":50,
       "cultureId":1,"cultureName":"en-US"}'

The JWT is in the page, and it isn't a login#

The career-site HTML carries csod.context.token = '<jwt>'. There's no account, no client credentials and no OAuth dance — the token is a public context token that the tenant's own front end uses, and the API rejects you without it. So the "auth" is really a scrape: fetch the HTML, pull the token, use it before it expires.

The API host is in that HTML too, and it's regional. na.api.csod.com, eu-fra.api.csod.com, uk.api.csod.com. Hardcode the North American one and every European tenant fails, which looks like a dead board rather than a wrong host.

The description is in the listing#

The externalDescription field on each requisition is the full body. No N+1, no detail fetch, one POST per 50 postings. This is why Cornerstone descriptions land at 99.7% and why the platform is cheap to keep fresh despite being enterprise.

What bites you#

Unpublished drafts ship with the real jobs

Cornerstone returns template and draft requisitions through the same public endpoint as live ones, and they announce themselves in the title:

<<ENTER DISPLAY JOB TITLE>>
// ENTER DISPLAY JOB TITLE \\

They parse perfectly. Nothing about the response says they're not real. Store them and your dataset has jobs whose title is a placeholder an admin never filled in.

"Please upload the job description" is a description. As far as any fill-rate check is concerned, anyway. A subset of postings carry a placeholder body — please upload the job description, no description available, see job description — which is a populated string of plausible length. It counts as a hit on every naive coverage metric and is worth nothing to a reader. We null it rather than serve it.

A raw job URL is not a board. Same shape of trap as Oracle: the careers URL and a link to one posting differ by a /requisition/{id} suffix, so storing whatever URL discovery found turns every requisition into its own board. Canonicalise to host, site id and slug first.

country is an ISO-2 code. MG, not Madagascar. The location string won't resolve until it's expanded, and a two-letter token is exactly the kind of thing a geocoder will cheerfully match to something else.

Or use ours#

The listing-side descriptions genuinely are the easy part. The rest isn't.

You get the JWT handshake and the regional host resolved per tenant, draft requisitions and placeholder bodies filtered out rather than counted, ISO-2 countries expanded so locations resolve deterministically, real posting expiry from postingExpirationDate rather than a synthetic default, and the company join. And you get the board list, which is the part that took the time: Cornerstone publishes no index of its customers, so every tenant behind these 12,463 postings was found individually.

Cornerstone sits in the heavy refresh tier, attempted every third cycle — see coverage.

Query it with ?source=cornerstone. Quickstart · Pricing

Every source above normalises onto the same schema, with the hiring company already joined onto each posting.

Get an API key