ApplicantPro Jobs API
Query jobs from ApplicantPro boards through one API with ?source=applicantpro — plus how ApplicantPro's core jobs endpoint really behaves.
ApplicantPro is applicant tracking for small US employers, and its customer list
skews municipal: city governments, aquatic centers, libraries, airports,
non-profits. Boards live at <tenant>.applicantpro.com, backed by a JSON API
that's unusually rich once you can get it to answer you. There are 251
ApplicantPro postings in this dataset, 100% of them with a real description —
counts move hourly, so read them from /v1/meta
rather than trusting this sentence.
Why that count is small, and what it isn't
251 is not this platform's size and it isn't a quality verdict. The adapter is
verified at 100% descriptions across every tenant tested, and our queue holds
423 ApplicantPro boards — most of which have not yet been through a full
rotation. The number is a coverage state, not a ceiling, and it should grow.
/v1/meta is the live answer;
coverage is where the rotation is explained.
Try it#
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "source=applicantpro" \
--data-urlencode "time_frame=7d" \
--data-urlencode "limit=2"{
"data": [
{
"id": "applicantpro:cityofprescott_1029",
"title": "Lifeguard",
"company": {
"slug": "cityofprescott",
"name": "City of Prescott",
"domain": "prescott-az.gov",
"website": "https://prescott-az.gov",
"logo": "https://media.licdn.com/dms/image/…",
"industry": "Government Administration",
"employee_count": 480
},
"location": {
"remote": false,
"countries": ["United States"],
"cities": ["Prescott"],
"regions": ["AZ"],
"raw": ["Prescott, AZ"]
},
"employment_type": ["PART_TIME"],
"work_arrangement": "On-site",
"seniority": null,
"salary": {
"min": 16.5,
"max": 19.25,
"currency": null,
"unit": "HOUR",
"annual_min": 34320,
"annual_max": 40040,
"summary": "DOE, plus certification reimbursement"
},
"skills": ["lifeguarding", "cpr"],
"taxonomies": ["Recreation"],
"apply_url": "https://cityofprescott.applicantpro.com/jobs/1029",
"url": "https://cityofprescott.applicantpro.com/jobs/1029",
"source": "applicantpro",
"posted_at": "2026-06-19T00:00:00.000Z",
"expires_at": null,
"updated_at": "2026-07-14T07:45:11.000Z"
}
],
"total": 2,
"limit": 2,
"offset": 0
}How ApplicantPro boards actually work#
Three steps, and the first one is not optional:
# 1. Bootstrap — the board HTML is the only place `domainId` exists
curl -L "https://<tenant>.applicantpro.com/jobs/" # → componentData: { domainId : 4689 }
# 2. List — every live posting, no descriptions
curl -L "https://<tenant>.applicantpro.com/core/jobs/<domainId>?getParams=%7B%7D"
# 3. Detail — the body, plus a benefits block
curl -L "https://<tenant>.applicantpro.com/core/jobs/<domainId>/<jobId>/job-details?getParams=%7B%7D"The list is genuinely rich: salary as minSalary/maxSalary with payTypeFrame
carrying the unit, employmentType, workplaceType, structured city/state/ISO3
location, and startDateRef/endDateRef dates. What it does not carry is the
description, so the body is one detail call per new posting.
We read the JSON detail rather than the job page's JSON-LD deliberately: the
description is identical on both (8,759 characters on each, measured) and the JSON
additionally carries a benefits block the JSON-LD has no room for.
getParams is required, and omitting it returns 200#
A PHP error, served with a success status
Leave getParams off and the API answers HTTP 200 with a PHP type error in
the body:
Argument 1 passed to Awesome\Utils\… must be of the type array, null givenA status check sails straight past this. So does anything that trusts a 200 and moves on. You get a successful request, an empty job list, and a board that looks like it simply isn't hiring.
getParams={} — url-encoded, %7B%7D — satisfies it. The value doesn't have to
say anything; the parameter just has to exist.
domainId cannot be derived#
It's a per-tenant integer that lives in exactly one place: the board HTML, as
componentData: { domainId : 4689 }. There's no pattern to it and no endpoint
that maps a slug to it. That's why the bootstrap fetch above is a step rather than
an optimisation you can skip — every board costs one extra request before you can
ask it anything.
A useful side effect: churned tenants redirect to /notset.php and yield no
domainId at all, which is a clean signal that the board is gone rather than
broken.
What bites you#
orgTitle is not the employer. It reads like one on a few tenants
(sportadvisory → "Shafter Aquatic Center"), which is exactly what makes it
dangerous. On most it's the internal org unit: "Airport", "Data",
"Library -Dept #4100". Map it to the company name and you write "Data" into
your dataset as a company that employs people. It's an org unit in every case, so
it belongs in team, and the board's own name stays the employer.
untilFilled makes endDateRef a lie. When a posting is open until filled,
endDateRef still carries a date — a rolling placeholder, not a deadline.
Expiring on it drops live jobs. When untilFilled is set, there is no expiry.
The published URL form is the one your regex won't match. The canonical link
employers actually publish is the path form,
www.applicantpro.com/openings/<tenant>/jobs, which 302s to the subdomain. A
subdomain regex reads that URL's host as www and rejects it. This is not
academic: the Wayback index holds ApplicantPro tenants almost exclusively in the
path shape — 1,821 in a 20,000-URL sample, against zero in subdomain form.
Check the path first or discovery finds nothing.
Dates arrive as "Jun 19, 2026". Parse that with a naive local-time parser
and the date lands a day early for anyone west of UTC.
Or use ours#
A bootstrap fetch, a list call with a parameter whose absence returns 200, a detail call per posting, and a discovery path that only works if you match the URL form nobody's regex expects — across the 423 boards in our queue.
You get the domainId bootstrap handled per tenant, getParams always present,
descriptions and the benefits block the JSON-LD omits, salary with the unit
read off payTypeFrame, orgTitle filed as a team rather than libelling "Data"
as an employer, untilFilled postings left unexpired, dates parsed at UTC so they
don't drift a day, ids scoped per tenant, dedup against the same job on
LinkedIn or elsewhere, and the
company join.
The same adapter also backs iSolved Hire, which is this exact product white-labelled onto another domain.
Query it with ?source=applicantpro.
Quickstart, then pricing.
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key