DEV Community

agenticemail.dev
agenticemail.dev

Posted on

Every major ATS has a public job feed. Here is how to read them all.

Most job-board data products are built on scraping HTML. That is the hard way. Nearly every applicant tracking system that powers a company careers page also exposes a public JSON or XML feed, because the career page itself is just a client of that feed. If you know where the feeds live, you can read most of the world's job postings without parsing a single div.

I have spent the last year building integrations against these feeds for JobsPipe, and this post is the field guide I wish had existed when I started: the actual endpoints, the quirks of each, and the parts that stay hard even after you know the URLs.

The endpoints

All of these are public and unauthenticated. They exist so that companies can embed job widgets on their own sites, which means they are stable, documented (sometimes), and legal to read.

ATS Endpoint pattern Format
Greenhouse boards-api.greenhouse.io/v1/boards/{token}/jobs JSON
Lever api.lever.co/v0/postings/{slug}?mode=json JSON
Ashby api.ashbyhq.com/posting-api/job-board/{name} JSON
SmartRecruiters api.smartrecruiters.com/v1/companies/{id}/postings JSON
Workable apply.workable.com/api/v1/widget/accounts/{account} JSON
Recruitee {company}.recruitee.com/api/offers/ JSON
Personio {company}.jobs.personio.de/xml XML
BambooHR {company}.bamboohr.com/careers/list JSON
Workday {tenant}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs JSON, POST

A few worked examples.

Greenhouse is the friendliest. One GET per board, and content=true inlines the full HTML description:

curl "https://boards-api.greenhouse.io/v1/boards/stripe/jobs?content=true"
Enter fullscreen mode Exit fullscreen mode

Lever returns a flat array with the description split into structured lists:

curl "https://api.lever.co/v0/postings/netflix?mode=json"
Enter fullscreen mode Exit fullscreen mode

Ashby will even give you structured compensation if the company opted in:

curl "https://api.ashbyhq.com/posting-api/job-board/openai?includeCompensation=true"
Enter fullscreen mode Exit fullscreen mode

Workday is the strange one. There is no documented public API, but every Workday career site is a single-page app talking to a JSON endpoint you can call directly:

curl -X POST "https://{tenant}.wd5.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs" \
  -H "Content-Type: application/json" \
  -d '{"limit": 20, "offset": 0, "searchText": ""}'
Enter fullscreen mode Exit fullscreen mode

The quirks

Every feed has at least one.

Dates lie in different ways. Lever gives you createdAt in epoch milliseconds. Greenhouse gives you updated_at, which changes when anything about the post is edited, not when it was published. Workday gives you postedOn as a human string like "Posted 3 Days Ago", so your freshness logic ends up parsing English.

Pagination is five different ideas. Greenhouse returns everything in one response. SmartRecruiters uses limit and offset. Workable's newer endpoint uses an opaque continuation token. Workday uses offset paging but silently caps how deep you can go on some tenants. Personio is one big XML document.

Location is unstructured almost everywhere. You will see "Remote - US", "New York / London / Remote", "EMEA", and my favorite, an empty string on a posting that is very much in San Francisco. If you need country-level filtering you are building a location resolver, not reading a field.

Compensation is in the description. Outside of Ashby's opt-in field and pay-transparency jurisdictions that force structure, salary lives in free text: "up to $185k", "£70,000 - £90,000 DOE", "competitive". Extracting it reliably is an NLP problem, not a regex.

The part that stays hard

Knowing the endpoints gets you one company's jobs. The actual work is everything above that:

Tenant discovery. The endpoint needs a board token or slug, and there is no directory of them. You find tenants by noticing that careers links point at boards.greenhouse.io/{token} or jobs.lever.co/{slug}, by crawling sitemaps, and by watching for companies that migrate ATSs (they do, constantly, and their old feed keeps serving stale jobs).

Normalization. Nine sources means nine schemas for title, location, department, employment type, and date. A "Senior Backend Engineer (m/f/d)" in a Personio XML feed and a "Sr. Backend Engineer" in a Greenhouse JSON feed need to become the same shape before your queries mean anything.

Deduplication. The same role appears on the company's ATS feed, on Indeed, on LinkedIn, and on three regional boards. If you count postings without cross-source dedup, your "who is hiring" numbers are fiction.

Freshness. Feeds do not push. You are polling tens of thousands of tenants on a schedule, diffing results, and deciding what "closed" means when a job just disappears.

If you would rather not maintain all this

This is exactly the layer JobsPipe sells. We crawl the public feeds of 30+ ATSs and job boards (all of the above plus iCIMS, Taleo, SuccessFactors, Teamtailor, ZipRecruiter, and others), normalize everything to one schema with resolved countries and parsed salary ranges, dedupe across sources, and serve it as a single REST endpoint:

curl -X POST "https://api.jobspipe.dev/v1/jobs/search" \
  -H "Authorization: Bearer jp_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "job_title_or": ["data engineer"],
    "job_country_code_or": ["US"],
    "remote": true,
    "posted_at_max_age_days": 7,
    "limit": 20
  }'
Enter fullscreen mode Exit fullscreen mode

Every result comes back in the same shape regardless of whether it started life in a Workday tenant or a Lever board: job_title, company, location, country_code, remote, seniority, date_posted, final_url.

There is a free tier (100 requests/month, no code required beyond the curl above), API docs here, an open-source CLI and agent skill on GitHub, and an MCP server at jobspipe.dev/mcp if you want your coding agent to query it directly.

And if you would rather build the crawler yourself: the table above is a real starting point, and honestly, it is a fun problem for about the first six ATSs. It is the 25th one that gets you.

Top comments (0)