Careerpuck Jobs API.
Pull every active role from a single unauthenticated JSON call per company job board, with full HTML descriptions, departments, work type, and the underlying source ATS all returned in one response.
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 Careerpuck.
- Full HTML Descriptions
- Department & Team
- Work & Workplace Type
- Source ATS Platform & ID
- Direct Apply URLs
- Posted Date
- 01Job Board Aggregation
- 02Source-ATS Attribution
- 03Multi-Company Job Monitoring
- 04Careers Page Enrichment
How to scrape Careerpuck.
Step-by-step guide to extracting jobs from Careerpuck-powered career pages—endpoints, authentication, and working code.
import requests
import re
sitemap_url = "https://app.careerpuck.com/sitemap.xml"
response = requests.get(sitemap_url)
pattern = r'https://app\.careerpuck\.com/job-board/([^<]+)'
companies = re.findall(pattern, response.text)
print(f"Found {len(companies)} company job boards")
print(f"Sample companies: {companies[:5]}")import requests
company = "earnest"
url = f"https://api.careerpuck.com/v1/public/job-boards/{company}"
headers = {
"accept": "*/*",
"origin": "https://app.careerpuck.com",
"referer": "https://app.careerpuck.com/"
}
response = requests.get(url, headers=headers)
data = response.json()
jobs = data.get("jobs", [])
print(f"Found {len(jobs)} active jobs")import html
for job in jobs:
# HTML-decode the content field (unescape twice if double-encoded)
description = html.unescape(job.get("content", ""))
print({
"id": job.get("atsSourceId") or job.get("permalink"),
"title": job.get("title"),
"location": job.get("location"),
"department": job.get("department"),
"team": job.get("team"),
"work_type": job.get("workType"),
"workplace_type": job.get("workplaceType"),
"url": job.get("publicUrl"),
"apply_url": job.get("applyUrl"),
"posted_at": job.get("postedAt"),
"source_platform": job.get("atsSourcePlatform"),
"description_html": description[:200],
})employer = data.get("employerProfile", {})
departments = data.get("departments", [])
print({
"company_name": employer.get("name") or employer.get("companyName"),
"website": employer.get("website"),
"career_page_url": employer.get("careerPageUrl"),
"departments": departments,
})import requests
import time
def fetch_careerpuck_jobs(company: str) -> list:
url = f"https://api.careerpuck.com/v1/public/job-boards/{company}"
headers = {
"accept": "*/*",
"origin": "https://app.careerpuck.com",
"referer": "https://app.careerpuck.com/"
}
try:
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 404:
print(f"Company '{company}' not found")
return []
if response.status_code in (403, 429):
print(f"Throttled or blocked ({response.status_code}); backing off")
time.sleep(5)
return []
response.raise_for_status()
data = response.json()
# Keep only published roles
return [j for j in data.get("jobs", []) if j.get("status") == "public"]
except requests.exceptions.RequestException as e:
print(f"Request failed: {e}")
return []
jobs = fetch_careerpuck_jobs("earnest")The 'content' field arrives HTML-encoded (e.g. <div>, occasionally double-encoded as &lt;div&gt;). Unescape it — twice if needed — before parsing or displaying.
Send 'origin: https://app.careerpuck.com' and 'referer: https://app.careerpuck.com/' on every request. Calls without these headers may be rejected.
Only jobs with status == 'public' are live listings. Filter out any other status so drafts or closed roles do not leak into your dataset.
Verify the company slug taken from the job-board URL. The slug in 'app.careerpuck.com/job-board/{company}' must match the API path parameter exactly.
Some boards have no active postings. Check whether 'jobs' is empty and handle it gracefully rather than treating it as an error.
CareerPuck aggregates other ATSes, so 'atsSourcePlatform' names the origin (greenhouse, lever, etc.) and 'applyUrl' points at that ATS. Use 'atsSourceId' as the stable ID and route applicants via 'applyUrl'.
- 1Use the listings endpoint for complete data — the per-job details API is only needed for extras like jobClips
- 2Always HTML-decode the 'content' field (twice if double-encoded) before parsing or display
- 3Send origin and referer headers set to app.careerpuck.com on every request
- 4Keep only jobs where status == 'public' and de-duplicate by atsSourceId
- 5Space requests (~200ms) and back off when you see 403 or 429
- 6Cache per company and refresh daily — boards change slowly and return all jobs at once
One endpoint. All Careerpuck jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=careerpuck" \
-H "X-Api-Key: YOUR_KEY" Access Careerpuck
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.