Pinpoint Jobs Scraper — Company Job Boards avatar

Pinpoint Jobs Scraper — Company Job Boards

Pricing

from $1.45 / 1,000 posting saveds

Go to Apify Store
Pinpoint Jobs Scraper — Company Job Boards

Pinpoint Jobs Scraper — Company Job Boards

Every open role on any company's Pinpoint careers site as structured data: title, department, location with country, employment type, workplace type, pay range where shown, posted and closing dates, full text on request. Incremental mode charges only for what changed. No personal data.

Pricing

from $1.45 / 1,000 posting saveds

Rating

0.0

(0)

Developer

Adderley Data

Adderley Data

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

4 hours ago

Last modified

Share

What does Pinpoint Jobs Scraper do?

Pinpoint Jobs Scraper reads every open role on any company's public Pinpoint careers site and returns it as structured data you can load straight into a spreadsheet, a database or a model. If a company's jobs live at <name>.pinpointhq.com, this Actor reads them, given the company's Pinpoint name.

Give it the company's Pinpoint name (acme) or any acme.pinpointhq.com link, and for each job you get the title, the company's name, the department, the location with its country, the employment type and workplace type in Pinpoint's own terms, the pay range where the company shows one, the date it was posted, the closing date where the company set one, and a link to the job — and, if you ask for it, the full text of the job.

What makes it different:

  • Two kinds of request, both counted. One request reads a board's whole list of jobs — every job's full text and pay with it — from Pinpoint's public Job Postings JSON Endpoint, the feed Pinpoint documents for companies that list their jobs on their own website. Then one request per job you save reads that job's own page, because the feed carries no posting date, no country and no company name; the page states all three in the structured data it publishes for search engines. At the Actor's default of 90 requests a minute, 100 new jobs take a little over a minute.
  • Pay ranges as numbers, only where the company shows them. Where a job shows its pay on Pinpoint, the range comes back as min, max, currency and period, with salary.raw worded the way Pinpoint prints it (£70,000 - £90,000 / year). Pay the company has chosen to hide is never read. Where no range is shown but the job's text states one, that range is read and its sentence kept in salary.raw. Where no range is stated anywhere, the fields are null; nothing is estimated.
  • Incremental mode. Put the Actor on a schedule and each run returns only jobs that are new, changed, back again or gone. Unchanged jobs are skipped, not charged, and cost no request for their pages.
  • One stable schema. Every row has every field, every time. Unknown is null, never a missing key, so nothing downstream breaks on a sparse job. The schema is versioned (job.v1) and every Adderley Data jobs Actor uses it, so a Pinpoint board, a Recruitee board and a job board sit in the same table.
  • No personal data. Pinpoint's feed says who each role reports to, often by name, and a job's page carries the application form. This Actor reads neither and has nowhere to put them. Contact details inside job text are redacted by default.
  • Bounded cost. You set a maximum number of results; the run stops there, and no page is requested for a job that will not be saved. It also stops at the spending limit you set on the run in Apify.

What Pinpoint data can you extract?

FieldWhat it holds
idStable across runs: source:market:sourceJobId. Use it as your primary key.
titleJob title as listed.
company.nameThe hiring company, where the listing names one.
advertiser.nameThe business that placed the listing — often a recruitment agency. Never a person.
location.rawLocation text as listed. Several locations are joined with |.
location.suburbSuburb, when the listing states one.
location.cityCity or area, when the listing states one.
location.regionState or region, e.g. VIC.
location.postcodePostcode, when the source provides it.
location.countryISO 3166-1 alpha-2 country code.
workArrangementon_site, hybrid, remote or unknown.
employmentTypesNormalised: full_time, part_time, contract, casual, temporary, internship, volunteer.
salary.rawThe salary text exactly as shown, or null when the listing shows none.
salary.minLower bound as a number, when the text contains one.
salary.maxUpper bound as a number. Equal to min for a single figure.
salary.currencyISO 4217. Taken from the text, otherwise the market default.
salary.periodhour, day, week, month or year; null when the text does not say.
salary.includesSupertrue / false when the text says so ("plus super", "inc. super"); otherwise null.
classificationsThe source's category and subcategory pairs.
teaserThe short summary shown on the results page.
bulletPointsSelling points shown on the results page.
postedAtWhen the listing was posted, ISO 8601 UTC.
updatedAtThe source's own last-modified time, ISO 8601 UTC. Published by ATS and API sources; null where the site does not show one.
expiresAtExpiry, ISO 8601 UTC, where the source states one.
isPromotedtrue for paid placements. A listing shown both promoted and organic is returned once.
urlLink to the listing.
descriptionNull unless requested. text, optional sanitised html, and contactsRedacted.
changeTypeIncremental runs: NEW, UPDATED, REAPPEARED, EXPIRED (or UNCHANGED if you ask for those). Otherwise null.
firstSeenAtIncremental runs: when this monitor first saw the listing.
contentHashSHA-256 over the fields that define a change. Compare it to detect edits yourself.
scrapedAtWhen this row was produced, ISO 8601 UTC.
sourceSource key, e.g. seek.
marketMarket key, e.g. au, nz.
sourceJobIdThe source's own identifier for the listing.
company.sourceCompanyIdThe source's identifier for the company, when exposed.
company.urlThe company's page on the source site, when exposed.
advertiser.sourceAdvertiserIdThe source's identifier for the advertiser.
schemaVersionAlways job.v1. Breaking changes ship as job.v2 in a new Actor version, never silently.

How Pinpoint's fields fill the schema:

  • company.name is the company name each job's page gives. company.sourceCompanyId is the board's Pinpoint name and company.url its pinpointhq.com address. Where a job's page cannot be read, the company is named by its Pinpoint name.
  • url is the job's own page on the company's Pinpoint careers site.
  • location.raw is the company's own name for the job's location (Remote, India, Tampa, FL, London Office). city, region and postcode are that location's address. location.country is the country of the location's address as the job's page states it, as an ISO code; where the page cannot be read, it is the country the location's name or region states, never one guessed from a city.
  • classifications holds the job's department as category. The company's own structure groups are not read, so subcategory is null.
  • employmentTypes comes from Pinpoint's employment type, one of the 21 codes Pinpoint documents: "Permanent - Full Time" gives full_time; "Fixed Term - Full Time" gives full_time and contract; contract_temp gives contract and temporary; zero_hours gives casual. "Permanent" and "Flexible" on their own say nothing about hours, and an apprenticeship or a programme has no type in the schema, so they give nothing.
  • workArrangement comes from Pinpoint's workplace type: "Fully remote" gives remote, "Hybrid" gives hybrid, "Onsite" gives on_site.
  • postedAt is when the job was posted, from its page; expiresAt is the closing date where the company set one. Pinpoint publishes no last-modified time, so updatedAt is null.
  • market is always global: a Pinpoint board is the company's, not a country's.

How much does it cost to scrape Pinpoint job boards?

You pay per job saved to your dataset — $1.75 per 1,000 jobs on Apify's Starter plan — plus $0.005 each time a run starts. There is no monthly rental.

Apify planPricePer listing
Free$1.75 per 1,000 listings$0.00175
Starter (Bronze)$1.75 per 1,000 listings$0.00175
Scale (Silver)$1.60 per 1,000 listings$0.00160
Business (Gold)$1.45 per 1,000 listings$0.00145

Plus $0.005 per run start. Compute and proxy are included in these prices.

What you runCost (USD, Starter plan)
100 listings, one run$0.18
1,000 listings, one run$1.75
10,000 listings, one run$17.50
50,000 listings, one run$87.50
A daily incremental monitor finding about 150 new or changed listings a day, for a month$8.03

Descriptions cost nothing extra here: they arrive in the same request as the list of jobs. Use incremental mode for anything you run more than once — after the first run you pay only for what changed, and unchanged jobs cost no request.

How to scrape a Pinpoint careers site

  1. Find the company's Pinpoint name. It is the first part of the address in https://acme.pinpointhq.com: here, acme. A job link (acme.pinpointhq.com/en/postings/<job>) works too.
  2. Open the Actor in Apify Console and go to the Input tab. Paste one name or link per line into Job boards. Up to 500 boards per run.
  3. Optionally filter: Title keywords, Locations, Departments. Filters are applied to the board's list, so a job they leave out costs no request.
  4. Set Maximum results. This is also your cost cap.
  5. Press Start. When the run finishes, open the Output tab and export as JSON, CSV, Excel, XML or HTML, or read the dataset through the Apify API.

A job that matches more than one board or filter is returned once.

Input

FieldTypeDefaultWhat it does
boardsarray—One entry per company: its Pinpoint name (in https://acme.pinpointhq.com it is "acme"), or any link on that address — the careers site or one of its jobs. A company that shows its jobs on its own domain still has a pinpointhq.com name, and that name is needed: the company's own domain does not say which Pinpoint account is behind it. Up to 500 boards per run; each is one request for its list of jobs, plus one request per job saved.
keywordsarray—Keep jobs whose title contains every word of any keyword, in any order — "engineer data" matches "Senior Data Engineer". Leave empty for all titles.
locationsarray—Keep jobs whose location name, city or region contains this text ("London", "Texas"), whose location the board states is in this country ("India", "UK", "IN"), or with this workplace type ("Remote", "Hybrid", "On-site"). The country is the one the board's location name or region states. Leave empty for all locations.
departmentsarray—Keep jobs whose department contains this text, e.g. "Engineering". Leave empty for all departments.
maxResultsinteger100The run stops once this many jobs are saved. You are charged per job saved, so this is also your cost cap.
includeDescriptionbooleanfalseOn: the full job text — the description, then the responsibilities, the skills and the benefits, under the company's own headings — comes back with each row, read from the same request as the list of jobs, so it costs no extra requests. Off: listing fields only.
descriptionFormattext, text_and_html"text"Plain text, or plain text plus sanitised HTML.
redactContactsbooleantrueOn by default: email addresses and phone numbers inside description text are replaced with [redacted]. This Actor never outputs recruiter names or contact fields.
incrementalbooleanfalseRemember what earlier runs saw and save only jobs that are new, changed or gone. Unchanged jobs are skipped, not charged and cost no request for their pages. Put the Actor on a schedule with this on.
stateKeystring—Optional name for this monitor, e.g. "competitor-engineering". Runs with the same key share memory. Left empty, a key is derived from the boards and filters themselves.
emitExpiredbooleantrueIncremental mode only. When a complete run no longer finds a job it saw before, save one row with changeType EXPIRED.
emitUnchangedbooleanfalseIncremental mode only. Saves (and charges for) every job, labelled UNCHANGED where nothing moved.
proxyConfigurationobject{"useApifyProxy":true}Apify Proxy, automatic group, is the default and is what this Actor is tested with.
maxConcurrencyinteger4Parallel requests. The default is deliberately modest.
maxRequestsPerMinuteinteger90An upper bound on request rate across the whole run.

A typical input:

{
"boards": [
"intermedia"
],
"maxResults": 100
}

The prefilled board, intermedia, is one company's public Pinpoint careers site, used here only as an example of a public Pinpoint board. This Actor is not affiliated with that company. The prefill saves 20 jobs, so a trial run makes 21 requests: one for the list and one for each job's page.

Output

One row per job. This is a synthetic example in the exact shape the Actor returns:

{
"schemaVersion": "job.v1",
"id": "pinpoint:global:2900001",
"source": "pinpoint",
"market": "global",
"sourceJobId": "2900001",
"url": "https://example-freight.pinpointhq.com/en/postings/00000000-0000-4000-8000-000000290001",
"title": "Data Analyst",
"company": {
"name": "Example Freight Co",
"sourceCompanyId": "example-freight",
"url": "https://example-freight.pinpointhq.com"
},
"advertiser": {
"name": "Example Freight Co",
"sourceAdvertiserId": "example-freight"
},
"location": {
"raw": "Melbourne, VIC",
"suburb": null,
"city": "Melbourne",
"region": "Victoria",
"postcode": "3121",
"country": "AU"
},
"workArrangement": "hybrid",
"employmentTypes": [
"full_time"
],
"salary": {
"raw": "A$95,000 - A$110,000 / year",
"min": 95000,
"max": 110000,
"currency": "AUD",
"period": "year",
"includesSuper": null
},
"classifications": [
{
"category": "Data",
"subcategory": null
}
],
"teaser": null,
"bulletPoints": [],
"postedAt": "2026-09-20T22:14:05.000Z",
"updatedAt": null,
"expiresAt": "2026-10-19T12:59:59.000Z",
"isPromoted": false,
"description": null,
"changeType": "NEW",
"firstSeenAt": "2026-09-21T19:30:12.000Z",
"contentHash": "97f9abefa8d93fb036a182b819aca44ca11280558164aab89d4b0e4ed858cc2c",
"scrapedAt": "2026-09-21T19:30:12.000Z"
}

The Output tab has two table views: Overview (the fields most people want, flattened) and Changes (for incremental runs).

Incremental mode: monitor new roles across companies

Turn on Incremental mode and run the same input on a schedule — hourly, daily, weekly. The Actor keeps a small record of what it has seen and every row tells you what happened:

changeTypeMeaning
NEWFirst time this monitor has seen the job
UPDATEDSeen before, and the title, location, department, employment type, workplace type, pay range, closing date or any word of the job's text has changed
REAPPEAREDWas reported as expired and is back
EXPIREDSeen before and no longer on the board. One row, once
UNCHANGEDOnly if you turn on Also save unchanged jobs

How it behaves, so there are no surprises:

  • The first run returns everything as NEW. From the second run you pay only for the difference.
  • A change is found by comparing the job in the board's list with the last run, before any job's page is requested. That is what keeps an unchanged job free: no charge and no request. An edit to the text counts: a corrected typo is reported as UPDATED and charged like any other change. A change of formatting alone does not count, and neither does a change to who the role reports to or to the company's internal references, which the Actor never reads.
  • The posted date, the country and the company's name come from each job's page, after that comparison, so they are delivered but not compared.
  • EXPIRED is only ever reported by a complete run. If a run hits your result cap or your spending limit, or a board cannot be read, nothing is declared expired — a job on a board the run never read is not gone.
  • Each board is read in one response, so a run compares whole boards, never pages read seconds apart.
  • Runs share memory when they share a State key. Leave it empty and the key is derived from the boards and filters themselves, so the same input always continues the same monitor. Name it (competitor-engineering) if you want to change filters later without starting again.
  • A job not seen for 45 days is forgotten.

Descriptions and contact details

Full descriptions are off by default. Turn on Include full descriptions and each row carries description.text (and sanitised description.html if you choose that format): the description, then the responsibilities, the skills and the benefits, each under the heading the company gave it, in the order Pinpoint's own job page shows them. Because Pinpoint returns the text in the same response as the list of jobs, this costs no extra requests.

Job text sometimes contains a recruiter's email address or phone number. With Redact contact details on — the default — those are replaced with [redacted] and description.contactsRedacted is true; so is a personal profile address (linkedin.com/in/…). Neither description.text nor the optional HTML carries the address behind a link: the HTML keeps each link's words and drops its address, and drops images. The Actor never returns recruiter names or contact details as fields, under any setting: it never reads who a role reports to, the company's structure groups, office street addresses or the application form on a job's page, and the schema has nowhere to put a person. If your use case is contacting individuals, this is the wrong tool.

What people use it for

  • Competitor and market hiring signals. Which companies are opening which roles, in which departments and countries, and how often — a daily monitor across a list of boards is one scheduled run.
  • Pay-transparency datasets. Shown ranges across companies, roles and countries, as numbers with their currency and period.
  • Job aggregators and alert products. A clean feed of new jobs from a curated list of employers, deduplicated and labelled by change.
  • Sales and partnership research at company level. Growth signals from hiring, without collecting anything about the individuals involved.
  • Research and teaching. A clean, repeatable dataset with a documented schema.

Using the API

Run it from code with the Apify client, using your own API token:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('adderleydata/pinpoint-jobs-scraper').call({"boards":["intermedia"],"maxResults":100});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, items[0]?.salary);
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("adderleydata/pinpoint-jobs-scraper").call(run_input={"boards":["intermedia"],"maxResults":100})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
print(item["title"], item["location"]["raw"], item["postedAt"])

Schedules, webhooks and the Make, Zapier, n8n and Google Sheets integrations all work the way they do for any Apify Actor. The Actor runs with limited permissions and is priced per event, so AI agents can call it through Apify's MCP server as well.

The Actor reads Pinpoint's public Job Postings JSON Endpoint — the feed Pinpoint documents for companies that list their jobs on their own website, with no login and no key — and each saved job's public page, and returns facts about job postings. It does not log in, does not solve CAPTCHAs, does not submit applications and does not collect personal information.

What you do with the data is your responsibility. Each company's job text is its own copyright — analyse it, do not republish it. If your project touches personal information, privacy law applies to you wherever you are. This is general information, not legal advice.

Questions

Where do I find a company's Pinpoint name? Open any job on the company's careers site. If the address is acme.pinpointhq.com/en/postings/<job>, the name is acme; paste the whole link or just the name.

The company's careers site is on its own domain. A company's own careers address does not say which Pinpoint account is behind it, so the Actor asks for the pinpointhq.com name instead of guessing, and never sends a request to a domain Pinpoint does not run. If the careers site links to <name>.pinpointhq.com anywhere, that is it.

A board I gave came back as "not found". Pinpoint answers "The page you were looking for doesn't exist" when no company on Pinpoint uses that name. The run log names the board, and the other boards in the run are unaffected. A missing board is asked for once, not retried, and a run in which every board is missing finishes with an empty dataset and a message naming each one. Pinpoint gives the same answer for a feed it has moved, so if the company's careers site opens in your browser under that name, tell us on the Issues tab.

Does it need a Pinpoint login or API key? No. The feed and the job pages are public.

Why does each saved job cost a request? Pinpoint's feed carries each job's title, department, location, text and pay, but not the date it was posted, the country of its location or the company's name. Each job's page states all three, so the Actor asks for that page once for every job it saves — and never for a job a filter, the result cap or incremental mode leaves out.

Can I filter by the date a job was posted? Not inside the run: the feed has no dates, so a date filter could only be applied after paying a request for every job. Every row carries postedAt; filter on it in your dataset or with a dataset view.

Why did a country filter miss a job in that country? Filters are applied to the board's list, before any job's page is read, so a country in Locations matches the country the company's own location name or region states ("Remote, India", "New South Wales"). A location named only "London Office" states no country in the list; filter on "London" instead. The row still carries the country from the job's page.

Why is salary empty when the job's page shows pay? It is not, if the company shows it: pay is read wherever Pinpoint marks it as shown. A range Pinpoint prints as "Competitive" has no numbers, so it gives no salary. Pay per two weeks keeps its numbers and currency with period empty, because the schema has no two-week period.

What if a job is withdrawn during the run? If a job disappears in the seconds between the board's list and the request for its page, it is saved with what the list said — title, department, location, pay and link — and the run log says so.

Can I get recruiter emails or phone numbers? No, by design.

How current is the data? It is read from Pinpoint while your run is in progress. postedAt is when the job was posted; scrapedAt records when the row was produced.

The field I need is not there. Open an issue on the Issues tab. Fields are added to the schema without breaking existing ones.

Support

Use the Issues tab on this page. We read it every day. Include the run ID and what you expected to see.

Other Adderley Data Actors

Every Actor in a vertical returns the same fields, so adding a source needs no new code on your side.

About

Made by Adderley Data, Melbourne — https://adderleydata.com. Not affiliated with, endorsed by or sponsored by Pinpoint. Pinpoint is a trade mark of its owner and is used here only to describe what this Actor reads.