- highNo job links match, but the board is not actually empty
- Template drift, a truncated body, or an interstitial can all produce zero matches. Only treat the board as empty when the explicit gnewtonNoActiveJobs sentinel is present; otherwise retry rather than persisting a zero-job snapshot.
- mediumBoard renders as JavaScript arrays instead of anchors
- Tenants on the sort-by-department view emit jobName[dep][i] and jobId[dep][i] arrays rather than <a> tags. Try anchor parsing (Template A) first, then fall back to pairing the JS arrays by department and index (Template B).
- mediumQuery strings are sometimes HTML-escaped (& vs &)
- Some tenants escape the href query string and some don't. HTML-unescape each href before parsing and match both & and & separators so clientId and id extraction is reliable.
- mediumDetail page marked 'this job is no longer active'
- The description table is missing on inactive landings. Detect the inactive sentinel and skip the record instead of storing empty or stale HTML as a valid job.
- lowDuplicate or cross-client anchors on the page
- Dedupe listings by their native hex job id and skip any anchor whose clientId does not match the tenant you are scraping, so shared or template rows don't create phantom jobs.
- mediumRequests blocked or rate limited (403 / 429)
- Paycor throttles aggressive scraping. Space requests ~250ms apart, cap concurrent detail fetches at ~3, and retry transient network/5xx failures a couple of times with a short backoff.
Paycor Jobs API.
Pull structured job titles, locations, and full descriptions from company career boards hosted on Paycor's recruiting (gnewton) platform at recruitingbypaycor.com.
What's in every response.
Data fields, real-world applications, and the companies already running on Paycor.
Data fields
- Full HTML job descriptions
- Job titles
- Office & remote locations
- Department & team grouping
- Native hex job IDs
- Openings count
Use cases
- 01Career board aggregation
- 02SMB hiring signals
- 03Local job market tracking
- 04Recruiting data pipelines
DIY GUIDE
How to scrape Paycor.
Step-by-step guide to extracting jobs from Paycor-powered career pages—endpoints, authentication, and working code.
Step 1: Validate the URL and extract the clientId
from urllib.parse import urlparse, parse_qs
import re
url = "https://recruitingbypaycor.com/career/CareerHome.action?clientId=8a7883c67239c84401727af009b53231"
parsed = urlparse(url)
assert parsed.netloc == "recruitingbypaycor.com", "Not a Paycor recruiting host"
client_id = (parse_qs(parsed.query).get("clientId") or [""])[0]
assert re.fullmatch(r"[0-9a-fA-F]{16,}", client_id), "Missing or malformed clientId"
listings_url = f"https://recruitingbypaycor.com/career/CareerHome.action?clientId={client_id}"
print(listings_url)Step 2: Fetch the career board HTML
import requests
resp = requests.get(
listings_url,
headers={"Accept": "text/html,application/xhtml+xml"},
timeout=15,
)
resp.raise_for_status()
page_html = resp.textStep 3: Parse Template A (server-rendered anchors)
from bs4 import BeautifulSoup
from urllib.parse import urlparse, parse_qs
import html
soup = BeautifulSoup(page_html, "html.parser")
listings = []
seen = set()
for a in soup.select('a[href*="JobIntroduction.action"]'):
href = html.unescape(a.get("href", ""))
qs = parse_qs(urlparse(href).query)
href_client = (qs.get("clientId") or [""])[0]
job_id = (qs.get("id") or [""])[0]
# Skip cross-client links and repeated job IDs.
if href_client.lower() != client_id.lower() or not job_id or job_id in seen:
continue
seen.add(job_id)
listings.append({
"external_id": f"{client_id}:{job_id}",
"job_id": job_id,
"title": a.get_text(strip=True),
"detail_url": (
"https://recruitingbypaycor.com/career/JobIntroduction.action"
f"?clientId={client_id}&id={job_id}&source=&lang=en"
),
})
print(f"Template A found {len(listings)} jobs")Step 4: Fall back to Template B (JavaScript arrays)
import re
if not listings:
names = {}
for dep, idx, title in re.findall(
r'jobName\["([0-9a-fA-F]{16,})"\]\[(\d+)\s*-\s*1\]\s*=\s*"([^"]*)"',
page_html,
):
names[(dep, idx)] = title
for dep, idx, job_id in re.findall(
r'jobId\["([0-9a-fA-F]{16,})"\]\[(\d+)\s*-\s*1\]\s*=\s*"([0-9a-fA-F]{16,})"',
page_html,
):
if any(l["job_id"] == job_id for l in listings):
continue
listings.append({
"external_id": f"{client_id}:{job_id}",
"job_id": job_id,
"title": names.get((dep, idx), ""),
"detail_url": (
"https://recruitingbypaycor.com/career/JobIntroduction.action"
f"?clientId={client_id}&id={job_id}&source=&lang=en"
),
})
print(f"Template B found {len(listings)} jobs")Step 5: Fetch full job details
import re
import requests
from bs4 import BeautifulSoup
def fetch_detail(detail_url: str) -> dict | None:
resp = requests.get(
detail_url,
headers={"Accept": "text/html,application/xhtml+xml"},
timeout=15,
)
resp.raise_for_status()
# Skip stale pages that no longer describe a live job.
if re.search(r"this\s+job\s+is\s+no\s+longer\s+active", resp.text, re.I):
return None
soup = BeautifulSoup(resp.text, "html.parser")
def cell(selector: str, strip_label: bool = False) -> str | None:
el = soup.select_one(selector)
if el is None:
return None
text = el.get_text(" ", strip=True)
if strip_label and ":" in text:
text = text.split(":", 1)[1].strip()
return text or None
desc_el = soup.select_one("#gnewtonJobDescriptionText")
return {
"title": cell("#gnewtonJobPosition", strip_label=True),
"description_html": desc_el.decode_contents().strip() if desc_el else None,
"locations": [el.get_text(" ", strip=True) for el in soup.select("#gnewtonJobLocationInfo")],
"client_job_id": cell("#gnewtonJobID", strip_label=True),
"openings": cell("#gnewtonJobOpening", strip_label=True),
}
for job in listings[:3]:
print(fetch_detail(job["detail_url"]))Step 6: Distinguish an empty board from a parse failure
import re
if not listings:
if re.search(r'id\s*=\s*["\']gnewtonNoActiveJobs["\']', page_html, re.I):
print("Board explicitly reports no active jobs — safe to record as empty")
else:
raise RuntimeError(
"No jobs parsed and no empty-state sentinel found — "
"treat as incomplete and retry, do not record zero jobs"
) Common issues
Best practices
- 1Validate the host is recruitingbypaycor.com and the clientId is 16+ hex chars before fetching
- 2Try Template A anchors first, then fall back to the Template B JavaScript arrays
- 3HTML-unescape hrefs and match both & and & separators when extracting IDs
- 4Only record an empty board when the gnewtonNoActiveJobs sentinel is present
- 5Dedupe listings by native hex job id and drop cross-client anchors
- 6Fetch detail pages for full descriptions, locations, and openings count; keep ~250ms between requests
Or skip the complexity
One endpoint. All Paycor jobs. No scraping, no sessions, no maintenance.
Get API accesscURL
curl "https://connect.jobo.world/api/jobs?sources=paycor" \
-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 Paycor
Access Paycor
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