Every major applicant tracking system publishes its job board as a public JSON endpoint. That part is already well documented, and I'm not going to rewrite it.
What isn't documented is what happens once you actually try to read all of them and put the results in one table. I built a normalizer across seven ATS — Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters and Recruitee — and every single interesting bug came from an API that answered 200 OK while telling me something false.
These are the five that cost me real time. All of them are verifiable with curl, no authentication, no account. Workday silently caps limit at 20 — and returns an empty array if you exceed it
The obvious optimization is to raise limit to cut the number of round trips. Try it:
You get 200 OK. You get a well-formed response body. And jobPostings is an empty array.
This is the worst possible failure mode, because a naive implementation doesn't crash — it concludes "this company has no open roles" and moves on. If you're running across hundreds of tenants, you will silently emit zero rows for every Workday employer and the run will look perfectly healthy.
I lost an afternoon to this before I thought to compare limit: 20 against limit: 100 on the same tenant. Workday stops paginating at 10,000 postings
On large tenants, pagination stops returning new results once offset passes 10,000. There's no flag in the response telling you that you've hit a wall rather than the end of the list.
If you don't cap explicitly, your loop either terminates on an empty page and under-reports, or spins. Cap it and record that you capped it:
The general principle, and the reason both of these bit me: an ATS telling you "no more results" and an ATS refusing to give you more results look identical over HTTP. You have to decide which one you're looking at, and log it. There is no public directory of Workday tenants
Greenhouse, Lever and Ashby all take a single slug you can guess from the company's domain. Workday needs three values — host, tenant and site:
wd5 isn't guessable (it's whichever Workday cluster the customer landed on), and the site name is free text the customer chose. There's no registry, no lookup API, nothing to enumerate.
Practical consequence: for six of the seven ATS you can auto-discover a board from a company domain. For Workday you cannot — the only reliable source of those three values is the careers URL itself. So I let users paste the URL and I parse it, and I left Workday out of automatic discovery entirely rather than pretend it works.
Being explicit about what can't be automated turned out to be more useful than a discovery function that quietly misses every Workday employer. Ashby has salary data, but only if you ask for it by name
Postings come back with title, location, employmentType, descriptionHtml and friends. No compensation field anywhere — not null, absent. Easy to conclude Ashby doesn't expose salary.
Now every posting carries a compensation object with tier summaries, currency codes and min/max values. On the board I tested, 131 of 137 postings had compensation data that the default response omits entirely.
An absent field reads exactly like "this data doesn't exist." Here it meant "you didn't ask." Three of the seven never tell you the company's name
This one I only caught by auditing my own output. I noticed 30% of my rows had companyName: null, and the nulls weren't random — they were 100% of the rows from specific sources.
