# Dayforce Jobs API

> Query jobs from Dayforce boards through one API with ?source=dayforce — plus how Dayforce's CSRF handshake and its 25-row page size really behave.

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

Dayforce is Ceridian's HCM platform, and its career sites are the enterprise
end of this pool: hospitals, media groups, manufacturers, the kind of employer
that runs one Dayforce tenant across a dozen brands. Every board lives under
`jobs.dayforcehcm.com/<clientNamespace>/<jobBoardCode>`, a Next.js front end over
an ASP.NET Core API. There are 28,727 Dayforce postings in this dataset — a number
that moves hourly, so read it from [`/v1/meta`](/docs/api-reference/meta) rather
than trusting this sentence.

## Try it

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

```json title="Illustrative — the shape, not a live response"
{
  "data": [
    {
      "id": "dayforce:northgate_10482",
      "title": "Multimedia Journalist",
      "company": {
        "slug": "northgate-media",
        "name": "Northgate Media Group",
        "domain": "northgatemedia.com",
        "website": "https://northgatemedia.com",
        "logo": "https://media.licdn.com/dms/image/…",
        "industry": "Broadcast Media",
        "employee_count": 3100
      },
      "location": {
        "remote": false,
        "countries": ["United States"],
        "cities": ["Des Moines"],
        "regions": ["Iowa"],
        "raw": ["Des Moines, IA, US"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": null,
      "seniority": null,
      "salary": {
        "min": 24.5,
        "max": 31.5,
        "currency": "USD",
        "unit": "HOUR",
        "annual_min": 50960,
        "annual_max": 65520,
        "summary": "$24.50 – $31.50 an hour"
      },
      "skills": ["video editing", "adobe premiere"],
      "taxonomies": ["Media"],
      "apply_url": "https://jobs.dayforcehcm.com/en-US/northgate/CANDIDATEPORTAL/jobs/10482",
      "url": "https://jobs.dayforcehcm.com/en-US/northgate/CANDIDATEPORTAL/jobs/10482",
      "source": "dayforce",
      "posted_at": "2026-07-09T13:04:00.000Z",
      "expires_at": "2026-08-08T00:00:00.000Z",
      "updated_at": "2026-07-14T02:19:07.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}
```

Note the id: `northgate_10482`, not `10482`. That prefix is load-bearing and the
reason is below.

## How Dayforce boards actually work

Three endpoints, and only the middle one is guarded:

```bash
# 1. CSRF — a token AND a Set-Cookie you have to keep
curl "https://jobs.dayforcehcm.com/api/auth/csrf"

# 2. List — POST, 25 rows, needs both halves of the handshake
curl -X POST "https://jobs.dayforcehcm.com/api/geo/<ns>/jobposting/search" \
  -H "X-CSRF-TOKEN: <token>" \
  -H "Cookie: __Host-next-auth.csrf-token=<cookie>" \
  -d '{"clientNamespace":"<ns>","jobBoardCode":"<board>","cultureCode":"en-US","paginationStart":0}'

# 3. Detail — GET, no auth at all
curl "https://jobs.dayforcehcm.com/api/geo/<ns>/jobposting/<ns>/en-US/<boardId>/<id>"
```

### The CSRF wall is a double-submit

The site's axios interceptor injects `X-CSRF-TOKEN` from next-auth on every POST,
and the API checks the header **and** the matching `__Host-next-auth.csrf-token`
cookie. Both, together, or nothing:

| Sent | Result |
| --- | --- |
| No token, no cookie | 403 |
| Header token only | 403 |
| Cookie only | 403 |
| Bogus token + real cookie | 403 |
| Header + its matching cookie | **200** |

So `GET /api/auth/csrf` first, keep the cookie it sets, and send both. That one
cookie is enough — the `__cf_bm` and callback-url cookies it also hands you are
not checked. Most HTTP clients have no cookie jar by default, which is the whole
of the difficulty: this is a two-line problem that reads as an auth wall.

The detail endpoint, and the `sitecontext` endpoint that tells you which languages
a board publishes in, take **no auth at all**.

### The page size is 25 and cannot be raised

The paging field is `paginationStart`, a row offset rather than a page index — the
site computes it as `(page - 1) * 25`. Arbitrary values work, and it's honest:
0/25/50/…/150 on one board returned 159 unique ids with zero overlap, exactly the
`maxCount` it reported.

What you cannot do is ask for more rows. Roughly 45 override names were sent —
`count`, `take`, `pageSize`, `limit`, `maxCount`, `paginationCount`,
`paginationEnd` and the rest, in the body, nested under `paging` / `pagination` /
`searchCriteria` / `filter`, and as query strings — and every one was **silently
ignored** at HTTP 200, with the response echoing `count: 25` regardless. One
request per 25 jobs is the floor on this platform.

Finding the field that *does* work is harder than it sounds, because
System.Text.Json drops unknown properties silently: a wrong name is
indistinguishable from a right one that "doesn't work". `offset: "XX"`, a string
in an int field, does **not** type-error, which is the tell that the field isn't
in the model at all.

### Board codes are not guessable

`boardToken` is `<clientNamespace>/<jobBoardCode>`, and the second half is not
derivable. The obvious guesses, `Careers` and `JobOpenings`, both 404. The real
ones observed are `CANDIDATEPORTAL`, `alljobs` and `jobboards`. There's no index
and no pattern, so discovery is the only way to learn a board exists. This is the
"finding the board is harder than fetching it" problem again, and on Dayforce it's
the entire cost of coverage.

## What bites you

<Warning title="`jobPostingId` is a per-client counter, not a global id">
  The ranges overlap hard across tenants — one client's postings run 9,637–11,915
  while another's run 9,749–12,513. A key of `dayforce:<jobPostingId>` collides
  and silently merges two employers' jobs into one row.

  The key has to be scoped, and scoped to the **client**, not the board: the id
  is drawn from one sequence shared across all of a client's boards, so scoping by
  namespace also correctly dedups a posting listed on two of them. Ours is
  `dayforce:<clientNamespace>_<jobPostingId>`, which is why the ids on this source
  look like `dayforce:northgate_10482`.
</Warning>

**`cultureCode` filters the board, it does not translate it.** A job is visible
only in the culture it was posted in. One board returns 154 jobs on `en-US`, 35 on
`fr-CA`, and 2 on `de-DE` — so the wrong culture hands you a **partial board at
HTTP 200**, invisibly, and a genuinely multilingual employer quietly yields only
its English postings. We default to `en-US` and probe `sitecontext` for the real
list only when a board comes back empty, rather than writing it off as dead.

**`coordinates` is a centroid of whatever the address names.** Every
`"United States"` row on the platform carries the same `lat: 38.7945952` — the
dead centre of the country, in rural Colorado. `"Canada"` gets 56.13,-106.35;
counties and states get their own. Emit them and every vague or remote posting
pins to one fake point on a map. `cityName` is the only signal that the
coordinates refer to a real place, and a comma in the address is **not** one:
states and counties have commas too.

**`jobPostingAttributes[].value` is not uniformly a string.** The `type: "string"`
rows (`JobFamily`, `PayType`, `EducationLevel`) carry strings; the
`type: "currency"` rows — `HiringMinRate`, `HiringMaxRate`, `HiringRate`, which is
to say *every salary* — carry a JSON **number**. A `typeof v === "string"` guard
therefore drops the entire salary map while the string attributes sail through,
and the board then reports department at 100% and salary at 0% and looks
completely healthy. That bug shipped here for one run and took a per-field value
check across five tenants to see.

**`PayType` is the only period signal, and it occasionally lies.** One posting in
161 was marked `"Salary"` on a 28.20–32.30 CAD range that is plainly an hourly
wage. Trust the label and you write a $28-per-year salary. We honour it only when
the magnitude agrees and fall back to inferring from the amount when they
contradict each other.

**`postingLocations` can be null, not merely empty** — a third of the rows on one
tenant. And the list's `jobDescription` is the full body but HTML-stripped and
**capped at 12,000 characters**, while `jobDescriptionHeader`/`Footer` are
per-board boilerplate: the same 6.5k EEO block repeated on every job. Concatenate
them and you add 8k of noise to every row.

## Or use ours

Dayforce is cheap per board and expensive per posting, and none of the cost is
where you'd guess. It isn't the CSRF handshake, which is two lines once you know
the cookie matters. It's that the page size is pinned at 25, the board code can't
be derived, the culture parameter silently truncates, and the salary type isn't
what its neighbours' types are.

You get the descriptions uncapped rather than truncated at 12,000 characters, the
boilerplate header and footer left out, the client-scoped ids that stop one
employer's `10482` from clobbering another's, salary read as a number where it's
stored as a number and refused where the label contradicts the magnitude,
centroids withheld so a remote job doesn't land in rural Colorado, and the
[company join](/docs/objects/company).

Refresh cadence varies by [source](/docs/reference/sources), and one request per
25 postings puts this one at the expensive end. See
[coverage](/docs/reference/coverage), and don't assume uniform latency.

Query it with `?source=dayforce`.
[Quickstart](/docs/quickstart) · [Pricing](/pricing)
