Pinpoint Jobs API.
Pull every open role — full HTML descriptions, pay ranges, department data, and remote/hybrid flags — from any Pinpoint careers site in a single unauthenticated JSON request.
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 Pinpoint.
- Full HTML Job Descriptions
- Compensation Ranges & Currency
- Department & Division
- Employment & Workplace Type
- Location & Postal Code
- Application Deadlines
- 01Compensation Benchmarking
- 02Remote & Hybrid Job Feeds
- 03Careers Page Aggregation
- 04Department-Level Hiring Signals
How to scrape Pinpoint.
Step-by-step guide to extracting jobs from Pinpoint-powered career pages—endpoints, authentication, and working code.
import requests
company_slug = "cardfactory" # from https://{slug}.pinpointhq.com
url = f"https://{company_slug}.pinpointhq.com/postings.json"
headers = {
"Accept": "application/json, text/javascript, */*; q=0.01",
"X-Requested-With": "XMLHttpRequest",
}
response = requests.get(url, headers=headers, timeout=15)
response.raise_for_status()
jobs = response.json().get("data", [])
print(f"Found {len(jobs)} active jobs")def build_location(job: dict) -> str | None:
loc = job.get("location") or {}
# Pinpoint prefers a single display name; fall back to city + province.
if loc.get("name"):
return loc["name"].strip()
parts = [p for p in (loc.get("city"), loc.get("province")) if p]
return ", ".join(parts) or None
def build_salary(job: dict) -> str | None:
# Compensation fields are flat and only meaningful when visible.
if not job.get("compensation_visible"):
return None
low = job.get("compensation_minimum")
high = job.get("compensation_maximum")
if low is None and high is None:
return None
currency = job.get("compensation_currency") or ""
frequency = job.get("compensation_frequency") or ""
return f"{currency} {low}-{high} {frequency}".strip()
for job in jobs:
detail = job.get("job") or {}
print({
"id": job.get("id"),
"title": job.get("title"),
"department": (detail.get("department") or {}).get("name"),
"division": (detail.get("division") or {}).get("name"),
"location": build_location(job),
"workplace_type": job.get("workplace_type_text"),
"employment_type": job.get("employment_type_text"),
"url": job.get("url"),
"salary": build_salary(job),
"deadline": job.get("deadline_at"),
})def build_full_description(job: dict) -> str:
"""Concatenate the HTML sections the way Pinpoint's board renders them."""
parts: list[str] = []
def add(body_key: str, header_key: str) -> None:
body = job.get(body_key)
if not body:
return
header = job.get(header_key)
if header:
parts.append(f"<h3>{header.strip()}</h3>")
parts.append(body)
if job.get("description"):
parts.append(job["description"])
add("key_responsibilities", "key_responsibilities_header")
add("skills_knowledge_expertise", "skills_knowledge_expertise_header")
add("benefits", "benefits_header")
return "\n<br>\n".join(parts)
for job in jobs:
html = build_full_description(job)
print(f"{job.get('title')}: {len(html)} chars")from urllib.parse import urlparse
import requests
from requests.exceptions import RequestException
def resolve_company_slug(board_url: str) -> str | None:
parsed = urlparse(board_url)
host = (parsed.hostname or "").lower()
if "pinpointhq.com" not in host:
return None
# Shared apply host: apply.pinpointhq.com/en/companies/{slug}
if host.startswith(("apply.", "www.")):
parts = parsed.path.strip("/").split("/")
if len(parts) >= 3 and parts[0] == "en" and parts[1] == "companies":
return parts[2]
return None
slug = host.replace(".pinpointhq.com", "")
# app.pinpointhq.com is Pinpoint's product login, not a customer board.
return None if slug in ("", "app") else slug
def fetch_pinpoint_jobs(board_url: str) -> list[dict]:
slug = resolve_company_slug(board_url)
if not slug:
raise ValueError(f"Not a Pinpoint tenant board: {board_url}")
url = f"https://{slug}.pinpointhq.com/postings.json"
headers = {"Accept": "application/json", "X-Requested-With": "XMLHttpRequest"}
try:
resp = requests.get(url, headers=headers, timeout=15)
if resp.status_code == 404:
raise ValueError(f"Pinpoint tenant '{slug}' not found")
if resp.status_code in (403, 429):
raise RuntimeError(f"Soft-blocked '{slug}' (HTTP {resp.status_code}) — back off")
resp.raise_for_status()
return resp.json().get("data", []) # [] is valid: tenant has no open roles
except (RequestException, ValueError) as e:
print(f"Error fetching {slug}: {e}")
return []
jobs = fetch_pinpoint_jobs("https://cardfactory.pinpointhq.com")
print(f"Retrieved {len(jobs)} jobs")app.pinpointhq.com is Pinpoint's shared product/login app, not a tenant board. For the apply host, extract the slug from /en/companies/{slug}; otherwise use the subdomain of {slug}.pinpointhq.com.
Confirm the exact tenant slug from the careers page URL. An unknown or misspelled subdomain returns 404 rather than an empty result.
postings.json returns {"data": []} when a tenant has no open roles (a live and valid state). Treat an empty array as zero jobs, not an error.
Salary lives in flat compensation_* keys and is only meaningful when compensation_visible is true. Check that flag and guard for null compensation_minimum/compensation_maximum before formatting.
location is a top-level object with string fields (name, city, province, postal_code). Prefer name, fall back to city + province, and tolerate nulls for remote or unspecified locations.
No hard rate limit is documented, but aggressive polling can trigger a soft block. Space requests (~5 req/s), and treat 403/429 as a signal to back off and retry with delay.
- 1Fetch everything from the single postings.json endpoint — descriptions ship inline, so no per-job detail calls are needed
- 2Combine description, key_responsibilities, skills_knowledge_expertise, and benefits (with their *_header fields) for the full posting
- 3Always check compensation_visible before reading compensation_minimum / compensation_maximum
- 4Read location.name first, then fall back to city + province; treat null locations as remote or unspecified
- 5Dedupe on the UUID in the posting URL (/postings/{uuid}), falling back to the numeric id when the URL is absent
- 6Throttle to roughly 5 requests/second and back off on 403 or 429 to stay under Pinpoint's soft limits
One endpoint. All Pinpoint jobs. No scraping, no sessions, no maintenance.
Get API accesscurl "https://connect.jobo.world/api/jobs?sources=pinpoint" \
-H "X-Api-Key: YOUR_KEY" Access Pinpoint
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.