Pinpoint Jobs API: Get Pinpoint Job Listings as JSON
TL;DR
Every Pinpoint career site serves https://{company}.pinpointhq.com/postings.json, a public JSON list of all open postings with descriptions, workplace type and structured location, in one request with no API key. Salary is in compensation_minimum and compensation_maximum, but only when compensation_visible is true. The JSON has no posting date, so the date comes from the same site's jobs.rss feed, matched by requisition ID.
On this page
To get a company's jobs from Pinpoint, call https://{company}.pinpointhq.com/postings.json: it returns every open posting, with its description, in one JSON response and needs no API key. The one thing it lacks is a posting date, which you get from the same site's jobs.rss feed.
That's the DIY route, and it works for a handful of companies you already know. If you'd rather not build and maintain a Pinpoint scraper, CleanJobData already ingests ATS platforms and company career sites and serves the jobs normalized through one REST API — see the Jobs API docs.
At a Glance
| Endpoint | GET https://{company}.pinpointhq.com/postings.json |
| API key | None |
| Pagination | None: one response holds every posting |
| Full description | Yes, in the list, split into sections |
| Salary field | compensation_minimum / compensation_maximum, when compensation_visible is true |
| Remote | workplace_type (remote, hybrid or on-site) |
Finding the Company Subdomain
Pinpoint career sites live at https://{company}.pinpointhq.com, so for acme.pinpointhq.com the subdomain is acme. Job pages look like https://acme.pinpointhq.com/en/postings/{uuid}. If a company shows its Pinpoint jobs on its own site, look for links or requests to pinpointhq.com in the page source or Network tab.
The List Request
curl "https://acme.pinpointhq.com/postings.json"The response is an object with a data array (one posting shown, trimmed):
{
"data": [
{
"id": "524420",
"title": "QA Engineer",
"url": "https://acme.pinpointhq.com/en/postings/39a39239-f6ec-4a8d-8013-f5d760fde0a1",
"path": "/en/postings/39a39239-f6ec-4a8d-8013-f5d760fde0a1",
"description": "<div><strong>About us:</strong></div>...",
"key_responsibilities": "<ul><li>...</li></ul>",
"key_responsibilities_header": "Key Responsibilities",
"skills_knowledge_expertise": "",
"skills_knowledge_expertise_header": " ",
"benefits": null,
"benefits_header": "Job Benefits",
"employment_type": "full_time",
"employment_type_text": "Full Time",
"workplace_type": "hybrid",
"workplace_type_text": "Hybrid",
"compensation_visible": false,
"compensation_minimum": null,
"compensation_maximum": null,
"compensation_currency": null,
"compensation_frequency": null,
"deadline_at": null,
"reporting_to": "",
"job": {
"id": "529976",
"requisition_id": "0048",
"department": { "id": "77352", "name": "Engineering" }
},
"location": {
"id": "86155",
"city": "Bengaluru",
"name": "Bangalore - India",
"postal_code": "560102",
"province": "Karnataka",
"street_address": "HSR Layout"
}
}
]
}Response Fields
| Field | What it holds |
|---|---|
id | Numeric posting ID (see the ID gotcha below) |
url, path | Public posting page; the URL contains a stable UUID |
title | Job title |
description | Main HTML description |
key_responsibilities, skills_knowledge_expertise, benefits | Extra HTML sections, each with a *_header title; often empty or null |
employment_type, employment_type_text | Code and label, e.g. full_time / "Full Time" |
workplace_type, workplace_type_text | Code and label, e.g. hybrid / "Hybrid" |
compensation_* | Pay range, currency and frequency, gated by compensation_visible |
job.id | Requisition ID, shared by every posting for the same role |
job.department.name | Department label |
location | city, province, postal_code, street_address and a display name |
deadline_at | Closing date, if set |
Getting the Posting Date
postings.json has no date field. The same site publishes an RSS feed that does:
curl "https://acme.pinpointhq.com/jobs.rss"Each <item> has a <pubDate> and a <link> that contains /jobs/{id}. That number is the requisition ID, job.id in the JSON, not the posting ID. Build a map from requisition ID to pubDate and look each posting up through posting.job.id. Postings that share a requisition share its date.
Fetching Jobs (JavaScript)
const SECTIONS = [
["description", null],
["key_responsibilities", "key_responsibilities_header"],
["skills_knowledge_expertise", "skills_knowledge_expertise_header"],
["benefits", "benefits_header"],
];
function datesByRequisition(rssXml) {
const map = new Map();
for (const item of rssXml.match(/<item>[\s\S]*?<\/item>/g) ?? []) {
const link = item.match(/<link>([\s\S]*?)<\/link>/)?.[1] ?? "";
const id = link.match(/\/jobs\/(\d+)/)?.[1];
const pubDate = item.match(/<pubDate>([\s\S]*?)<\/pubDate>/)?.[1];
if (id && pubDate) map.set(id, new Date(pubDate.trim()));
}
return map;
}
export async function fetchPinpointJobs(company) {
const base = `https://${company}.pinpointhq.com`;
const [postingsRes, rssRes] = await Promise.all([
fetch(`${base}/postings.json`),
fetch(`${base}/jobs.rss`),
]);
if (!postingsRes.ok) throw new Error(`Pinpoint ${postingsRes.status}`);
const { data = [] } = await postingsRes.json();
const dates = rssRes.ok ? datesByRequisition(await rssRes.text()) : new Map();
return data.map((p) => {
const uuid = p.url?.match(/\/postings\/([0-9a-f-]{36})/i)?.[1];
const html = SECTIONS
.filter(([key]) => p[key])
.map(([key, header]) => (header && p[header]?.trim() ? `<h3>${p[header]}</h3>` : "") + p[key])
.join("\n");
const showPay = p.compensation_visible === true;
return {
id: uuid ?? String(p.id),
title: p.title,
url: p.url,
published: dates.get(String(p.job?.id)) ?? null,
location: p.location?.city
? [p.location.city, p.location.province].filter(Boolean).join(", ")
: p.location?.name ?? "",
workplaceType: p.workplace_type,
employmentType: p.employment_type_text,
department: p.job?.department?.name,
salaryMin: showPay ? p.compensation_minimum : null,
salaryMax: showPay ? p.compensation_maximum : null,
salaryCurrency: showPay ? p.compensation_currency : null,
salaryFrequency: showPay ? p.compensation_frequency : null,
html,
};
});
}The same in Python is two requests.get calls, a regex over the RSS and a loop over data.
Gotchas
Use the URL's UUID as the job ID. Pinpoint has reassigned numeric posting IDs to different postings in the past, after a platform migration. The UUID in url (/postings/{uuid}) stays with the posting, and old numeric job URLs redirect to it. Key your records on the UUID and fall back to id only when a URL has no UUID.
A requisition can have several postings. One job.id can carry multiple postings, for example the same role in different locations or re-advertised under a new title. Each posting is what candidates see, so treat postings, not requisitions, as your jobs.
Only trust pay when compensation_visible is true. When it's false, the four compensation fields are null, even if the employer entered figures internally. When it's true, check compensation_frequency before comparing figures, since the unit isn't always annual. See the salary extraction guide.
Location city can be empty. Some postings leave city blank while location.name still holds a readable place such as "Bangalore - India". Fall back to name and parse it, and note that its city name can differ from city on the same posting (Bangalore vs Bengaluru). The location normalization guide covers this.
Section headers can be blank. A *_header can be a single space, and a section can be an empty string rather than missing. Only render sections that have content.
Old requisitions stay open. Evergreen roles can carry an RSS date that's years old. Check the date before you treat a posting as fresh.
Public Endpoint vs Pinpoint's Official API
Pinpoint has an official API for its customers that needs an API key from the employer's own Pinpoint account and covers that company's recruiting data. For published postings from companies you don't work with, the public postings.json and jobs.rss on each career site are what you use.
When This Is Enough
For one company, Pinpoint is one of the easiest platforms there is: two requests, every posting, descriptions and pay included.
When It Isn't
Across many companies and platforms you end up maintaining a different collector for each, with different ID rules, date sources and salary formats. The ATS public job postings API list shows how they compare.
CleanJobData reads Pinpoint career sites alongside other platforms and returns one schema, with locations resolved to cities, states and countries, a remote type, an employment type, and salary converted to a consistent range when the employer shows it. Try it in the Playground or see the API Documentation.
Skip the scraper: get this data from the CleanJobData API
If you'd rather not maintain a Pinpoint collector, you can query normalized jobs from ATS platforms and company career sites with a single request. The response has structured city/state/country locations, parsed salary when the employer publishes one, a remote type, and company metadata, and you page through results with the cursor in pagination.next_page.
curl "https://api.cleanjobdata.com/jobs?title=engineer&domain=acme.pinpointhq.com&limit=20" \
-H "Authorization: Bearer $CLEANJOBDATA_API_KEY"The domain filter is an exact match on the host of each job's application_url. Pinpoint jobs apply on each company's own pinpointhq.com subdomain, so this scopes the query to one employer; use company_website_url or employer_id as alternatives. Coverage depends on which employers are in the dataset, so check that the companies you care about appear before you rely on it.
- Jobs API reference for every filter and response field
- Playground to run queries without writing code
- Pricing — the Trial is free with no card required
FAQ
Does Pinpoint have a public jobs API?
Yes. Every Pinpoint career site serves https://{company}.pinpointhq.com/postings.json, which returns all open postings with descriptions in one request and needs no API key.
Why doesn't Pinpoint's postings.json have a date?
The JSON simply has no posting date field. The same career site's jobs.rss feed has a pubDate per requisition, which you can match to each posting through job.id.
Does Pinpoint include salary?
Yes, when the employer chooses to show it. compensation_minimum, compensation_maximum, compensation_currency and compensation_frequency are filled in only when compensation_visible is true, and they're null otherwise.
What should I use as a unique ID for Pinpoint jobs?
Use the UUID in the posting's url, from the /postings/{uuid} path. Numeric posting IDs have been reassigned to different postings in the past, while the UUID stays with the same posting.