ADP Jobs API
Query jobs from ADP Workforce Now boards through one API with ?source=adp — plus why finding an ADP board is harder than fetching one.
ADP is payroll for a very large share of American employers, and Workforce Now
carries a recruiting module with a public career center attached. The API behind
it is unauthenticated, paginated, and returns full descriptions. There are 4,560
ADP postings in this dataset — a number that moves hourly, so read it from
/v1/meta rather than trusting this sentence.
That number is small next to Oracle or SuccessFactors, and the reason why is the whole story of this page. It isn't that ADP is hard to scrape. ADP is easy to scrape. It's that for months, this adapter was finished, tested, working, and returned nothing at all — because nobody could hand it a list of boards.
The board id is a query parameter#
Every other platform on this site gives each customer a subdomain.
jobs.lever.co/<site>. careers-<slug>.icims.com. <tenant>.wd5.myworkdayjobs.com.
That subdomain is a handle: you can crawl for it, pattern-match it in a link,
recognise it in a sitemap, guess it from a company name and check.
ADP doesn't have one. Every customer in the world is on the same host, and the
board is identified by a cid query parameter that is a bare UUID:
https://workforcenow.adp.com/mascsr/default/mdf/recruitment/recruitment.html?cid=9f2b1c04-7d3e-4a88-b115-6c0e2f7a91d4There is nothing in that string to enumerate. 9f2b1c04-… is not derivable from
the company's name, domain, or anything else about it. It is an opaque identifier
you either find written down somewhere or never learn. There is no directory, no
sitemap of tenants, and no pattern to sweep.
Finding boards is the hard part, not fetching them
This is the single most-repeated lesson in building this dataset, and ADP is its cleanest example. A perfect adapter against a public, unauthenticated, well-behaved API is worth exactly zero jobs until someone solves discovery. Three platforms here sat in that state at once: ADP, Eightfold, and join.com. join.com turned out to have had a 23,548-board list sitting in a public dataset the whole time. Eightfold is still waiting.
And then the boards were found, and it still returned nothing#
The second half is better, because it's the kind of bug that survives code review. Discovery eventually turned up ADP boards and reported success:
adp → 1530 new boardsThe table got 1.
A shared guard called isAggregatorBoardToken rejects board tokens that look like
bare UUIDs. That rule is correct and load-bearing for Ashby, where a
jobs-page-<uuid> token is an aggregator board mixing many employers together
and must not be stored as one company. For ADP it was catastrophic, because for
ADP a bare UUID is the board id. The same rule that protects one platform
silently deletes another.
The log counted map inserts. The guard rejected at write. So the run reported 1,530 new boards and wrote one, and nothing anywhere returned an error. Between ADP and Paylocity, which has the same UUID-shaped board id, 4,188 boards were nearly lost to it.
Try it#
curl -G https://api.hyperjobs.io/v1/jobs \
-H "Authorization: Bearer $HYPERJOBS_KEY" \
--data-urlencode "source=adp" \
--data-urlencode "time_frame=7d" \
--data-urlencode "limit=2"{
"data": [
{
"id": "adp:c41f8a26e9b74d02",
"title": "Warehouse Associate",
"company": {
"slug": "cascade-supply",
"name": "Cascade Supply Co",
"domain": "cascadesupply.com",
"website": "https://cascadesupply.com",
"logo": "https://media.licdn.com/dms/image/…",
"industry": "Wholesale",
"employee_count": 640
},
"location": {
"remote": false,
"countries": ["United States"],
"cities": ["Tacoma"],
"regions": ["Washington"],
"raw": ["Tacoma, WA, US"]
},
"employment_type": ["FULL_TIME"],
"work_arrangement": "On-site",
"seniority": null,
"salary": {
"min": 19,
"max": 24,
"currency": "USD",
"unit": "HOUR",
"annual_min": 39520,
"annual_max": 49920,
"summary": "19 To 24 (USD) Hourly"
},
"skills": ["forklift", "inventory"],
"taxonomies": ["Operations"],
"apply_url": "https://workforcenow.adp.com/mascsr/default/mdf/recruitment/recruitment.html?cid=9f2b1c04-7d3e-4a88-b115-6c0e2f7a91d4",
"url": "https://workforcenow.adp.com/mascsr/default/mdf/recruitment/recruitment.html?cid=9f2b1c04-7d3e-4a88-b115-6c0e2f7a91d4",
"source": "adp",
"posted_at": "2026-07-12T00:00:00.000Z",
"expires_at": null,
"updated_at": "2026-07-14T05:52:03.000Z"
}
],
"total": 2,
"limit": 2,
"offset": 0
}How ADP boards actually work#
Once you have the cid, it's two clean endpoints and OData-style paging:
# 1. List — $top / $skip, 50 per page
curl "https://workforcenow.adp.com/mascsr/default/careercenter/public/events/staffing/v1/\
job-requisitions?cid=<cid>&\$top=50&\$skip=0"
# 2. Detail — this is where the description lives
curl "https://workforcenow.adp.com/mascsr/default/careercenter/public/events/staffing/v1/\
job-requisitions/<itemID>?cid=<cid>"The detail is worth the N+1 here. It carries payGradeRange on roughly 100%
of postings, a verbatim SalaryRange string on about 96%, and the employer's
own requisition number on about 100%. Descriptions land at 100%.
SalaryRange is prose in a structured field, and it's the good kind: 19 To 24 (USD) Hourly, 90000.00 To 100000.00 (USD) Annually. The unit comes from a
SalaryType code — HR or AN — rather than being inferred, though a bound
under 200 is a wage whatever the code says.
What bites you#
There is no per-job apply URL on this source
ADP publishes no clean deep link to an individual posting. The list item's
links array is empty, and recruitment.html is a single-page app that returns
200 for any parameters you give it, so a per-job URL can't be constructed and
then trusted.
So on adp rows, url and apply_url are the board-level career centre
link, carrying only the cid. The candidate lands on the employer's ADP job
list rather than on that job. Everywhere else on this site apply_url is the
employer's real form for that posting; here it's as close as the platform
permits. That's a deliberate limit, not an omission, and it's why title,
location, description and pay are all complete on this source while the link is
the weak field.
Pagination that stops at 200 truncates silently. The page size is 50 and the
loop has to run to a short page. A cap set for safety turns into a data bug on any
large employer, with a 200 on every request on the way.
ADP's own item id is not the id in its URLs. The API's itemID and the job id
used by ADP's older recruitment front end differ, so rows collected through the
API don't line up with rows collected from those URLs. Two ids for one job, and
neither is wrong.
Or use ours#
The API is genuinely pleasant. The dataset is the product.
You get 4,560 postings across the ADP boards we've found, with descriptions at
100%, structured pay off payGradeRange with the unit read from ADP's own code
rather than guessed, the employer's requisition number, and the company
join — a cid is a UUID and tells you nothing about who
it belongs to.
Mostly, you get the board list. That's the part that was hard, that's the part that's still growing, and that's the part you cannot go and fetch, because ADP publishes no index of the customers running a public career centre on it.
Query it with ?source=adp.
Quickstart · Pricing
Every source above normalises onto the same schema, with the hiring company already joined onto each posting.
Get an API key