All platforms

Paylocity Jobs API.

Pull an entire recruiting board in a single request: every posting ships as structured JSON embedded in the page, so there are no pages to crawl and no API keys to manage.

Get API access
Paylocity
Live
50K+jobs indexed monthly
<3haverage discovery time
1hrefresh interval
Developer tools

Try the API.

Test Jobs and Feed endpoints against https://connect.jobo.world with live request/response examples, then copy ready-to-use curl commands.

What's in every response.

Data fields, real-world applications, and the companies already running on Paylocity.

Data fields
  • Structured JSON Job Listings
  • Pay Ranges & Currency
  • City, State & Postal Location
  • Department & Remote Flags
  • Full Job Descriptions
  • Published & Closing Dates
Use cases
  1. 01Salary Benchmarking
  2. 02Local Job Market Analysis
  3. 03Recruitment Data Aggregation
  4. 04Hiring Trend Monitoring
DIY GUIDE

How to scrape Paylocity.

Step-by-step guide to extracting jobs from Paylocity-powered career pages—endpoints, authentication, and working code.

HTMLbeginnerUndocumented; throttle to ~3 concurrent detail fetches and expect 403/429 if aggressiveNo auth

Identify the tenant GUID and URL structure

Every Paylocity board is keyed by a company-specific tenant GUID. The listings page follows /recruiting/jobs/All/{tenantId}/Available-Positions, and each job maps to /Recruiting/Jobs/Details/{jobId}. Pull the GUID out of the board URL before scraping.

Step 1: Identify the tenant GUID and URL structure
import re

# Paylocity URL patterns
listing_url = "https://recruiting.paylocity.com/recruiting/jobs/All/b181f77f-0432-453f-b229-869d786bb46c/Available-Positions"
detail_url = "https://recruiting.paylocity.com/Recruiting/Jobs/Details/3898273"

# Extract tenant GUID from URL
guid_pattern = r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
guid_match = re.search(guid_pattern, listing_url)
tenant_guid = guid_match.group(0) if guid_match else None
print(f"Tenant GUID: {tenant_guid}")

Fetch the board and extract window.pageData

Paylocity renders the complete listings array as JSON in a window.pageData script block. This is the recommended path — it hands you structured data without HTML parsing. Extract the JSON with a regex against the script tag.

Step 2: Fetch the board and extract window.pageData
import requests
import re
import json

tenant_guid = "b181f77f-0432-453f-b229-869d786bb46c"
listing_url = f"https://recruiting.paylocity.com/recruiting/jobs/All/{tenant_guid}/Available-Positions"

response = requests.get(listing_url, timeout=30)
html = response.text

# Extract window.pageData JSON from the HTML
pattern = r'window\.pageData\s*=\s*(\{.*?\});\s*</script>'
match = re.search(pattern, html, re.DOTALL)

if match:
    json_str = match.group(1)
    page_data = json.loads(json_str)
    jobs = page_data.get("Jobs", [])
    print(f"Found {len(jobs)} jobs in embedded JSON")
else:
    print("No embedded pageData found - may need HTML parsing fallback")

Parse structured jobs from window.pageData

Each entry in the Jobs array exposes JobId, JobTitle, LocationName, PublishedDate, HiringDepartment, IsRemote, and a nested JobLocation with the full address, city, state, ZIP, and country.

Step 3: Parse structured jobs from window.pageData
import json

# Assuming page_data was extracted from the previous step
jobs = page_data.get("Jobs", [])

for job in jobs[:3]:  # Show first 3 jobs
    job_info = {
        "job_id": job.get("JobId"),
        "title": job.get("JobTitle"),
        "location": job.get("LocationName"),
        "is_remote": job.get("IsRemote", False),
        "published_date": job.get("PublishedDate"),
        "department": job.get("HiringDepartment"),
    }

    # Full location details
    location = job.get("JobLocation", {})
    if location:
        job_info["address"] = location.get("Address")
        job_info["city"] = location.get("City")
        job_info["state"] = location.get("State")
        job_info["zip"] = location.get("Zip")
        job_info["country"] = location.get("Country")

    print(json.dumps(job_info, indent=2))

Fetch full job details from the detail page

The listing JSON leaves Description empty, so fetch each job's server-rendered detail page for the full write-up. Pass the tenant GUID as ?listingId so the page stays tenant-resolvable, read the body fields from their CSS selectors, and pull salary, dates, and company name from the embedded JSON-LD JobPosting block.

Step 4: Fetch full job details from the detail page
import requests
import json
from bs4 import BeautifulSoup

job_id = 3898273
tenant_guid = "b181f77f-0432-453f-b229-869d786bb46c"

# Detail URLs carry the tenant GUID in the listingId query param
detail_url = f"https://recruiting.paylocity.com/Recruiting/Jobs/Details/{job_id}?listingId={tenant_guid}"

response = requests.get(detail_url, timeout=30)
html = response.text

# Removed roles render "Job Not Found" in the body without a 404
if "Job Not Found" in html:
    raise ValueError(f"Job {job_id} has been removed")

soup = BeautifulSoup(html, "html.parser")

def text_or_none(selector):
    el = soup.select_one(selector)
    return el.get_text(strip=True) if el else None

job = {
    "job_id": job_id,
    "title": text_or_none("span.job-preview-title") or text_or_none("span.breadcrumb-title"),
    "description": text_or_none("div.job-preview-details"),
    "location": text_or_none("div.preview-location"),
    "apply_url": detail_url,
}

# Salary, dates, employment type, and company name live in the JSON-LD JobPosting
for script in soup.find_all("script", type="application/ld+json"):
    try:
        data = json.loads(script.string or "{}")
    except json.JSONDecodeError:
        continue
    if isinstance(data, dict) and data.get("@type") == "JobPosting":
        job["date_posted"] = data.get("datePosted")
        job["valid_through"] = data.get("validThrough")
        job["employment_type"] = data.get("employmentType")
        job["company_name"] = (data.get("hiringOrganization") or {}).get("name")
        base_salary = data.get("baseSalary") or {}
        salary_value = base_salary.get("value") or {}
        if salary_value:
            job["salary_min"] = salary_value.get("minValue")
            job["salary_max"] = salary_value.get("maxValue")
            job["salary_currency"] = base_salary.get("currency", "USD")
        if not job["description"]:
            job["description"] = data.get("description")
        break

print(json.dumps(job, indent=2))

Add retries and conservative throttling

Paylocity publishes no rate limit and returns 403/429 when pushed. Cap detail fetches at a few concurrent requests, add a short delay between calls, and back off exponentially on failure.

Step 5: Add retries and conservative throttling
import requests
import time
import json
import re

def fetch_paylocity_jobs(tenant_guid: str, max_retries: int = 3) -> list:
    listing_url = f"https://recruiting.paylocity.com/recruiting/jobs/All/{tenant_guid}/Available-Positions"

    for attempt in range(max_retries):
        try:
            response = requests.get(
                listing_url,
                timeout=30,
                headers={"User-Agent": "Mozilla/5.0 (compatible; JobBot/1.0)"}
            )
            response.raise_for_status()

            # Extract embedded JSON
            pattern = r'window\.pageData\s*=\s*(\{.*?\});\s*</script>'
            match = re.search(pattern, response.text, re.DOTALL)

            if match:
                page_data = json.loads(match.group(1))
                return page_data.get("Jobs", [])
            return []

        except requests.RequestException as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            if attempt < max_retries - 1:
                time.sleep(2 ** attempt)  # Exponential backoff
            else:
                raise

    return []

# Usage with rate limiting
tenant_guids = ["b181f77f-0432-453f-b229-869d786bb46c"]
for guid in tenant_guids:
    jobs = fetch_paylocity_jobs(guid)
    print(f"Retrieved {len(jobs)} jobs for tenant {guid}")
    time.sleep(1.5)  # Conservative delay between requests
Common issues
highNo way to discover a company's tenant GUID from a job link

Job detail URLs contain only a numeric job ID; a bare /Details/{id} URL is tenantless and cannot be resolved. Discover board GUIDs through search engines (site:recruiting.paylocity.com), third-party databases like TheirStack, or existing records, and append ?listingId={tenantGuid} to detail URLs to keep them resolvable.

mediumRemoved jobs return HTTP 200 with a 'Job Not Found' body

A pulled role does not always return a 404 — Paylocity often serves a 200 whose HTML contains 'Job Not Found'. Check the body for that marker and treat the job as removed rather than trusting the status code alone.

mediumwindow.pageData JSON extraction fails

The regex pattern can vary across Paylocity template versions. Match window.pageData = ({.*?}); with a non-greedy, dot-all pattern, and fall back to parsing job links from the HTML if extraction returns nothing.

mediumDescriptions and salary are missing from the listings JSON

The window.pageData Jobs array ships an empty Description and no pay data. Fetch each /Recruiting/Jobs/Details/{jobId} page for the full description, and read salary, dates, and company name from that page's JSON-LD JobPosting block.

mediumRequests blocked with 403 or 429

Paylocity does not document limits and blocks aggressive clients with 403 or 429. Keep concurrency low (about three parallel detail fetches), add a short delay between requests, and back off exponentially on failures.

highInvalid or expired tenant GUID returns 404

GUIDs change when a company migrates or reconfigures its Paylocity account, after which the board 404s. Verify a GUID is current by confirming the listing URL returns HTTP 200 before scraping it.

Best practices
  1. 1Read window.pageData for listings — it returns the full board as JSON in one GET, with no pagination to handle
  2. 2Append ?listingId={tenantGuid} to detail URLs so the tenant stays resolvable
  3. 3Parse the JSON-LD JobPosting block on detail pages for salary, dates, employment type, and company name
  4. 4Treat a 'Job Not Found' body as a removed role even when the response is HTTP 200
  5. 5Keep concurrency low (~3 parallel fetches) with a short delay to avoid 403/429 blocks
  6. 6Cache tenant GUIDs — there is no public Paylocity company directory to rediscover them
Or skip the complexity

One endpoint. All Paylocity jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=paylocity" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

Access Paylocity
job data today.

One API call. Structured data. No scraping infrastructure to build or maintain — start with the free tier and scale as you grow.

99.9%API uptime
<200msAvg response
50M+Jobs processed