- 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.
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.
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
- 01Salary Benchmarking
- 02Local Job Market Analysis
- 03Recruitment Data Aggregation
- 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.
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}")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")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))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))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
Best practices
- 1Read window.pageData for listings — it returns the full board as JSON in one GET, with no pagination to handle
- 2Append ?listingId={tenantGuid} to detail URLs so the tenant stays resolvable
- 3Parse the JSON-LD JobPosting block on detail pages for salary, dates, employment type, and company name
- 4Treat a 'Job Not Found' body as a removed role even when the response is HTTP 200
- 5Keep concurrency low (~3 parallel fetches) with a short delay to avoid 403/429 blocks
- 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 accesscURL
curl "https://connect.jobo.world/api/jobs?sources=paylocity" \
-H "X-Api-Key: YOUR_KEY"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.
Ready to integrate Access Paylocity
Access Paylocity
job data today.
One API call. Structured data. No scraping infrastructure to build or maintain — start with the $5 free starting balance and scale as you grow.
99.9%API uptime
<200msAvg response
50M+Jobs processed