If you want every open job at a company, you usually don't need a scraper. Most tech companies don't host their own job listings. They use an applicant tracking system (ATS), and the popular ones publish the job board as plain JSON, with no login or API key, because they want the jobs to be found.

I run an index of these boards, about 310,000 open jobs from 18,000 companies, refreshed every 8 hours. This post is what I learned building it: the endpoints, how to work out which ATS a company uses, and the parts that break.

The endpoints

Each ATS identifies a company by a short slug, the "board token". Once you have it, one request gets you the whole board.

Greenhouse

GET https://boards-api.greenhouse.io/v1/boards/{slug}/jobs?content=true 

Everything comes back in one response. content=true adds the description as escaped HTML. Without it the response is much smaller, which helps if you only want titles and locations.

Lever

GET https://api.lever.co/v0/postings/{slug}?mode=json&skip=0&limit=500 

Returns an array. Page with skip until you get fewer than limit back. Some companies are on Lever's EU instance, which is api.eu.lever.co.

Ashby

GET https://api.ashbyhq.com/posting-api/job-board/{slug}?includeCompensation=true 

Ashby is the friendliest of the three. includeCompensation=true gives you structured salary ranges when the company has filled them in, and AI startups have mostly moved here.

Smaller ones, in case you hit them:

Workable: GET https://apply.workable.com/api/v1/widget/accounts/{slug} SmartRecruiters: GET https://api.smartrecruiters.com/v1/companies/{slug}/postings?limit=100&offset=0 Recruitee: GET https://{slug}.recruitee.com/api/offers/ BambooHR: GET https://{slug}.bamboohr.com/careers/list Workday: POST https://{tenant}.{wdN}.myworkdayjobs.com/wday/cxs/{tenant}/{site}/jobs body: {"limit":20,"offset":0,"searchText":"","appliedFacets":{}} 

Workday is the awkward one. It needs three values instead of one slug, pages 20 at a time, and only gives you the description if you make a second request per job.

Finding which ATS a company uses

The quickest way: open the company's careers page and look at where the "Apply" links go. boards.greenhouse.io/stripe, jobs.lever.co/... and jobs.ashbyhq.com/... all have the slug right in the URL.

To do it in code, fetch the careers page and look for those hostnames:

const PATTERNS = { greenhouse: /(?:boards|job-boards)(?:\.eu)?\.greenhouse\.io\/(?:embed\/job_board\?for=)?([\w-]+)/, lever: /jobs\.(?:eu\.)?lever\.co\/([\w.-]+)/, ashby: /jobs\.ashbyhq\.com\/([\w.%-]+)/, }; async function detectAts(careersUrl) { const html = await (await fetch(careersUrl)).text(); for (const [ats, re] of Object.entries(PATTERNS)) { const m = html.match(re); if (m) return { ats, slug: m[1] }; } return null; } 

I tried it on five careers pages while writing this. It found Notion, Linear and Ramp on Ashby and Figma on Greenhouse. It missed Anthropic, whose page builds the job list with JavaScript, so the links aren't in the HTML (they're on Greenhouse, slug anthropic).

When that happens, guess instead. Take the company name, try a few spellings (acme, acmeinc, acme-inc) against each endpoint, and keep whichever answers with jobs. It's crude, but it finds most of them.

What breaks

This is the part that took me the longest.

Boards die quietly. I started from public lists of known company boards, about 27,000 of them. On the first full crawl, 9,140 were dead: a third. On Lever it was more than half. Usually the company switched ATS, got acquired, or stopped hiring. When I went looking for the switchers, I found 296 boards that had reappeared on another ATS, and 212 of them had moved to Ashby. A dead Greenhouse board returns a 404, which is easy to spot. Some others return an empty list, which looks exactly like a company that just has no openings. If you're monitoring specific companies, a board that suddenly drops to zero jobs is worth a second look.

Salary is free text. Ashby gives you structured numbers. Greenhouse and Lever mostly bury the range inside the description: "$95,000 - $125,000", "€60k–75k", "$45/hr". Parsing it is easy for the common cases and then gets weird. My parser once read a yearly salary as hourly because there was a "$45/hr" note further down the page for contractors, and turned a normal $125k job into $260 million a year. Sales jobs mention quota targets like "$2M–$5M" that look exactly like pay. I now reject any annual figure outside a sane band, try the other likely period (hour, month, year) once, and if it still doesn't fit I leave the salary empty. A missing salary is better than a wrong one.

Roughly, here's how much salary data you can expect from each, from my index today: Greenhouse 49% of jobs, Ashby 49%, Lever 42%, BambooHR almost none.

Locations are a mess. "Remote", "Remote - US", "US or Canada (Remote)", "Anywhere", "NYC / SF / Remote". If you need a country filter, you'll be writing a lot of small rules. Treat "Remote" with no country as unknown, not as "worldwide".

Be polite. These endpoints are public, but they're not built for hammering. I keep it to a few requests at a time per ATS, identify myself in the User-Agent, and refresh each board every 8 hours. Jobs don't change faster than that.

If you don't want to maintain this

Everything above fits in a weekend script for ten companies. Keeping it working for thousands of companies is the ongoing part: boards move, parsers drift, new ATSs show up.

That's the part I turned into a tool. It's on Apify as "Company Job Search API" (lucymade/ats-job-search). You give it company names or careers-page URLs, or just keywords, and it returns the jobs in one format, with salary converted to a yearly number, plus country, remote status, seniority and visa sponsorship when the posting mentions it. It can also return only the jobs that are new since the last run, which is what most people want for job alerts. It costs $2 per 1,000 jobs, and you can try it on Apify's free credit.

If you build your own instead, I hope the list of what breaks saves you a week. Happy to answer questions in the comments.