Paycor Jobs API
Query jobs from Paycor Recruiting boards through one API with ?source=paycor — plus how Paycor's CareerHome endpoint really behaves.
Paycor Recruiting is the ATS inside Paycor's payroll suite, and underneath it is
still Newton — the product Paycor acquired, whose markup hooks (gnewtonJobPosition,
gnewtonJobDescriptionText) are what you actually parse. Boards live at
recruitingbypaycor.com/career/CareerHome.action?clientId=<cid>, and there are
8,181 Paycor postings in this dataset — a number that moves hourly, so read it
from /v1/meta rather than trusting this sentence.
Try it#
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "source=paycor" \
--data-urlencode "limit=2"{
"data": [
{
"id": "paycor:4c17b9e0d2a84f6395bd07e1fa62c83d_1187",
"title": "Maintenance Technician",
"company": {
"slug": "harbor-point-parks",
"name": "Harbor Point Parks",
"domain": "harborpointparks.com",
"website": "https://harborpointparks.com",
"logo": "https://media.licdn.com/dms/image/…",
"industry": "Recreational Facilities",
"employee_count": 480
},
"location": {
"remote": false,
"countries": ["United States"],
"cities": ["Kansas City"],
"regions": ["Missouri"],
"raw": ["Kansas City, MO"]
},
"employment_type": ["FULL_TIME"],
"work_arrangement": "On-site",
"seniority": null,
"salary": {
"min": 22,
"max": 26,
"currency": "USD",
"unit": "HOUR",
"annual_min": 45760,
"annual_max": 54080,
"summary": "$22 - $26 per hour"
},
"skills": ["hvac", "preventive maintenance"],
"taxonomies": ["Skilled Trades"],
"apply_url": "https://recruitingbypaycor.com/career/JobIntroduction.action?clientId=4c17b9e0d2a84f6395bd07e1fa62c83d&id=1187&lang=en",
"url": "https://recruitingbypaycor.com/career/JobIntroduction.action?clientId=4c17b9e0d2a84f6395bd07e1fa62c83d&id=1187&lang=en",
"source": "paycor",
"posted_at": "2026-07-13T09:41:02.000Z",
"expires_at": null,
"updated_at": "2026-07-14T09:41:02.000Z"
}
],
"total": 2,
"limit": 2,
"offset": 0
}Read posted_at on this source carefully. Paycor publishes no posting date, and
that sentence has consequences — see below.
How Paycor boards actually work#
The list is one of the best deals in this pool:
# 1. List — ONE GET, the entire board, server-rendered
curl "https://recruitingbypaycor.com/career/CareerHome.action?clientId=<cid>"
# 2. Detail — one per posting; the body only exists here
curl "https://recruitingbypaycor.com/career/JobIntroduction.action?clientId=<cid>&id=<jid>&lang=en"One request returned 1,271 jobs and about 1MB on a single tenant. There is no
pagination at all, and that's proven rather than assumed: on that same board,
page, pageNumber, start, offset and the separate CareerHomeSearch.action
endpoint each returned a byte-identical 1,028,449-byte response with the same
1,271 ids. There's no paging parameter to get wrong because there's no paging.
boardToken is the clientId query param — a 32-hex id, not a subdomain.
newton.newtonsoftware.com serves the same board byte-for-byte, and legacy boards
still link to it, so both hosts are the same board.
What bites you#
Nested tables truncate the description to about 5%, silently
The body lives in a <td>, and tenants paste rich text with whole tables
inside it. A lazy ([\s\S]*?)</td> stops at the first inner </td> and cuts
the posting to a fraction of itself: 273 characters instead of 6,376 on one
board, 1,202 instead of 19,734 on another — 5 of 32 sampled postings, at HTTP
200, with a populated-looking description field.
Nothing alerts you. The field isn't empty, it's just wrong, and it's wrong on
exactly the longest and most detailed postings. Depth-count the <td> tags.
Tenants paste whole rendered Newton pages into the description. Markup,
gnewton* ids and all. One posting carries a second
<td id="gnewtonJobDescriptionText"> and a second <td id="gnewtonJobRemoteStatus">
inside its own body, so a global regex for a field id can read a value straight
out of the prose. The fix is structural rather than clever: locate the body span
first, then parse the label fields from the page with that span removed.
The group header means different things on different tenants. Most boards
group by department, with the row cell holding the location. Others group by
location — one groups by store, (529) 17239 FM 529 Houston,, TX — with the
department in the row. Read either position blindly and you write a department
into location, or a store address into department, on a real slice of the
platform. The sort control declares which field is grouped; when it's absent we
set department to null rather than guess.
gnewtonCareerGroupJobDescriptionClass is not a description. Despite the
name it holds the row's location — or its department, per the flip above. The real
body is detail-only.
Labels are localized. <b>Position:</b> renders as <b>Puesto:</b> on a
Spanish board, so stripping the literal string "Position:" leaves you with
Puesto: Tecnico de Pisos as the job title. Strip the <b> element instead
and the problem doesn't exist in any language.
The same job renders once per translation. lang=en and lang=es are the
same posting under the same id, so every bilingual board double-counts unless you
dedup on the id.
A bogus job id returns HTTP 200. A soft 404 — about 10.7KB, no body block. An empty board returns 200 too. Neither is a failure, and neither should yield rows.
There is no date on this platform at all#
Paycor exposes no posting date anywhere. Not on the listing, not on the detail, no "Posted" line, no date field of any kind — checked across eight tenants. This is not us failing to map it.
What that means for `posted_at`
posted_at falls back rather than going null:
where a board publishes no date, the value is our first sight of the posting,
not the employer's publication date.
It's still a sound ordering key and a sound "newer than X" filter — a job we first saw yesterday really is newer to this dataset than one we first saw last month. It is not a claim about the employer's own timeline, and on this source it never can be. If your product shows "posted 3 days ago" to an end user, know which sources can support that sentence.
Salary is the mirror image: Paycor states the period explicitly and consistently
($47,000 - $67,000 per year, $18 - $22 per hour), so where an amount and a
period are both present the pay is real and structured. Where the tenant wrote
"DOE" or a bare figure, we keep the text and emit no number — a guessed unit is
worse than none.
Or use ours#
One GET is the whole board, which makes Paycor look like a weekend project. The weekend goes on the other things: the nested-table truncation that only bites your best postings, the pasted-page duplicates that poison a global regex, the group header that inverts per tenant, and the localized labels.
You get bodies depth-counted rather than cut at the first inner </td>, labels
parsed from the page with the body removed so a pasted Newton page can't leak into
your fields, department claimed only when the board declares its own grouping, the
bilingual duplicate collapsed to one row, salary structured only where Paycor
actually stated a period, and the company join — a Paycor
board tells you a 32-hex clientId and nothing else about who that is.
Refresh cadence varies by source; the free list and the per-posting detail put Paycor in the middle. See coverage, and don't assume uniform latency.
Query it with ?source=paycor.
Quickstart · Pricing
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key