Company job postings as JSON from the public Greenhouse, Lever and Ashby APIs: endpoints, field differences, a Python example and a free sample
How to read the open jobs of a company that hires through Greenhouse, Lever or Ashby straight from the public JSON endpoints those systems offer for career pages, how to tell which one a company uses, how the fields differ (salary, remote, dates), where the limits are, and when an Actor saves you the code.
The short version
- All three systems publish the jobs of a company as plain JSON over HTTPS, with no account, key or login:
boards-api.greenhouse.io/v1/boards/{token}/jobs,api.lever.co/v0/postings/{slug}andapi.ashbyhq.com/posting-api/job-board/{name}. They exist so that companies can show their jobs on their own site, and each returns the whole board in one response. - The hard part is not the request, it is the differences: salary only appears if you pass the right flag (Greenhouse
pay_transparency=true, AshbyincludeCompensation=true), dates come in three formats, and "remote" is a free-text location, a workplace type or a boolean depending on the system. - The one rate rule we found in writing is Lever's: its
robots.txtsaysCrawl-delay: 1. Boards are big (Anthropic's Greenhouse board was 9.9 MB in one response), so fetch one company at a time and keep what you need. - This page gives the endpoints with curl, one Python script (about 90 lines, tested today) that handles all three and writes a CSV, a field comparison, a normalized schema, and a free 20-job sample.
1. Which system does a company use?
Start from the careers URL. A hosted board has a recognizable host, and the first path segment is the id you need:
| System | Careers URL | Id in the API |
|---|---|---|
| Greenhouse | boards.greenhouse.io/airbnb or job-boards.greenhouse.io/airbnb | board token airbnb |
| Lever | jobs.lever.co/palantir | slug palantir |
| Ashby | jobs.ashbyhq.com/ramp | name ramp |
Many companies hide the board behind their own domain. What we saw on 6 November 2026:
boards.greenhouse.io/airbnbanswersHTTP 200after redirecting tocareers.airbnb.com/positions/. The token still works in the API. Job links on that site carry?gh_jid=8306554, and the HTML of a job page includes a scriptboards.greenhouse.io/embed/job_board/js?for=airbnb: thefor=value is the token. Agh_jidparameter in a job URL is a good Greenhouse hint.- The static HTML of
discord.com/careerscontains JavaScript that callsapi.greenhouse.io/v1/boards/${board}/jobs?content=truefor a list of boards. - The static HTML of
palantir.com/careers/links tojobs.lever.co/palantir, and those ofnotion.com/careersandlinear.app/careerslink tojobs.ashbyhq.com/notionandjobs.ashbyhq.com/Linear. - Some pages give no hint in the HTML you fetch:
stripe.com/jobsandspotifyjobs.comshowed nothing. Stripe's tokenstripeworks in the Greenhouse API (766 jobs) and Spotify's slugspotifyin Lever's (85 postings), but we only know that by trying. When the HTML is silent, open the browser developer tools on the careers page and look at the network requests for one of the hosts above. - Guessing is cheap and safe: an unknown id returns
HTTP 404on all three (Greenhouse{"status":404,"error":"Job not found"}, Lever{"ok":false,"error":"Document not found"}, Ashby the plain textNot Found). Ids are not always the brand name, so a 404 does not prove the company is on another system. - Lever also has an EU region (
api.eu.lever.co). We did not find a real EU account to test, so we cannot tell you how those slugs behave.
2. The three endpoints, with curl
Greenhouse Job Board API
Documentation: docs.greenhouse.io/job-board.html. It states that Job Board data is public and that no authentication is needed for any GET endpoint (only the application-submission POST needs a key).
# list: id, title, location, dates, URL (155,048 bytes for Airbnb's 202 jobs)
curl -s "https://boards-api.greenhouse.io/v1/boards/airbnb/jobs"
# the same list plus description HTML, departments and offices (2,909,857 bytes)
curl -s "https://boards-api.greenhouse.io/v1/boards/airbnb/jobs?content=true"
# salary ranges: add pay_transparency=true
curl -s "https://boards-api.greenhouse.io/v1/boards/discord/jobs?content=true&pay_transparency=true"
# one job
curl -s "https://boards-api.greenhouse.io/v1/boards/discord/jobs/{id}?pay_transparency=true"
- The response is
{"jobs": [...], "meta": {"total": 202}}: no paging, everything in one response (Anthropic: 673 jobs, 9,857,703 bytes withcontent=true). departmentsandofficesonly appear withcontent=true. Thecontentfield is HTML escaped as entities (it starts with<div): unescape it before use.- Salary. Without
pay_transparency=truewe found nopay_input_rangeson any board. With it, Discord returned a range on 26 of 26 jobs and Anthropic on 584 of 673; Stripe on 0 of 766, because Stripe publishes none. Each range is{"min_cents": 20000000, "max_cents": 22500000, "currency_type": "USD", "title": "Base Salary", "blurb": "..."}. The documentation lists this parameter on the single-job endpoint; it also works on the list endpoint, as the Discord and Anthropic numbers show. - Dates:
first_publishedandupdated_at, ISO 8601 with the company's UTC offset (2026-10-28T10:52:05-04:00). - Remote: there is no field.
location.nameis free text ("San Francisco Bay Area or Remote (U.S.)"), and some boards add a custommetadataentry, e.g. Airbnb's"Workplace Type": "Remote".
Lever Postings API
Documentation: github.com/lever/postings-api.
# every published posting of a company (Palantir: 311 postings, 6,089,826 bytes)
curl -s "https://api.lever.co/v0/postings/palantir?mode=json"
# paging and filters: limit, skip, and team / department / location / commitment
curl -s "https://api.lever.co/v0/postings/outreach?mode=json&limit=2&skip=1"
- The response is a JSON array.
limitandskipwork (we got 2 items back forlimit=2&skip=1); without them you get everything. - Description as
description(HTML) anddescriptionPlain, pluslists[](responsibilities, requirements) andadditional/additionalPlain. - Date:
createdAtin epoch milliseconds (UTC). There is no update date. categoriesholdscommitment("Full-time"),department,team,locationandallLocations.workplaceTypewasremote,hybridoronsitein every posting we looked at (Spotify: 28 remote, 45 hybrid, 12 onsite of 85).- Salary:
salaryRange{"min": 123000, "max": 168000, "currency": "USD", "interval": "per-year-salary"}plussalaryDescriptionPlain, only on postings where the company published one. Zoox: 216 of 238 postings; Outreach: 12 of 22; Palantir: 0 of 311; Spotify: 0 of 85.intervalis a Lever string such asper-year-salary; Zoox also has an hourly posting (the Actor maps it to periodhour, 43.25 to 52.5 USD). api.lever.co/robots.txtsaysUser-agent: *,Allow: /,Crawl-delay: 1. Honor it: at most one request per second, one at a time.
Ashby public job posting API
# every listed job, with compensation (Ramp: 168 jobs, 3,024,560 bytes)
curl -s "https://api.ashbyhq.com/posting-api/job-board/ramp?includeCompensation=true"
- The response is
{"apiVersion": ..., "jobs": [...]}, everything in one response. WithoutincludeCompensation=truethecompensationkey was absent from all 168 Ramp jobs; with it, present on all 168. - Compensation is a structure:
compensationTierSummary("$211.4K – $290.6K • Offers Equity"),scrapeableCompensationSalarySummary("$211.4K - $290.6K") andsummaryComponents[]withcompensationType(Salary, EquityPercentage...),interval("1 YEAR"),currencyCode,minValue,maxValue. The summary was non-empty on 157 of Ramp's 168 jobs. - Date:
publishedAt, ISO 8601 UTC with milliseconds. No update date. - Remote:
isRemote(boolean) andworkplaceType(Remote,Hybrid,OnSite). They do not mean the same thing: of Ramp's 168 jobs, 134 wereHybridwithisRemote: true, 17Remotewithtrueand 17OnSitewithfalse. TrustworkplaceTypewhen you need a strict remote filter. - Others:
employmentType(FullTime,Intern,Temporary),department,team,locationplussecondaryLocations[]with a postal address,jobUrl,applyUrl,isListed. Titles can carry a leading space (one Ramp title did), so strip them.
3. The three side by side
| Field | Greenhouse | Lever | Ashby |
|---|---|---|---|
| Posted | first_published, ISO with offset | createdAt, epoch ms | publishedAt, ISO UTC |
| Updated | updated_at | none | none |
| Salary | pay_input_ranges (cents), only with pay_transparency=true | salaryRange (min, max, currency, interval) | compensation, only with includeCompensation=true |
| Remote | none; free-text location, sometimes custom metadata | workplaceType (remote/hybrid/onsite) | isRemote and workplaceType |
| Employment type | none | categories.commitment | employmentType |
| Department / team | departments[], offices[] (with content=true) | categories.department, categories.team | department, team |
| Locations | location.name (free text) | categories.location, allLocations, country code | location, secondaryLocations[] |
| Description | content, HTML as escaped entities | description, descriptionPlain, lists[] | descriptionHtml, descriptionPlain |
| Paging | none (one response) | limit, skip | none (one response) |
| robots.txt of the API host | Disallow: /embed/ | Crawl-delay: 1 | answers 401 with no rules |
A note on Ashby and robots.txt: jobs.ashbyhq.com/robots.txt disallows /api/ on the careers site host, but the documented posting API lives on api.ashbyhq.com, whose robots.txt answers 401 Unauthorized (no rules). We use the second host only.
4. A normalized schema
If you combine companies from several systems you need one record shape. This is the one the Actors below output (schema version 1.0), with the source field each value comes from:
| Field | Meaning | Greenhouse / Lever / Ashby source |
|---|---|---|
jobId, ats, companyId, companyName | identity | id / id / id; the board token or slug you requested |
title, department, team | as published, trimmed | see table above |
location, locations[], country, countryCode | primary text, all locations, country when the system gives it | location.name / categories + country code / location + secondaryLocations |
isRemote, workplaceType | boolean or null; remote/hybrid/onsite or null | word "remote" in the location, custom workplace field / workplaceType / isRemote, workplaceType |
employmentType | full_time, part_time, contract, ... | - / commitment / employmentType |
postedAt, updatedAt | ISO 8601 in UTC | first_published, updated_at / createdAt / publishedAt |
salary | {min, max, currency, period, raw} or null | pay_input_ranges / salaryRange / compensation |
jobUrl, applyUrl | public page, application page | absolute_url / hostedUrl, applyUrl / jobUrl, applyUrl |
descriptionText, descriptionHtml | clean text and HTML | content (unescaped) / descriptionPlain, description / descriptionPlain, descriptionHtml |
scrapedAt, sourceUrl, extras | run time, API URL, system-specific leftovers | - |
Two rules keep it honest: a missing value is null, never a guess, and the word "remote" can make isRemote true but is never used to set it to false.
5. DIY: three systems to one CSV in Python
This script takes careers URLs, detects the system, fetches the board, reduces every job to eight columns and writes jobs.csv. Run on 6 November 2026 on Discord (Greenhouse), Zoox (Lever) and Ashby's Ramp it returned 432 rows (26 + 238 + 168), of which 399 had a salary range: 26 of 26 for Discord, 216 of 238 for Zoox, 157 of 168 for Ramp. It needs pip install requests. Put a real contact in the User-Agent.
import csv
import re
import time
from datetime import datetime, timezone
from urllib.parse import urlparse
import requests
HEADERS = {"User-Agent": "my-script/1.0 (contact: you@example.com)"}
def detect(url):
"""Careers URL -> (ats, slug), or None if it is not a hosted board URL."""
u = urlparse(url if "//" in url else "https://" + url)
host, parts = (u.hostname or "").lower(), [p for p in u.path.split("/") if p]
if host in ("boards.greenhouse.io", "job-boards.greenhouse.io") and parts:
return "greenhouse", parts[0]
if host == "jobs.lever.co" and parts:
return "lever", parts[0]
if host == "jobs.ashbyhq.com" and parts:
return "ashby", parts[0]
return None
def get(url, **params):
r = requests.get(url, params=params, headers=HEADERS, timeout=60)
r.raise_for_status()
return r.json()
def iso_ms(ms):
return datetime.fromtimestamp(ms / 1000, timezone.utc).isoformat(timespec="seconds")
def job(ats, slug, title, loc, remote, posted, url, salary):
return {"ats": ats, "company": slug, "title": (title or "").strip(), "location": loc,
"remote": remote, "posted": posted, "url": url, "salary": salary}
def greenhouse(slug):
data = get(f"https://boards-api.greenhouse.io/v1/boards/{slug}/jobs", pay_transparency="true")
for j in data["jobs"]:
pr = (j.get("pay_input_ranges") or [None])[0]
sal = f"{pr['min_cents'] / 100:.0f}-{pr['max_cents'] / 100:.0f} {pr['currency_type']}" if pr else None
loc = (j.get("location") or {}).get("name")
yield job("greenhouse", slug, j["title"], loc, None, j.get("first_published"), j["absolute_url"], sal)
def lever(slug):
for j in get(f"https://api.lever.co/v0/postings/{slug}", mode="json"):
sr, cat = j.get("salaryRange"), j.get("categories") or {}
sal = f"{sr['min']}-{sr['max']} {sr['currency']} ({sr['interval']})" if sr else None
remote = j.get("workplaceType") == "remote" if j.get("workplaceType") else None
yield job("lever", slug, j["text"], cat.get("location"), remote, iso_ms(j["createdAt"]), j["hostedUrl"], sal)
def ashby(slug):
data = get(f"https://api.ashbyhq.com/posting-api/job-board/{slug}", includeCompensation="true")
for j in data["jobs"]:
sal = (j.get("compensation") or {}).get("scrapeableCompensationSalarySummary")
remote = j["workplaceType"] == "Remote" if j.get("workplaceType") else None
yield job("ashby", slug, j["title"], j.get("location"), remote, j.get("publishedAt"), j["jobUrl"], sal)
FETCH = {"greenhouse": greenhouse, "lever": lever, "ashby": ashby}
def main(urls):
rows = []
for url in urls:
found = detect(url)
if not found:
print("not a Greenhouse/Lever/Ashby board URL:", url)
continue
ats, slug = found
rows += list(FETCH[ats](slug))
print(ats, slug, len(rows), "jobs so far")
if ats == "lever":
time.sleep(1) # Lever's robots.txt asks for Crawl-delay: 1
with open("jobs.csv", "w", newline="", encoding="utf-8-sig") as f:
w = csv.DictWriter(f, fieldnames=list(rows[0]))
w.writeheader()
w.writerows(rows)
main(["https://job-boards.greenhouse.io/discord",
"https://jobs.lever.co/zoox",
"https://jobs.ashbyhq.com/ramp"])
Things the script does on purpose: it sleeps one second after each Lever company (the Crawl-delay), uses workplaceType and not Ashby's isRemote for the remote column, and leaves remote empty for Greenhouse because the API has no such field. Description fields are not kept; add content=true (Greenhouse) and read descriptionPlain (Lever, Ashby) if you need them.
Limits worth stating: we did not find a published rate limit for Greenhouse or Ashby and did not stress them. Boards are one response of up to several MB, so do not poll a hundred companies in parallel. Postings are what each employer chose to publish; unlisted or internal jobs are not in these feeds. The postings are the employers' content: do not use them to contact individuals, and check the employer's terms for your use.
6. When the Actors save time
For one company the script above is enough, and free. The Apify Actors by Nexus Scrapers run the same public endpoints and add what you would otherwise build:
- Greenhouse Jobs Scraper, Lever Jobs Scraper and Ashby Jobs Scraper: one per system, for when you only need that one.
- Career Site Jobs Scraper: the same schema for Greenhouse, Lever, Ashby and also Workable, Recruitee, Personio and Workday (see our Workday guide).
What they do:
- Companies mode: give career URLs,
ats:identries (lever:palantir) or a company website (the Actor fetches that one page and looks for an embedded board,robots.txtrespected; it finds nothing if the page builds its job list with JavaScript). - Discovery mode: leave the company list empty and it scans the bundled company directory (biggest boards first, up to
maxCompanies, default 200) and filters live by title keyword, location, remote flag and posting date. - The normalized schema above, in JSON, CSV or Excel, with scheduling, webhooks and API/MCP access from Apify.
- Politeness built in: honest User-Agent,
robots.txtread per host, Lever limited to one request per second, retries with backoff on 429/5xx, no proxies, no browser. - No personal data is extracted: no recruiter names or e-mail addresses are copied into fields.
Pricing is pay per result; the Actor page shows the current price.
Since build 0.1.2 the Actors request the Greenhouse feed with pay_transparency=true, so salary is filled for Greenhouse jobs whenever the company publishes a range (the 20-job sample on this page was taken before that change). What they do not do: Ashby's isRemote is passed through as published, so hybrid Ramp jobs show isRemote: true with workplaceType: hybrid; filter on workplaceType. There is no "only jobs new since the last run" mode yet: schedule runs and compare jobIds, or use postedWithinDays.
7. Real sample: 20 normalized jobs
These 20 jobs were produced on 6 November 2026 by running the Actor's own code locally on the live endpoints (no Apify platform involved): the newest postings of six public boards, Airbnb 4 and Discord 3 (Greenhouse), Outreach 4 and Zoox 3 (Lever), Ramp 3 and PostHog 3 (Ashby). They are the first postings in newest-first order, not hand-picked. Description text is cut at 1,500 characters and the HTML description is left out to keep the files small. A scrub pass for e-mail addresses ran over every text field and none remain in the files. Salary shows only where the board published one and the Actor read it (see the note above for Greenhouse).
Download: sample.json (full records, UTF-8) and sample.csv (flattened, lists joined with | , opens in Excel).
| System | Company | Title | Location | Remote | Posted | Salary |
|---|---|---|---|---|---|---|
| greenhouse | Airbnb | Business Operations and Growth Lead - DACH & CEE | Berlin, Germany | no | 2026-11-06 | - |
| greenhouse | Airbnb | Senior Manager, Quality | Gurugram, India | no | 2026-11-06 | - |
| greenhouse | Airbnb | Senior Account Executive, Hotels | Tokyo, Japan | no | 2026-11-05 | - |
| greenhouse | Airbnb | Senior Manager, Business Technology Enablement | United States | yes | 2026-11-04 | - |
| greenhouse | Discord | Senior Machine Learning Engineer (Growth) | San Francisco Bay Area | - | 2026-11-04 | - |
| greenhouse | Discord | Senior Manager, Global Indirect Tax | San Francisco Bay Area | - | 2026-11-03 | - |
| greenhouse | Discord | Associate Commercial Counsel | San Francisco Bay Area or Rem… | yes | 2026-11-03 | - |
| lever | Outreach | Staff Software Engineer, Mobile (Android and iO… | Hyderabad | no | 2026-11-05 | - |
| lever | Outreach | Staff Software Engineer (Mobile, Android) - Hyd… | Hyderabad | no | 2026-11-05 | - |
| lever | Outreach | Staff Software Engineer (Mobile, iOS) - Hyderab… | Hyderabad | no | 2026-11-05 | - |
| lever | Outreach | Senior Staff Software Engineer, Mobile (Android… | Hyderabad | no | 2026-11-04 | - |
| lever | Zoox | Staff Chassis Engineer, Steering and Braking Ac… | Banbury, UK | no | 2026-11-06 | 111,000-153,000 GBP/year |
| lever | Zoox | Senior/Staff Technical Program Manager - Proces… | Foster City, CA | no | 2026-11-05 | 193,000-279,000 USD/year |
| lever | Zoox | Second Shift Lead, Senior Logistics Operations… | Hayward, CA | no | 2026-11-04 | 43.25-52.5 USD/hour |
| ashby | Ramp | Account Executive, Inside Sales | New York, NY (HQ) | yes | 2026-11-06 | 165,000 USD/year |
| ashby | Ramp | Manager, Account Manager | Growth | New York, NY (HQ) | yes | 2026-11-05 | 180,000-247,000 USD/year |
| ashby | Ramp | Recruiting Coordinator (Fixed Term) | New York, NY (HQ) | yes | 2026-11-04 | 75,000-100,000 USD/year |
| ashby | Posthog | Animator & Motion Graphics Designer | Remote | yes | 2026-10-14 | - |
| ashby | Posthog | Developer Marketer - Sales | Remote | yes | 2026-10-07 | - |
| ashby | Posthog | Product Manager, Observability (ex-founder or e… | Remote | yes | 2026-10-06 | - |
"Remote" is the normalized isRemote: yes, no, or - when the system does not say.
8. Terms
These are public endpoints that each vendor offers for building career pages (Greenhouse and Lever document them for that purpose, Ashby calls its endpoint the public job posting API), but the postings belong to the employers and we reviewed no terms of service beyond that. Use job data to display or analyse openings, not to harvest personal data, and read the terms that apply to your use. This is not legal advice.
Links
- Career Site Jobs Scraper on Apify (Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Workday)
- Greenhouse Jobs Scraper, Lever Jobs Scraper, Ashby Jobs Scraper
- Greenhouse Job Board API documentation
- Lever Postings API documentation
- The companion guide: Workday career sites
- The same kind of guide for EU tenders (TED)