# Workday Jobs API

> Query jobs from Workday tenants through one API — and what the Workday CXS endpoint really costs if you build the scraper yourself.

Source: https://hyperjobs.io/ats/workday

Workday is the HR platform most large enterprises run on, which makes it the ATS
behind a very large share of the jobs you actually want and the least pleasant one
in this pool to read. Every customer is a **tenant** with its own host and its own
career site: `nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite`. There is no
global index, no shared API host, and no single endpoint that serves more than one
company. Our queue tracks 7,586 Workday boards, and every one of them was found
individually.

## Try it

```bash
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "source=workday" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=2"
```

```json title="Illustrative — the shape, not a live response"
{
  "data": [
    {
      "id": "workday:globex_R-007754",
      "title": "Principal Firmware Engineer",
      "company": {
        "slug": "globex",
        "name": "Globex Corporation",
        "domain": "globex.com",
        "website": "https://globex.com",
        "logo": "https://globex.wd5.myworkdayjobs.com/wday/cxs/…",
        "industry": "Semiconductors",
        "employee_count": 24000
      },
      "location": {
        "remote": false,
        "countries": ["United States"],
        "cities": ["Maryville"],
        "regions": ["Tennessee"],
        "raw": ["Maryville, TN 37801, United States of America"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": "On-site",
      "seniority": null,
      "salary": null,
      "skills": ["c", "rtos", "embedded"],
      "taxonomies": ["Software"],
      "apply_url": "https://globex.wd5.myworkdayjobs.com/en-US/GlobexCareers/job/Maryville/Principal-Firmware-Engineer_R-007754",
      "url": "https://globex.wd5.myworkdayjobs.com/en-US/GlobexCareers/job/Maryville/Principal-Firmware-Engineer_R-007754",
      "source": "workday",
      "posted_at": "2026-07-06T00:00:00.000Z",
      "expires_at": "2026-08-30T00:00:00.000Z",
      "updated_at": "2026-07-12T04:41:52.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}
```

`salary` being null there is not an omission — see below. Workday genuinely does
not publish one.

## How Workday boards actually work

Workday's career sites are backed by a JSON API called **CXS**. It takes two
steps, and the first one is a POST.

```bash
# 1. List — POST, paginated, no descriptions
curl -X POST "https://<host>/wday/cxs/<tenant>/<site>/jobs" \
  -H "content-type: application/json" \
  -d '{"limit":20,"offset":0,"searchText":""}'

# 2. Detail — GET, one per posting, this is where the description is
curl "https://<host>/wday/cxs/<tenant>/<site><externalPath>" \
  -H "accept: application/json"
```

### Three identifiers, not one

You cannot derive a Workday board from a company name. You need three things, and
all three come out of the careers URL:

| From `https://nvidia.wd5.myworkdayjobs.com/en-US/NVIDIAExternalCareerSite` | |
| --- | --- |
| **tenant** | `nvidia` |
| **host** | `nvidia.wd5.myworkdayjobs.com` — note the `wd5`, it varies per customer |
| **site** | `NVIDIAExternalCareerSite` — arbitrary, employer-chosen |

The `wd5` is a Workday instance number and differs between tenants; there's no
rule that gets you from a company to its instance. The site name is whatever the
employer typed. An optional locale segment (`en-US`) sits between them and has to
be skipped. Miss any of the three and you have nothing — this is the "finding the
board is harder than fetching it" problem in its purest form.

### The list has no descriptions

Every posting needs its own detail call. `jobPostingInfo.jobDescription` holds the
full HTML, and it exists nowhere else. There is no `content=true` equivalent, no
bulk mode, no feed. A tenant with 6,000 postings costs 6,000 requests plus 300
list pages, and the page size is 20.

<Note title="What we do about the N+1">
  Two things. The list call reports the complete live key set first, so board
  diffing and expiry work off the cheap call. Then only postings we've never seen
  get a detail fetch. A re-scrape of an unchanged 6,000-job tenant costs the list
  pages and nothing else.
</Note>

### Department and workplace type only exist as facets

This is the strangest part of the platform. Workday's job detail exposes no
department field. But the **list** response carries `facets[]`, and each job
belongs to exactly one value of each facet. So you recover per-job department by
re-querying the list once per facet value with `appliedFacets` and tagging every
returned job's req id. Every job appears in exactly one partition, so this fetches
the same job set again, partitioned — not more jobs, just more requests.

The department facet's *parameter name* is tenant-configurable. `jobFamilyGroup`
is the common one, but tenants ship `jobCategory`, a custom field with
`Department` in the name, or a bare `jobFamily` instead. Look for only the common
one and those tenants come back with zero departments and no error.

The workplace-type facet (`remoteType` / `isRemoteType` / `workplaceType`) is
exposed by a minority of tenants — roughly one in five — but where it exists it
tags every job, which matters because the per-detail `remoteType` is carried by
only about 30% of postings. Measured on one university tenant: On-Site 1,290,
Hybrid 138, Remote 53.

## What bites you

<Warning title="Req ids are only unique within a tenant">
  Two different companies will both have an `R-12345`. A key of
  `workday:<reqId>` collides across tenants and silently overwrites one company's
  job with another's. The key has to be scoped — ours is
  `workday:<tenant>_<reqId>`, which is why the ids on this source look like
  `workday:globex_R-007754`.
</Warning>

**Pagination that stops at 200 will silently truncate.** The page size is 20 and
the loop must run to end-of-list — a short page, or `offset + limit >= total`. A
cap set for safety becomes a data bug: a tenant with ~6,000 postings quietly
yielding 200 of them, with a `200` on every request.

**`hiringOrganization.name` is a legal entity, not a brand.** Retail, staffing and
grocery tenants put a per-store or per-subsidiary name there — `L001 Loblaws
Inc.`, `TJMaxx of CA, LLC`, one of four different PwC LLPs. Take it at face value
and one employer splinters into forty. We vote across the whole tenant: adopt a
proper name only when a single non-fragmenting name dominates the tenant's
postings, otherwise keep the stable slug. `Lowe's Home Centers, LLC` survives;
TJX keeps its slug.

**The req id in the URL isn't the clean one.** The path-derived id carries a
posting-variant suffix — `R-007754-2`. The detail's own `jobReqId` is the real
one, `R-007754`.

**Locations drop their country.** The display string is `Maryville, TN 37801`.
Workday carries the country separately in `country.descriptor` as
`United States of America`. We append it — but only to the primary location, since
`additionalLocations[]` may be in other countries entirely and appending would
produce `London, United States of America`.

**There is no structured salary. At all.** Workday CXS exposes nothing. Pay
appears only as free text inside the description, if it appears. We deliberately
don't parse it during the scrape — that's a guess, not ground truth, and this
phase only stores what the platform actually declared. So `salary` is null on
Workday postings far more often than on [Ashby](/ats/ashby) or
[Lever](/ats/lever), and that's the platform, not us.

**A minimal request body returns zero jobs rather than an error.** Get the POST
body wrong and you get a valid, empty, 200-status response.

## Or use ours

Workday is the clearest case on this site for not building it yourself, and the
reason isn't the parsing — it's that the work is per-tenant and never ends.

Every Workday customer is a separate discovery problem, a separate host, a
separate site name, and a separate N+1. We track 7,586 of them. You get the
descriptions without paying a request per posting, the tenant-scoped ids that stop
Globex's `R-12345` from clobbering Initech's, the org-name vote that keeps one
employer as one employer, and the country appended from Workday's own structured
field rather than guessed from a zip code.

Workday sits in the heavy refresh tier — attempted every third cycle rather than
every hour, because the N+1 makes it expensive. A new Workday posting can take
several hours to appear, and a removed one takes as long to reap.
[Coverage](/docs/reference/coverage) is explicit about that; don't build anything
that assumes uniform latency across [sources](/docs/reference/sources).

[Quickstart](/docs/quickstart) · [Pricing](/pricing)
