In the last post
I listed six ATS platforms whose job boards are open JSON APIs — Greenhouse,
Lever, Ashby, Workable, Recruitee, SmartRecruiters. The honest caveat at the
bottom was that none of it covers Workday, which is where most of the Fortune
500 actually lives.
This post closes that gap. Workday careers sites are also a client-side app
calling a JSON API, and you can call it directly — no key, no login, no
headless browser. It is just harder to find, shaped differently, and mined
with quieter traps than any of the six easy ones.
Everything below was run live while writing this, in August 2026: NVIDIA
(2,000 open roles), Salesforce (1,477), Adobe (800). As before, the counts are
there to show the response shape, not as figures to check against later.
The endpoint
Workday careers pages live at URLs like:
https://{tenant}.wd{N}.myworkdayjobs.com/{site}
Behind each one is a POST endpoint:
POST https://{tenant}.wd{N}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs
Content-Type: application/json
{"appliedFacets": {}, "limit": 20, "offset": 0, "searchText": ""}
Real examples, all verified today:
nvidia.wd5.myworkdayjobs.com /wday/cxs/nvidia/NVIDIAExternalCareerSite/jobs → 2,000 roles
salesforce.wd12.myworkdayjobs.com/wday/cxs/salesforce/External_Career_Site/jobs → 1,477 roles
adobe.wd5.myworkdayjobs.com /wday/cxs/adobe/external_experienced/jobs → 800 roles
Note it is a POST with a JSON body, unlike every endpoint in the previous
post. A GET returns nothing useful, which is one reason people conclude the
API "doesn't exist" and reach for Selenium.
Finding the three parts
With Greenhouse you can guess the board token from the company domain.
With Workday you cannot guess any of the three parts:
-
{tenant}is usually the company name, but not reliably -
wd{N}is a shard number — NVIDIA is on wd5, Salesforce on wd12 — and there is no rule for which -
{site}is whatever the company named their career site:NVIDIAExternalCareerSite,External_Career_Site,external_experienced. Three companies, three naming conventions.
The good news: every Workday job link contains all three. Take any posting
URL a company shares —
https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite/job/...
— and you can read tenant (nvidia), shard (wd5) and site
(NVIDIAExternalCareerSite) straight out of it, dropping the locale segment
(en-US) if present. So the practical flow is: given a company, find one job
link (their careers page, a LinkedIn posting, a Google result), decompose it
once, and you have the API forever.
Trap 1: limit above 20 returns zero rows, not an error
This one cost me the most time, and I have not seen it written down anywhere.
The endpoint pages at 20 records. If you ask for more:
{"limit": 20} → 20 jobs
{"limit": 21} → 0 jobs
{"limit": 100} → 0 jobs
No error, no warning, HTTP 200, total still says 1,477 — and an empty
jobPostings array. If your code asks for 100 per page (a perfectly normal
thing to do, since SmartRecruiters takes 100), every Workday company comes
back as "not hiring". Silent wrong answers again: the response looks exactly
like a company with no openings.
Page with limit: 20 and step offset by 20 until you reach total:
import httpx
def workday_jobs(tenant: str, shard: str, site: str) -> list[dict]:
url = f"https://{tenant}.{shard}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs"
out, offset = [], 0
with httpx.Client(timeout=30) as client:
while True:
r = client.post(url, json={
"appliedFacets": {}, "limit": 20,
"offset": offset, "searchText": "",
}, headers={"Accept-Language": "en-US"})
r.raise_for_status()
data = r.json()
batch = data.get("jobPostings") or []
out.extend(batch)
offset += 20
if not batch or offset >= data.get("total", 0):
return out
Trap 2: postedOn is not a date — it is a sentence, in your language
The list response carries five fields per job:
title, externalPath, locationsText, postedOn, bulletFields
postedOn looks like a date field. It is a localized display string. The
same Salesforce posting, same endpoint, different Accept-Language:
Accept-Language: en-US → "postedOn": "Posted Today", "locationsText": "2 Locations"
Accept-Language: ko → "postedOn": "오늘 공고", "locationsText": "2개 근무지"
I only caught this because my browser is set to Korean. If you parse
postedOn as a date, your code works on your machine and breaks for anyone
whose locale differs — or breaks next week when "Posted Today" becomes
"Posted 30+ Days Ago", which is the only granularity you get past a month.
Two consequences: always send an explicit Accept-Language, and if you need a
real date, get it from the detail endpoint below (startDate is an actual
YYYY-MM-DD).
The detail endpoint
Each list item has an externalPath. Append it to the site base with an
Accept: application/json header:
GET https://{tenant}.wd{N}.myworkdayjobs.com/wday/cxs/{tenant}/{site}{externalPath}
Accept: application/json
That returns the full record: jobDescription (HTML), timeType
("Full time"), startDate (a real date at last), jobReqId, country,
additionalLocations. Without the Accept header you get the HTML shell of
the careers page instead — the same URL serves both.
One request per job, so for a 2,000-role tenant that is 2,000 extra requests.
Fetch details only for the postings you actually care about, after filtering
the list.
Trap 3: no CORS — this is server-side only
The six platforms in the previous post mostly tolerate browser calls; Workable
even sends explicit CORS headers. Workday does not. A cross-origin fetch
from a browser dies before you see a status code. Call it from a server, a
script, a notebook — anything but someone else's web page. (Same-origin works,
which is how the careers page itself uses it.)
Limits, honestly
- This is the API behind the public careers page, read-only and unauthenticated — but unlike Greenhouse's boards API it is not documented as a public contract. Workday can reshape it without notice. Build accordingly: tolerate missing fields, alert on shape changes.
- The list response is thin — five fields. Department, salary and employment type all live behind the per-job detail request.
- Some tenants run heavily customized sites; the three above are typical, not universal.
- Page politely. It is one POST per 20 jobs; there is no reason to hammer it.
What I have not built (and a question)
The previous post's
six platforms are packaged as ready-to-run scrapers with change detection.
Workday is not among them yet — the discovery step (finding tenant/shard/site
per company) makes it a different kind of product, and I would rather build it
properly than bolt it on.
If a Workday version would be useful to you, say so in the comments — what you
would feed it (company names? careers URLs?) decides how the input should
work. The code above is enough to get you unblocked either way.
Top comments (0)