# PageUp Jobs API: Get PageUp Job Listings as JSON (No Key)

> PageUp career sites serve every open job, with its full description, from one public jobs.json file. URL shapes, .NET dates, fields and gotchas.

Source: https://cleanjobdata.com/articles/how-to-scrape-pageup-job-listings
Published: 2026-11-01

TL;DR: PageUp career sites load their jobs from a public jobs.json file that returns every open position, with the full HTML description in Overview, in one request with no key. Hosted tenants use /{tenantId}/{siteCode}/{locale}/jobs.json on careers.pageuppeople.com, white-labeled custom domains use /{locale}/listing/jobs.json, and the two shapes are not interchangeable. Dates are .NET strings like /Date(1791295200000)/, LocationList is free text, and the Salary field is usually placeholder prose such as "Commensurate".

To get a company's jobs from PageUp, request the `jobs.json` file that sits next to its career listing page, for example `https://careers.pageuppeople.com/822/cw/en-us/jobs.json`. It's public, needs no key, and returns every open job with its full HTML description in a single JSON array. PageUp's own career-site widget calls this file, so it's the cleanest source on the platform, much more reliable than parsing the HTML listing pages.

That's the DIY route, and it works for a handful of companies you already know. If you'd rather not build and maintain a PageUp scraper, CleanJobData already ingests ATS platforms and company career sites and serves the jobs normalized through one REST API — see the [Jobs API docs](https://cleanjobdata.com/docs/api/jobs).

## At a Glance

| | |
|---|---|
| Endpoint (hosted) | `GET https://careers.pageuppeople.com/{tenantId}/{siteCode}/{locale}/jobs.json` |
| Endpoint (custom domain) | `GET https://{host}/{locale}/listing/jobs.json` |
| API key | None |
| Pagination | None: one response holds every open job |
| Full description | Yes, in `Overview` |
| Dates | .NET format, `/Date(1791295200000)/` |
| Salary field | `Salary`, free text, usually a placeholder |
| Remote | No flag; infer from title or description |

## What PageUp Is

PageUp is an Australian talent management and recruiting platform. It has a strong customer base in Australia and New Zealand, especially universities and public-sector employers, and it also has customers in the US, UK and Canada. Candidates browse jobs on a PageUp-hosted career site, and the apply flow stays on PageUp's own `secure.dc{N}.pageuppeople.com` hosts.

PageUp offers integrations to its customers that need credentials from the employer's account. For published jobs at companies you don't have that relationship with, the career site's `jobs.json` is the source.

## Finding the Listing Path

PageUp career sites come in two shapes, and you need to know which one you're looking at.

**Hosted tenants** live on `careers.pageuppeople.com` with three path segments before `listing`:

```
https://careers.pageuppeople.com/822/cw/en-us/listing
                                 │   │  └ locale
                                 │   └ site code
                                 └ numeric tenant ID
```

The tenant ID is numeric. The site code (`cw` is common) and the locale vary by tenant, and some tenants run more than one site.

**White-labeled custom domains** put the same site on the employer's own host, with just a locale before `listing`:

```
https://careers.acme.edu/en/listing
```

If a company's careers link doesn't show either pattern, open the browser's Network tab on its careers page and look for the request that ends in `jobs.json`.

## Fetching the Jobs

The two shapes put `jobs.json` at different depths. On hosted tenants it **replaces** `listing`; on custom domains it goes **inside** `listing/`:

```bash
# Hosted: sibling of "listing"
curl "https://careers.pageuppeople.com/822/cw/en-us/jobs.json"

# Custom domain: nested inside "listing/"
curl "https://careers.acme.edu/en/listing/jobs.json"
```

They aren't interchangeable, and getting it wrong doesn't fail cleanly. The wrong depth for the host type returns HTTP 200 with the tenant's HTML page instead of JSON. Decide the URL from the host, then check that the body actually parses as a JSON array before you trust it.

The response is served with `Content-Type: text/javascript`, not `application/json`, so some HTTP clients won't parse it for you. Read the body as text and parse it yourself.

There's no pagination. In our tests a 430-job tenant and a 106-job tenant both returned their complete list in one response.

## The Response Shape

A plain JSON array, one object per open position (trimmed, values illustrative):

```json
[
  {
    "Id": 512345,
    "Title": "Research Fellow, School of Engineering",
    "Overview": "<p>We are seeking a Research Fellow to join...</p>",
    "OpeningDateUtc": "/Date(1791295200000)/",
    "ClosingDateUtc": "/Date(1792504800000)/",
    "WorkType": "Staff Full Time",
    "LocationList": ["Acme Campus|Acme Campus"],
    "Locations": "Acme Campus",
    "Salary": "Commensurate with experience",
    "ApplyUrl": "https://secure.dc4.pageuppeople.com/apply/822/gateway/default.aspx?c=apply&lJobID=512345"
  }
]
```

## Fetching PageUp Jobs (Python)

```python
import json
import re
from datetime import datetime, timezone

import requests

NET_DATE = re.compile(r"/Date\((-?\d+)\)/")


def parse_net_date(raw):
    """'/Date(1791295200000)/' wraps a millisecond Unix timestamp."""
    m = NET_DATE.search(raw or "")
    return datetime.fromtimestamp(int(m.group(1)) / 1000, tz=timezone.utc) if m else None


def jobs_json_url(host, listing_path):
    path = listing_path.strip("/")
    if host == "careers.pageuppeople.com":
        # 822/cw/en-us/listing -> 822/cw/en-us/jobs.json
        return f"https://{host}/{path.removesuffix('/listing')}/jobs.json"
    # en/listing -> en/listing/jobs.json
    return f"https://{host}/{path}/jobs.json"


def location_text(position):
    labels = []
    for entry in position.get("LocationList") or []:
        # Some tenants repeat the same label on both sides of the pipe.
        parts = list(dict.fromkeys(s.strip() for s in entry.split("|") if s.strip()))
        label = ", ".join(parts)
        if label and label not in labels:
            labels.append(label)
    return ", ".join(labels) or (position.get("Locations") or "").strip()


def fetch_pageup_jobs(host, listing_path):
    res = requests.get(jobs_json_url(host, listing_path), headers={"accept": "*/*"}, timeout=30)
    res.raise_for_status()
    try:
        positions = json.loads(res.text)  # served as text/javascript
    except ValueError:
        raise RuntimeError("Got HTML, not JSON: wrong jobs.json path for this host type")
    if not isinstance(positions, list):
        raise RuntimeError("Unexpected jobs.json shape")

    base = listing_path.strip("/").removesuffix("/listing")
    return [
        {
            "id": str(p["Id"]),
            "title": (p.get("Title") or "").strip(),
            "description_html": p.get("Overview") or "",
            "opened_at": parse_net_date(p.get("OpeningDateUtc")),
            "closes_at": parse_net_date(p.get("ClosingDateUtc")),
            "location": location_text(p),
            "work_type": p.get("WorkType"),
            "job_url": f"https://{host}/{base}/job/{p['Id']}",
        }
        for p in positions
        if p and p.get("Id") is not None
    ]


jobs = fetch_pageup_jobs("careers.pageuppeople.com", "822/cw/en-us/listing")
```

## Fields Worth Knowing

- **`Id`** is the job's numeric ID and a safe dedup key within a tenant.
- **`Title`** and **`Overview`** were present on every job in every tenant we sampled. `Overview` is the complete HTML description, identical to what the job page shows, so there's no detail request to make.
- **`OpeningDateUtc`** and **`ClosingDateUtc`** are .NET JSON dates: a millisecond Unix timestamp wrapped in the literal string `/Date(...)/`. Passing that string to a date parser fails or gives garbage. Pull the number out with a regex, as above. `OpeningDateUtc` is the post date.
- **`LocationList`** looks like a `country|region` pair but isn't reliable. On one tenant every entry was the campus name on both sides of the pipe (`"UMass Lowell|UMass Lowell"`); on others it's a country and a city label. Treat each entry as free-text labels, drop duplicate segments, and fall back to the plain `Locations` string when the list is empty. See the [location normalization guide](https://cleanjobdata.com/guides/job-location-normalization) for turning text like that into structured places.
- **`WorkType`** is free text with no fixed set of values: "Staff Full Time", "Permanent Full-time", "Casual". Map it with loose matching (full time, part time, casual, contract, temporary) and leave anything else unmapped.
- **`Salary`** exists, but on every tenant we checked it held placeholder prose such as "Commensurate" or "See posting section for salary range details." If pay is published, it's usually inside `Overview`. The [salary extraction guide](https://cleanjobdata.com/guides/salary-data-extraction) covers pulling it from text.
- **`ApplyUrl`** points at PageUp's apply gateway on `secure.dc{N}.pageuppeople.com`, not at the job page. There's no job-page URL in the payload at all. Build it from the listing path by swapping the trailing `/listing` for `/job/{Id}`, for example `https://careers.pageuppeople.com/822/cw/en-us/job/512345`.

## Gotchas

**Wrong URLs fail in different ways.** A tenant ID that doesn't exist returns a clean 404. A real tenant ID with the wrong site code or locale returns a 400. The wrong `jobs.json` depth returns 200 with HTML. Handle all three explicitly, and never treat an HTML response as "zero jobs".

**Don't scrape the HTML instead.** PageUp tenants use several structurally different job-page templates. Some have no job-details container, some have no open-date field, and one puts a "How to apply" heading where a naive parser expects the title. The template isn't tied to anything in the URL. `jobs.json` has the same fields on every tenant, so it avoids all of that.

**No remote flag.** Infer remote or hybrid work from the title, location labels or description text.

**No webhooks.** Poll `jobs.json`, compare the `Id` values with your last run, and treat a missing `Id` as closed. Because the file is the complete list, a job's absence is a reliable closure signal, as long as the response parsed correctly.

## When This Is Enough

For one employer, or a few you already know, `jobs.json` is about as easy as ATS data gets: one request, full descriptions, no key.

## When It Isn't

Across many employers you need to know each tenant's ID, site code and locale (or its custom domain), and you still have free-text locations, free-text work types and salary hidden in description text to deal with. PageUp is one of many platforms with its own shape; the [ATS public job postings API list](https://cleanjobdata.com/articles/ats-public-job-postings-api-list) compares 33 of them, and [Teamtailor](https://cleanjobdata.com/articles/how-to-scrape-teamtailor-job-listings) is a similar one-file feed.

## Skip the scraper: get this data from the CleanJobData API

If you'd rather not maintain a PageUp collector, you can query normalized jobs from ATS platforms and company career sites with a single request. The response has structured city/state/country locations, parsed salary when the employer publishes one, a remote type, and company metadata, and you page through results with the cursor in `pagination.next_page`.

```bash
curl "https://api.cleanjobdata.com/jobs?title=lecturer&domain=careers.pageuppeople.com&limit=20" \
  -H "Authorization: Bearer $CLEANJOBDATA_API_KEY"
```

The `domain` filter is an exact match on the host of each job's `application_url`, so it only covers PageUp tenants on the shared `careers.pageuppeople.com` host, not those on a custom domain. Use `company_website_url` or `employer_id` to scope to one employer instead. Coverage depends on which employers are in the dataset, so check that the companies you care about appear before you rely on it.

- [Jobs API reference](https://cleanjobdata.com/docs/api/jobs) for every filter and response field
- [Playground](https://cleanjobdata.com/playground) to run queries without writing code
- [Pricing](https://cleanjobdata.com/pricing) — the Trial is free with no card required

## FAQ

### Does PageUp have a public jobs API?

Not a documented one, but every PageUp career site loads its jobs from a public `jobs.json` file that needs no key. It returns every open job with the full description in one JSON array.

### Why does my PageUp jobs.json request return HTML?

You've probably used the wrong URL shape for the host. Hosted tenants on `careers.pageuppeople.com` use `/{tenantId}/{siteCode}/{locale}/jobs.json`, while custom domains use `/{locale}/listing/jobs.json`, and the wrong one returns the career page HTML with a 200 status.

### How do I parse PageUp's date format?

`OpeningDateUtc` and `ClosingDateUtc` look like `/Date(1791295200000)/`. Extract the number with a regex and treat it as milliseconds since the Unix epoch, in UTC.

### Does PageUp include salary data?

There's a `Salary` field, but in practice it holds placeholder text like "Commensurate" rather than numbers. When an employer publishes pay, it's usually written into the `Overview` description.
