Greenhouse, Lever and Ashby each publish a company’s open jobs as public JSON, with no API key. This guide gives the endpoint for each, the fields you get, the parameters that add departments, descriptions and pay, and the traps that trip up a first script. At the end: how to do it for a whole company list in one run.
Each needs the company’s board name (its slug), the part after the host in its job board URL: job-boards.greenhouse.io/, jobs.lever.co/, jobs.ashbyhq.com/. Not sure which system a company uses? See how to find which ATS a company uses.
| ATS | Endpoint | Open jobs, 8 October 2026 |
|---|---|---|
| Greenhouse | boards-api.greenhouse.io/ | Stripe: 724 |
| Lever | api.lever.co/ | Palantir: 315 |
| Ashby | api.ashbyhq.com/ | Ramp: 161 |
All three return every open job in one response, so there’s no paging to get right. A slug that doesn’t exist answers 404 on all three.
curl -s https://boards-api.greenhouse.io/v1/boards/stripe/jobs | jq '.meta.total'
# 724
Each job has title, location.name, absolute_url, first_published and updated_at. Two things to know:
?content=true. Without it the response has no departments, offices or content fields at all. With it, content is the description as escaped HTML, and the response gets much larger.absolute_url is stripe.com/jobs/search?gh_jid=8172510 , its own careers site. Use absolute_url as given; don’t build a Greenhouse link from the ID.curl -s "https://api.lever.co/v0/postings/palantir?mode=json" | jq 'length'
# 315
Each posting has text (the title), categories (team, location, commitment, allLocations), workplaceType, hostedUrl, createdAt and the description as both HTML and plain text. Three traps:
jobs.eu.lever.co/seb : api.eu.lever.co/v0/postings/seb returns 119 jobs, while api.lever.co answers 404. If a slug 404s, try the other region..../postings/PALANTIR answers 404, and AI Fund’s board works only as AIFund. Copy the slug exactly from the board URL.createdAt is milliseconds since 1970, not a date string: 1786469891368. Divide by 1,000 before converting.Lever also accepts limit and skip if you want to page (?mode=json&limit=5&skip=5), but without them you get every posting.
curl -s "https://api.ashbyhq.com/posting-api/job-board/ramp?includeCompensation=true" | jq '.jobs | length'
# 161
Each job has title, department, team, location, secondaryLocations, isRemote, workplaceType, employmentType, publishedAt, jobUrl and the description as HTML and plain text.
?includeCompensation=true. Without it there is no compensation field. With it, 154 of Ramp’s 161 jobs had a pay summary such as $211.4K – $290.6K • Offers Equity..jobs, not at the top level as on Lever.The endpoints are easy once you have the slug. The slow part is a list of companies: finding each one’s system and slug, trying the EU host, mapping three different schemas into one, and being sure a board named acme really belongs to the Acme on your list. Company Jobs does that for names, domains or careers URLs, across Greenhouse, Lever, Ashby, Workday and 4 more systems (Ashby by its board URL or ashby:slug).
matchConfidence is explicit (you gave the board), high or medium. A low-confidence match returns no jobs and costs nothing.Example: input {"companies": ["stripe.com", "ashby:ramp", "https://jobs.eu.lever.co/seb"], "maxItems": 3}, run on 14 October 2026, found greenhouse:stripe (high), ashby:ramp (explicit) and lever:seb (explicit), with 725, 162 and 130 open jobs. An Ashby board is read only when you give it, as ashby:ramp or its URL: Ashby’s API gives no company name to confirm a board found from a name. The first job row, trimmed:
{
"type": "job",
"title": "Abuse Investigator",
"ats": "greenhouse",
"atsSlug": "stripe",
"matchConfidence": "high",
"url": "https://stripe.com/jobs/search?gh_jid=8172508",
"location": "Dublin",
"remote": false,
"department": "Security Analytics",
"postedAt": "2026-09-03T13:32:53-04:00",
"postedAtPrecision": "exact"
}
$0.001 per job row ($1 per 1,000), no subscription. Companies not found, or not confirmed, are free. maxItems caps job rows, and so spend, for the run.