- criticalJWT token extraction fails
- The token is embedded in the career-page HTML and can be wrapped in double quotes, single quotes, backticks, or preceded by '='. Try each delimiter around the eyJ... pattern; if none match, the page markup has changed and you should re-inspect the HTML source.
- highAPI returns 401 Unauthorized
- Paycom JWTs have a roughly two-hour TTL. On a 401, discard the cached token, re-fetch the career page for a fresh one, and retry the request once before treating it as an auth failure.
- highDeleted portal returns a page instead of a 404
- A removed job board still responds HTTP 200 with a 'job portal does not exist' (JobsNotExist) marker rather than a 404. Detect that marker and stop, otherwise you may wrongly flag every job on the board as removed.
- medium403 Forbidden or 429 Too Many Requests
- Space requests (~200ms) and cap concurrency at around 3 parallel detail fetches. A missing or incorrect origin header can also trigger a 403, so keep the origin and referer headers set.
- mediumListing descriptions are truncated
- The search endpoint returns preview text only. Call the job-postings/{jobId} endpoint per job to get the full HTML description and qualifications fields.
- lowJobs span multiple locations
- Parse the semicolon-separated location/locations string and the secondaryLocations array. Some records only expose locations inside the embedded Google Jobs JSON (jobLocation), so fall back to it when the primary fields are empty.
Paycom Jobs API.
Pull structured job data - titles, salary ranges, locations, and full HTML descriptions - from any Paycom-hosted career board through its JWT-authenticated REST API.
What's in every response.
Data fields, real-world applications, and the companies already running on Paycom.
Data fields
- Full HTML Job Descriptions
- Structured Salary Ranges
- Employment Type & Shift
- Multi-Location Listings
- Education & Travel Requirements
- Posted & Closing Dates
Use cases
- 01Job Board Aggregation
- 02Salary Benchmarking
- 03Location-Based Job Search
- 04Hiring Trend Analysis
Trusted by
- Salvation Army of North Texas
- Regency Integrated Health Services
- Cantex
DIY GUIDE
How to scrape Paycom.
Step-by-step guide to extracting jobs from Paycom-powered career pages—endpoints, authentication, and working code.
Step 1: Extract the portal key from the career page URL
import re
career_url = "https://www.paycomonline.net/v4/ats/web.php/portal/A7EA16ADA834B1F889CBB30B17AEB9A9/career-page"
# Extract the 32-character hex portal key
match = re.search(r'portal/([A-F0-9]+)/career-page', career_url)
if match:
portal_key = match.group(1)
print(f"Portal Key: {portal_key}")Step 2: Fetch the career page and extract the JWT token
import re
import requests
career_url = "https://www.paycomonline.net/v4/ats/web.php/portal/A7EA16ADA834B1F889CBB30B17AEB9A9/career-page"
# Fetch the career page HTML
response = requests.get(career_url)
html = response.text
# Extract JWT token from the embedded JavaScript
jwt_match = re.search(r'"(eyJ[A-Za-z0-9_-]+.[A-Za-z0-9_-]+.[A-Za-z0-9_-]+)"', html)
if jwt_match:
jwt_token = jwt_match.group(1)
print(f"JWT Token: {jwt_token[:50]}...")
else:
print("JWT token not found in page")Step 3: Fetch job listings from the search API
import requests
jwt_token = "YOUR_JWT_TOKEN"
url = "https://portal-applicant-tracking.us-cent.paycomonline.net/api/ats/job-posting-previews/search"
headers = {
"authorization": f"Bearer {jwt_token}",
"content-type": "application/json",
"locale": "en-US",
"origin": "https://www.paycomonline.net",
"referer": "https://www.paycomonline.net/",
}
payload = {
"skip": 0,
"take": 100, # Fetch up to 100 jobs per request
"filtersForQuery": {
"distanceFrom": 0,
"workEnvironments": [],
"positionTypes": [],
"educationLevels": [],
"categories": [],
"travelTypes": [],
"shiftTypes": [],
"otherFilters": [],
"keywordSearchText": "",
"location": "",
"sortOption": ""
}
}
response = requests.post(url, headers=headers, json=payload)
data = response.json()
print(f"Total jobs: {data.get('jobPostingPreviewsCount', 0)}")
for job in data.get('jobPostingPreviews', []):
print(f" - [{job['jobId']}] {job['jobTitle']} ({job['locations']})")Step 4: Fetch full job details for each listing
import requests
jwt_token = "YOUR_JWT_TOKEN"
job_id = 216798
portal_key = "A7EA16ADA834B1F889CBB30B17AEB9A9"
url = f"https://portal-applicant-tracking.us-cent.paycomonline.net/api/ats/job-postings/{job_id}"
headers = {
"authorization": f"Bearer {jwt_token}",
"content-type": "application/json",
"locale": "en-US",
"origin": "https://www.paycomonline.net",
"referer": "https://www.paycomonline.net/",
}
response = requests.get(url, headers=headers)
data = response.json()
job = data.get("jobPosting", {})
print({
"id": job.get("jobId"),
"title": job.get("jobTitle"),
"location": job.get("location"),
"position_type": job.get("positionType"),
"remote_type": job.get("remoteType"),
"salary_range": job.get("salaryRange"),
"is_hot_job": job.get("isHotJob"),
"apply_available": job.get("applyAvailable"),
"description_length": len(job.get("description", "")),
})
# Construct the job URL for reference
job_url = f"https://www.paycomonline.net/v4/ats/web.php/portal/{portal_key}/jobs/{job_id}"
print(f"Job URL: {job_url}")Step 5: Handle pagination for large job boards
import requests
import time
jwt_token = "YOUR_JWT_TOKEN"
url = "https://portal-applicant-tracking.us-cent.paycomonline.net/api/ats/job-posting-previews/search"
headers = {
"authorization": f"Bearer {jwt_token}",
"content-type": "application/json",
"locale": "en-US",
"origin": "https://www.paycomonline.net",
"referer": "https://www.paycomonline.net/",
}
all_jobs = []
skip = 0
take = 100
while True:
payload = {
"skip": skip,
"take": take,
"filtersForQuery": {
"distanceFrom": 0,
"workEnvironments": [],
"positionTypes": [],
"educationLevels": [],
"categories": [],
"travelTypes": [],
"shiftTypes": [],
"otherFilters": [],
"keywordSearchText": "",
"location": "",
"sortOption": ""
}
}
response = requests.post(url, headers=headers, json=payload)
data = response.json()
jobs = data.get("jobPostingPreviews", [])
all_jobs.extend(jobs)
total_count = data.get("jobPostingPreviewsCount", 0)
print(f"Fetched {len(jobs)} jobs (total: {len(all_jobs)}/{total_count})")
if len(all_jobs) >= total_count or len(jobs) == 0:
break
skip += take
time.sleep(0.5) # Rate limiting delay
print(f"\nTotal jobs collected: {len(all_jobs)}")Step 6: Handle JWT token expiration and refresh
import re
import requests
def get_fresh_jwt_token(portal_key: str) -> str:
"""Fetch career page and extract a fresh JWT token."""
career_url = f"https://www.paycomonline.net/v4/ats/web.php/portal/{portal_key}/career-page"
response = requests.get(career_url, timeout=15)
response.raise_for_status()
jwt_match = re.search(r'"(eyJ[A-Za-z0-9_-]+.[A-Za-z0-9_-]+.[A-Za-z0-9_-]+)"', response.text)
if not jwt_match:
raise ValueError("JWT token not found in page")
return jwt_match.group(1)
def fetch_jobs_with_retry(portal_key: str, max_retries: int = 2) -> list:
"""Fetch jobs with automatic token refresh on 401 errors."""
jwt_token = get_fresh_jwt_token(portal_key)
url = "https://portal-applicant-tracking.us-cent.paycomonline.net/api/ats/job-posting-previews/search"
headers = {
"authorization": f"Bearer {jwt_token}",
"content-type": "application/json",
"locale": "en-US",
"origin": "https://www.paycomonline.net",
"referer": "https://www.paycomonline.net/",
}
for attempt in range(max_retries + 1):
response = requests.post(url, headers=headers, json={"skip": 0, "take": 100, "filtersForQuery": {}})
if response.status_code == 401:
print(f"Token expired, refreshing (attempt {attempt + 1})...")
jwt_token = get_fresh_jwt_token(portal_key)
headers["authorization"] = f"Bearer {jwt_token}"
continue
response.raise_for_status()
return response.json().get("jobPostingPreviews", [])
raise Exception("Failed to fetch jobs after token refresh")
# Usage
portal_key = "A7EA16ADA834B1F889CBB30B17AEB9A9"
jobs = fetch_jobs_with_retry(portal_key)
print(f"Retrieved {len(jobs)} jobs") Common issues
Best practices
- 1Extract a fresh JWT at the start of each run and refresh it on any 401
- 2Send 'Authorization: Bearer <jwt>' with 'Content-Type: application/json' on every API call
- 3Use take=100 and advance the skip offset to page through job-posting-previews/search
- 4Fetch job-postings/{jobId} per job for full description, qualifications, and salary
- 5Cap concurrency (~3 parallel) with ~200ms delays to avoid 403/429 responses
- 6Detect the 'job portal does not exist' marker before treating jobs as removed
Or skip the complexity
One endpoint. All Paycom jobs. No scraping, no sessions, no maintenance.
Get API accesscURL
curl "https://connect.jobo.world/api/jobs?sources=paycom" \
-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 Paycom
Access Paycom
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