# Paylocity Jobs API: Get Paylocity Job Postings as JSON (No Key)

> Paylocity career pages embed every open job as JSON in window.pageData. Find the tenant GUID, parse the list, and read descriptions and salary per job.

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

TL;DR: Paylocity has no standalone public jobs API, but its career page at recruiting.paylocity.com/recruiting/jobs/All/ plus a company GUID embeds every open job as JSON in a window.pageData script, so one keyless request returns the whole list. Each job has a structured address, a publish date and an IsRemote flag, but only a short text snippet. The full description, and salary when the employer publishes it, come from the schema.org JSON-LD on each job's detail page.

Paylocity has no standalone jobs API, but its career page at `https://recruiting.paylocity.com/recruiting/jobs/All/{guid}` embeds every open job as JSON in a `window.pageData` script, so one keyless request gets the whole list. The full description, and salary when the employer publishes it, come from the schema.org JSON-LD on each `https://recruiting.paylocity.com/Recruiting/Jobs/Details/{JobId}` page.

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 Paylocity 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 | `GET https://recruiting.paylocity.com/recruiting/jobs/All/{guid}` (JSON inside HTML) |
| Job page | `GET https://recruiting.paylocity.com/Recruiting/Jobs/Details/{JobId}` (JSON-LD) |
| API key | None |
| Pagination | None; one page holds every job |
| Full description | JSON-LD on each job page; the list has a short snippet |
| Salary field | JSON-LD `baseSalary`, on some jobs |
| Remote | `IsRemote` boolean in `pageData` |

## Finding the Company GUID

Paylocity career pages have URLs like:

```
https://recruiting.paylocity.com/recruiting/jobs/All/0f1e2d3c-0000-0000-0000-000000000000/Acme-Inc
```

The GUID after `/All/` identifies the company. The trailing `Acme-Inc` is cosmetic, and the page loads without it. Every Paylocity company shares the `recruiting.paylocity.com` host, so the GUID is the only value you need. Companies often link to it from their own careers page, so follow the "View openings" or "Apply" link.

## The List Request

```bash
curl "https://recruiting.paylocity.com/recruiting/jobs/All/0f1e2d3c-0000-0000-0000-000000000000"
```

The response is HTML. Somewhere in an inline script is a line that starts `window.pageData = {...};`, and that object holds every job in `Jobs`. One job, trimmed:

```json
{
  "JobId": 4600001,
  "JobTitle": "Field Service Technician",
  "LocationName": "Prairieville Shop",
  "PublishedDate": "2026-10-29T10:30:00-05:00",
  "Description": "POWER UP YOUR CAREER WITH ACME. Acme has been proud to serve",
  "JobLocation": {
    "Name": "Prairieville Shop",
    "Address": "100 Main Street",
    "City": "Prairieville",
    "State": "LA",
    "Zip": "70700",
    "Country": "USA"
  },
  "IsRemote": false
}
```

| Field | What it is |
|---|---|
| `JobId` | Numeric job ID, used in the detail URL |
| `JobTitle` | Job title |
| `PublishedDate` | Publish time with a UTC offset |
| `Description` | A truncated plain-text snippet, not the description |
| `LocationName` | The employer's name for the location, not a place name |
| `JobLocation` | `Address`, `City`, `State`, `Zip`, `Country` |
| `IsRemote` | Boolean remote flag |

`pageData` also has `Departments`, `Locations` and `ModuleTitle`. `ModuleTitle` is the page heading, which on one company was "ALL JOB POSTINGS", so don't use it as the company name.

## Parsing `pageData`

Don't try to match the whole object with a regex. Find the assignment and let a JSON decoder read one object from that position:

```python
import json
import requests

def get_paylocity_jobs(guid: str) -> list[dict]:
    html = requests.get(f"https://recruiting.paylocity.com/recruiting/jobs/All/{guid}", timeout=20).text
    marker = html.find("window.pageData")
    if marker == -1:
        raise ValueError("pageData not found; check the GUID")
    start = html.index("{", marker)
    page_data, _ = json.JSONDecoder().raw_decode(html, start)
    return page_data.get("Jobs") or []
```

In JavaScript, the same idea works with a small brace-counting loop from the first `{` after `window.pageData`, then `JSON.parse` on the slice.

## The Job Page and Its JSON-LD

```bash
curl "https://recruiting.paylocity.com/Recruiting/Jobs/Details/4600001"
```

Each job page has a `script type="application/ld+json"` block with a `JobPosting` object:

- **`description`.** The full description, as HTML.
- **`hiringOrganization.name`.** The company name. This is the reliable source for it.
- **`datePosted`.** A timestamp with an offset.
- **`jobLocation.address`.** `addressLocality`, `addressRegion`, `postalCode`, `addressCountry`.
- **`baseSalary`.** Present on some jobs only: `currency` plus `value.minValue`, `value.maxValue` and `value.unitText`. On one company, one job in five had it.

See the [JobPosting schema guide](https://cleanjobdata.com/articles/jobposting-schema-guide) for the full field list.

```python
import re

LD_JSON = re.compile(r'<script[^>]+type="application/ld\+json"[^>]*>(.*?)</script>', re.S | re.I)

def get_paylocity_detail(job_id: int) -> dict | None:
    html = requests.get(f"https://recruiting.paylocity.com/Recruiting/Jobs/Details/{job_id}", timeout=20).text
    for block in LD_JSON.findall(html):
        try:
            ld = json.loads(block)
        except json.JSONDecodeError:
            continue
        if ld.get("@type") == "JobPosting":
            return ld
    return None

def fetch_paylocity_jobs(guid: str) -> list[dict]:
    jobs = []
    for raw in get_paylocity_jobs(guid):
        ld = get_paylocity_detail(raw["JobId"]) or {}
        loc = raw.get("JobLocation") or {}
        salary = ld.get("baseSalary") or {}
        value = salary.get("value") or {}
        jobs.append({
            "id": str(raw["JobId"]),
            "title": raw.get("JobTitle"),
            "company": (ld.get("hiringOrganization") or {}).get("name"),
            "published": raw.get("PublishedDate"),
            "city": loc.get("City"),
            "state": loc.get("State"),
            "country_alpha3": loc.get("Country"),
            "remote": bool(raw.get("IsRemote")),
            "description_html": ld.get("description") or raw.get("Description"),
            "salary_min": value.get("minValue"),
            "salary_max": value.get("maxValue"),
            "salary_currency": salary.get("currency"),
            "salary_unit": value.get("unitText"),
            "url": f"https://recruiting.paylocity.com/Recruiting/Jobs/Details/{raw['JobId']}",
        })
    return jobs
```

If a detail page fails, the list still gives you the title, location, date and remote flag, so keep the job and fill in the description later.

## Gotchas

**Country is an alpha-3 code.** `JobLocation.Country` is `"USA"`, not `"US"`. Convert it with an ISO 3166 table if you store alpha-2.

**`LocationName` isn't a location.** It's the employer's label for a site, such as a branch or shop name. Build the location from `City` and `State`, and fall back to `LocationName` only when both are empty.

**The list description is a snippet.** `Description` in `pageData` is cut off mid-sentence and has no formatting. Use the JSON-LD `description` for the real text.

**Salary needs a unit.** When `baseSalary` is present, read `unitText` before comparing figures, because hourly and annual values share the same fields. The [salary extraction guide](https://cleanjobdata.com/guides/salary-data-extraction) covers annualizing.

**Two dates.** `PublishedDate` in the list and `datePosted` in the JSON-LD didn't match exactly on the job we checked. Pick one and use it consistently.

**Remote has a flag, hybrid doesn't.** `IsRemote` is a real boolean, but there's no hybrid equivalent, so also check the title and location for "remote" or "hybrid".

**Edits are easy to miss.** If you only fetch job pages for new IDs, you won't see a changed description on a job you already stored.

## Career Page Data vs Paylocity's Official API

Paylocity offers APIs to its clients and integration partners, with credentials tied to the employer's Paylocity account. They cover payroll and HR data and aren't meant for reading other companies' job listings. The approach here uses only what the public career page already sends to every visitor, which also means it's undocumented and can change.

## When This Is Enough

For one company, a single list request plus one job page per new job is about as simple as career-site scraping gets, and you get a remote flag and structured addresses for free.

## When It Isn't

Across many companies you need each one's GUID, a detail request per job for descriptions, and country conversion, before you add other platforms like [BambooHR](https://cleanjobdata.com/articles/how-to-scrape-bamboohr-job-listings) and [iCIMS](https://cleanjobdata.com/articles/how-to-scrape-icims-job-listings). See [which ATS have a public job postings API](https://cleanjobdata.com/articles/ats-public-job-postings-api-list) for how they compare.

CleanJobData reads Paylocity career pages alongside other platforms and returns one schema, with parsed salary when it's published, locations resolved to cities, states and countries, and a remote type. Try it in the [Playground](https://cleanjobdata.com/playground) or see the [API Documentation](https://cleanjobdata.com/docs).

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

If you'd rather not maintain a Paylocity 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=technician&domain=recruiting.paylocity.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 returns jobs whose apply link resolves to `recruiting.paylocity.com`; 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 Paylocity have a public jobs API?

Not a standalone one. The career page at `recruiting.paylocity.com/recruiting/jobs/All/{guid}` embeds every open job as JSON in `window.pageData`, which you can parse without a key. Paylocity's official APIs need client or partner credentials.

### How do I get the full job description from Paylocity?

Fetch `https://recruiting.paylocity.com/Recruiting/Jobs/Details/{JobId}` and read the `description` field of the `JobPosting` JSON-LD block. The `Description` in the list is only a short snippet.

### Does Paylocity include salary?

Sometimes. The job page's JSON-LD has a `baseSalary` with a range, currency and unit when the employer publishes pay, and it's missing on many jobs.

### How do I find a company's Paylocity GUID?

It's the value after `/recruiting/jobs/All/` in the career page URL. Anything after the GUID, such as the company name, is optional.
