All platforms

UKG Pro Jobs API

Query jobs from UKG Pro boards through one API with ?source=ultipro — plus how UltiPro's LoadSearchResults endpoint really behaves.

?source=ultiprosource reference

UKG Pro is the HR and payroll suite formerly called UltiPro, and like Trakstar Hire, the rename never reached the plumbing. Boards still live at recruiting.ultipro.com/<tenant>/JobBoard/<guid>, the endpoints still answer to the old name, and the source value here is ultipro. There are 47,180 UKG Pro postings in this dataset — a number that moves hourly, so read it from /v1/meta rather than trusting this sentence.

The value is ultipro, not ukg or ukgpro. It matches the host the boards are served from, and it's the same string everywhere: the ?source= filter, the keys of /v1/metasources, and the source field on every returned posting. If you're looking for ukg in the vocabulary, that's why it isn't there.

Try it#

BASH
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "source=ultipro" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=2"
Illustrative — the shape, not a live response
{
  "data": [
    {
      "id": "ultipro:5b8f0f2e-91c4-4d17-a0e6-7f3b2c9d1a45",
      "title": "Patient Access Representative",
      "company": {
        "slug": "westbrook-health",
        "name": "Westbrook Health System",
        "domain": "westbrookhealth.org",
        "website": "https://westbrookhealth.org",
        "logo": "https://media.licdn.com/dms/image/…",
        "industry": "Hospitals and Health Care",
        "employee_count": 6400
      },
      "location": {
        "remote": false,
        "countries": ["United States"],
        "cities": ["Dayton"],
        "regions": ["Ohio"],
        "raw": ["Dayton, OH, United States"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": null,
      "seniority": null,
      "salary": {
        "min": 17.5,
        "max": 21,
        "currency": "USD",
        "unit": "HOUR",
        "annual_min": 36400,
        "annual_max": 43680,
        "summary": "$17.50 – $21.00 an hour"
      },
      "skills": ["patient registration", "epic"],
      "taxonomies": ["Healthcare"],
      "apply_url": "https://recruiting.ultipro.com/WES1004/JobBoard/8c2e1a70-5f43-4b91-9d0a-3e6c7b28f150/OpportunityDetail?opportunityId=5b8f0f2e-91c4-4d17-a0e6-7f3b2c9d1a45",
      "url": "https://recruiting.ultipro.com/WES1004/JobBoard/8c2e1a70-5f43-4b91-9d0a-3e6c7b28f150/OpportunityDetail?opportunityId=5b8f0f2e-91c4-4d17-a0e6-7f3b2c9d1a45",
      "source": "ultipro",
      "posted_at": "2026-07-11T00:27:00.000Z",
      "expires_at": null,
      "updated_at": "2026-07-14T06:12:41.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}

work_arrangement is null there on purpose, and salary is quoted per hour rather than per year. Both are the platform showing through — see below.

How UKG Pro boards actually work#

Two calls. The list is a POST and returns the whole board:

BASH
# 1. List — one request, every posting, no paging loop
curl -X POST "https://<host>/<tenant>/JobBoard/<guid>/JobBoardView/LoadSearchResults" \
  -H "content-type: application/json" \
  -d '{"opportunitySearch":{"Top":2000,"Skip":0}}'
 
# 2. Detail — one per posting; this is the only place the body exists
curl "https://<host>/<tenant>/JobBoard/<guid>/OpportunityDetail?opportunityId=<Id>"

Top is genuinely uncapped, which is rare in this pool. Top: 2000 returned all 254 postings of one board and all 388 of another in a single request, set-identical to the paged union, so there is no pagination loop to get wrong. Only Top and Skip matter; QueryString, OrderBy, Filters and matchCriteria are all optional.

The detail is not JSON. It's an HTML page with the posting inside a new US.Opportunity.CandidateOpportunityDetail({…}) JavaScript literal, so you brace-match the object (string-aware, or a } inside a description ends it early) and JSON.parse what you cut out. The list's BriefDescription is a teaser between roughly 109 and 718 characters and is never the body.

The board address is three things, not one#

From https://recruiting2.ultipro.com/WES1004/JobBoard/8c2e1a70-…
hostrecruiting2.ultipro.com
tenantWES1004
board guid8c2e1a70-5f43-4b91-9d0a-3e6c7b28f150

recruiting and recruiting2 are not interchangeable, and there's no rule that gets you from a company to its host — the wrong one 404s. Miss any of the three and you have nothing.

The salary fields are not the salary#

This is the one to know about UKG Pro, and it is the reason a naive scrape of this platform ships numbers that are worse than no numbers.

CompensationAnnualMinimum / CompensationAnnualMaximum and CompensationHourlyMinimum / CompensationHourlyMaximum look exactly like the pay band. They are the platform's validation bounds — the range the tenant's admin configured as acceptable input. One real posting carries an annual minimum of 20.80 and an annual maximum of 2,077,920.00, with an hourly minimum of 0.01, sitting directly beside its actual advertised rate of $17.50/hr. Read them and every posting on the board reports a $20.80–$2m salary, at HTTP 200, with a plausible-looking object.

The real shape is elsewhere:

FieldWhat it holds
PayRange.PayRangeMinimum / PayRangeMaximumThe advertised band, when the employer published one
PayRangeCurrencyCodeIts currency
CompensationAmount {Code, Value}A single posted rate, when there's no band
SalariedA boolean — the only period signal on the platform

There is no period string anywhere. Salaried: true means the amounts are annual, false means hourly, and when it's absent you cannot tell $60/hr from a $60 salary, so the honest move is to emit nothing. Moving from the bounds to these fields took salary on the verified boards from 0% to 92%.

What bites you#

A minimal request body returns an empty board, not an error

POST {} to LoadSearchResults answers HTTP 200 with {"opportunities":[],"totalCount":0}. That is byte-for-byte what a real board with no open roles looks like.

So a renamed field, a typo in opportunitySearch, or a body that drifts when the platform tightens something reads as "this company isn't hiring" — forever, silently, with a green status code. There is no error to catch and nothing to alert on.

FullTime is a boolean. There is no employment-type string anywhere on the platform. true and false are all you get, which is why employment_type on this source is only ever full-time, part-time, or nothing.

LocalizedName is a site label, and often an empty one. It reads like a location — WCH Service Ctr. — and on most jobs of some tenants it's the empty string, so a naive join yields "". The structured Address (city, state, country) is the ground truth.

The list and the detail disagree about PostedDate for the same job — one board reported 2026-07-15T00:27Z on the list and 2026-07-14T12:58Z on the detail. Neither is wrong exactly; you just have to pick one deliberately and know you did. We take the list's, because it's the board's own view of itself.

JobLocationType does not decode. It's an unlabeled integer enum, unset on about 74% of postings, and it doesn't survive contact with reality: on one tenant JobLocationType=1 covered 0 of 64 jobs literally named "Remote", while 2 covered remote and on-site roles alike. We don't map it, which is why work_arrangement is null on this source far more often than on Lever, where it's a real four-value enum. Null there means "the platform didn't say", not "we didn't look".

totalCount returns 0 on an over-shot page. If you do page, never read the total past the end of the list — it reports what came back, not the board size.

Or use ours#

The fetch is one POST. Everything after it is the work.

You get the description already cut out of its JavaScript literal, the pay read from PayRange/CompensationAmount with Salaried as the period rather than from the validation bounds that would put a $2m ceiling on a $17.50/hr job, the location built from the structured address instead of an empty site label, and the company join — a UKG Pro board tells you WES1004 and nothing else about who that is.

Refresh cadence isn't uniform across sources, and the per-posting detail call makes this one more expensive than a board that ships its bodies inline. See coverage; don't assume every source moves at the same speed.

Query it with ?source=ultipro. Quickstart · Pricing

Every source above normalises onto the same schema, with the hiring company already joined onto each posting.

Get an API key