# ApplicantPro Jobs API

> Query jobs from ApplicantPro boards through one API with ?source=applicantpro — plus how ApplicantPro's core jobs endpoint really behaves.

Source: https://hyperjobs.io/ats/applicantpro

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`](/docs/api-reference/meta)
rather than trusting this sentence.

<Note title="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`](/docs/api-reference/meta) is the live answer;
  [coverage](/docs/reference/coverage) is where the rotation is explained.
</Note>

## Try it

```bash
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"
```

```json title="Illustrative — the shape, not a live response"
{
  "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:

```bash
# 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

<Warning title="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 given
  ```

  A 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.
</Warning>

### `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](/ats/linkedin) or elsewhere, and the
[company join](/docs/objects/company).

The same adapter also backs [iSolved Hire](/ats/isolved), which is this exact
product white-labelled onto another domain.

Query it with `?source=applicantpro`.
[Quickstart](/docs/quickstart), then [pricing](/pricing).
