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.
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.
- Structured JSON Job Listings
- Pay Ranges & Currency
- City, State & Postal Location
- Department & Remote Flags
- Full Job Descriptions
- Published & Closing Dates
- 01Salary Benchmarking
- 02Local Job Market Analysis
- 03Recruitment Data Aggregation
- 04Hiring Trend Monitoring
How to scrape Paylocity.
Step-by-step guide to extracting jobs from Paylocity-powered career pages—endpoints, authentication, and working code.
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}")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")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))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))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 requestsJob 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.
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.
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.
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.
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.
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.
- 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
One endpoint. All Paylocity jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=paylocity" \
-H "X-Api-Key: YOUR_KEY" 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.