# PageUp Jobs API

> Query jobs from PageUp boards through one API with ?source=pageup — plus how PageUp's listing endpoint and its #job-details wrapper really behave.

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

PageUp is an enterprise talent management platform with a heavy footprint in
higher education and in Australia and New Zealand — universities, health
services, state agencies, the kind of employer with a thousand open roles and a
procurement process. Boards live at
`careers.pageuppeople.com/<orgId>/<code>/<lang>/`, and there are 8,644 PageUp
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=pageup" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=2"
```

```json title="Illustrative — the shape, not a live response"
{
  "data": [
    {
      "id": "pageup:533_caw_en_694218",
      "title": "Lecturer, Environmental Science",
      "company": {
        "slug": "kelvin-university",
        "name": "Kelvin University",
        "domain": "kelvin.edu.au",
        "website": "https://kelvin.edu.au",
        "logo": "https://media.licdn.com/dms/image/…",
        "industry": "Higher Education",
        "employee_count": 4200
      },
      "location": {
        "remote": false,
        "countries": ["Australia"],
        "cities": ["Melbourne"],
        "regions": ["Victoria"],
        "raw": ["Bundoora Campus, Melbourne, VIC"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": null,
      "seniority": null,
      "salary": null,
      "skills": ["teaching", "research"],
      "taxonomies": ["Education"],
      "apply_url": "https://careers.pageuppeople.com/533/caw/en/job/694218",
      "url": "https://careers.pageuppeople.com/533/caw/en/job/694218",
      "source": "pageup",
      "posted_at": "2026-07-08T00:00:00.000Z",
      "expires_at": "2026-08-05T13:59:00.000Z",
      "updated_at": "2026-07-14T05:33:18.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}
```

`salary` is null there and it is not an omission: PageUp exposes no structured pay
field. `expires_at` being real, on the other hand, is unusual — most ATS publish
no deadline at all.

## How PageUp boards actually work

No JSON API, no JSON-LD, HTML at both levels. The list is the easy half:

```bash
# 1. List — a plain GET, 100 per page, 1-indexed
curl "https://careers.pageuppeople.com/<orgId>/<code>/<lang>/listing/?page=1&page-items=100"

# 2. Detail — one per posting; the body only exists here
curl "https://careers.pageuppeople.com/<orgId>/<code>/<lang>/job/<jobId>"
```

That's genuinely all the list takes. No POST, no token, no handshake, and
`page-items=100` works — which makes PageUp one of the cheapest listings in this
pool to enumerate. The cost is the second call: the listing's `<tr class="summary">`
is a teaser that looks like a description, so every posting needs its own fetch.

### Three things you can't derive

`boardToken` is the `<orgId>/<code>/<lang>` **triple**, and all three vary:

| From `https://careers.pageuppeople.com/533/caw/en/listing/` | |
| --- | --- |
| **orgId** | `533` — a bare number |
| **code** | `caw` — also seen as `cw`, `eb`, `ci`, and others |
| **lang** | `en` — or `en-us` |

None of it is derivable from a company's own careers domain. A university links
its PageUp board from a page on its own site, and the triple is whatever PageUp
assigned it.

### The body is in `#job-details` on most tenants, not all

This is the structural thing to know about PageUp. The description sits in a
`<div id="job-details">` on the standard template — but not everywhere. One
university's board has no such wrapper at all; the body sits directly in
`#job-content`, and reading only `#job-details` scores **0 of 10** on that tenant
while looking perfectly healthy on every other. With the fallback in place it's
50 of 50 across five tenants.

That's the shape of every PageUp problem: the template is tenant-configurable, so
anything you key off has to be the thing that survives the customisation.

## What bites you

**`page` is 1-indexed, and `page=0` returns zero rows.** Not page one — zero
rows. So the off-by-one you'd normally shrug at silently drops the entire first
page, and the board reads as smaller than it is with no error anywhere.

**`page-items` caps at 100 silently.** Ask for 200 and you get 100, at HTTP 200,
with nothing to say you were trimmed.

<Note title="Exactly 100 looks like a cap and usually isn't">
  A board returning exactly `page-items` worth of rows is the classic tell of a
  silent truncation, and here it's usually just a board with more than 100 jobs.
  The only way to know is to fetch page 2. Guessing either way is wrong: assume
  it's a cap and you stop early, assume it isn't and you never check.
</Note>

**`<h1>` is the site name on the standard template and the job title on custom
ones.** "Careers at Kelvin University" is not a job. Key off the `<h2>` inside
`#job-content` and fall back to `<h1>`, never the reverse.

**Labels are tenant-configurable, so key off the CSS class.** One tenant writes
`Job no`, another `Job No.`, another translates it. The class names are the stable
part of the markup; the text next to them isn't.

**`<time datetime="…Z">` disagrees with the text beside it.** The attribute is
Z-suffixed while the adjacent literal string says "Eastern Daylight Time" — the
two are up to five hours apart, and there's no way to tell which the tenant meant.
We keep it, because the **date** is what a recency filter uses and that survives
the ambiguity, but don't read `posted_at` on this source as a precise instant.

**There's no structured salary at all.** Not on the listing, not on the detail.
Pay appears in the description prose or not at all, which is why
[`salary`](/docs/objects/job#salary) is null on PageUp postings far more often
than on a platform that publishes a real field. That's the platform, not us.

## Or use ours

PageUp is the case where the fetch really is easy and the parsing really isn't.
One GET gets you a hundred job ids. Then the template forks per tenant, the body
moves between two wrappers, the `<h1>` means two different things, the labels are
whatever the customer typed, and the timestamp contradicts itself.

You get the description from whichever wrapper the tenant actually used, the title
from the element that is a title on both templates, the requisition id read by
class rather than by label text, a real `expires_at` where the board published a
closing date, and the [company join](/docs/objects/company) — a PageUp board tells
you `533/caw/en` and nothing whatsoever about who that is.

Refresh cadence varies by [source](/docs/reference/sources), and a detail fetch
per posting puts PageUp at the expensive end even though its listing is cheap. See
[coverage](/docs/reference/coverage); don't assume uniform latency across sources.

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