- mediumWrong host resolves to no jobs (app.pinpointhq.com or apply.pinpointhq.com)
- 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.
- highCompany subdomain not found (HTTP 404)
- Confirm the exact tenant slug from the careers page URL. An unknown or misspelled subdomain returns 404 rather than an empty result.
- lowEmpty data array returned
- 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.
- lowMissing or hidden compensation
- 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.
- mediumLocation fields vary or are absent for remote roles
- 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.
- mediumIntermittent 403 or 429 responses
- 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.
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.
What's in every response.
Data fields, real-world applications, and the companies already running on Pinpoint.
Data fields
- Full HTML Job Descriptions
- Compensation Ranges & Currency
- Department & Division
- Employment & Workplace Type
- Location & Postal Code
- Application Deadlines
Use cases
- 01Compensation Benchmarking
- 02Remote & Hybrid Job Feeds
- 03Careers Page Aggregation
- 04Department-Level Hiring Signals
Trusted by
- Tripledot Studios
- SKIMS
- SafetyWing
- Card Factory
DIY GUIDE
How to scrape Pinpoint.
Step-by-step guide to extracting jobs from Pinpoint-powered career pages—endpoints, authentication, and working code.
Step 1: Fetch all job listings from the API
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")Step 2: Parse job details from the response
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"),
})Step 3: Assemble the complete job description
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")Step 4: Resolve the tenant slug and handle errors
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") Common issues
Best practices
- 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
Or skip the complexity
One endpoint. All Pinpoint jobs. No scraping, no sessions, no maintenance.
Get API accesscURL
curl "https://connect.jobo.world/api/jobs?sources=pinpoint" \
-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 Pinpoint
Access Pinpoint
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