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.
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 Paycor.
- Full HTML job descriptions
- Job titles
- Office & remote locations
- Department & team grouping
- Native hex job IDs
- Openings count
- 01Career board aggregation
- 02SMB hiring signals
- 03Local job market tracking
- 04Recruiting data pipelines
How to scrape Paycor.
Step-by-step guide to extracting jobs from Paycor-powered career pages—endpoints, authentication, and working code.
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)import requests
resp = requests.get(
listings_url,
headers={"Accept": "text/html,application/xhtml+xml"},
timeout=15,
)
resp.raise_for_status()
page_html = resp.textfrom 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")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")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"]))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"
)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.
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).
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.
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.
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.
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.
- 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
One endpoint. All Paycor jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=paycor" \
-H "X-Api-Key: YOUR_KEY" Access Paycor
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.