Nexus Tools / Data guides

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.

Updated: November 2026. Everything below was tested live on 6 November 2026.

The short version

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:

SystemCareers URLId in the API
Greenhouseboards.greenhouse.io/airbnb or job-boards.greenhouse.io/airbnbboard token airbnb
Leverjobs.lever.co/palantirslug palantir
Ashbyjobs.ashbyhq.com/rampname ramp

Many companies hide the board behind their own domain. What we saw on 6 November 2026:

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"

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"

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"

3. The three side by side

FieldGreenhouseLeverAshby
Postedfirst_published, ISO with offsetcreatedAt, epoch mspublishedAt, ISO UTC
Updatedupdated_atnonenone
Salarypay_input_ranges (cents), only with pay_transparency=truesalaryRange (min, max, currency, interval)compensation, only with includeCompensation=true
Remotenone; free-text location, sometimes custom metadataworkplaceType (remote/hybrid/onsite)isRemote and workplaceType
Employment typenonecategories.commitmentemploymentType
Department / teamdepartments[], offices[] (with content=true)categories.department, categories.teamdepartment, team
Locationslocation.name (free text)categories.location, allLocations, country codelocation, secondaryLocations[]
Descriptioncontent, HTML as escaped entitiesdescription, descriptionPlain, lists[]descriptionHtml, descriptionPlain
Pagingnone (one response)limit, skipnone (one response)
robots.txt of the API hostDisallow: /embed/Crawl-delay: 1answers 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:

FieldMeaningGreenhouse / Lever / Ashby source
jobId, ats, companyId, companyNameidentityid / id / id; the board token or slug you requested
title, department, teamas published, trimmedsee table above
location, locations[], country, countryCodeprimary text, all locations, country when the system gives itlocation.name / categories + country code / location + secondaryLocations
isRemote, workplaceTypeboolean or null; remote/hybrid/onsite or nullword "remote" in the location, custom workplace field / workplaceType / isRemote, workplaceType
employmentTypefull_time, part_time, contract, ...- / commitment / employmentType
postedAt, updatedAtISO 8601 in UTCfirst_published, updated_at / createdAt / publishedAt
salary{min, max, currency, period, raw} or nullpay_input_ranges / salaryRange / compensation
jobUrl, applyUrlpublic page, application pageabsolute_url / hostedUrl, applyUrl / jobUrl, applyUrl
descriptionText, descriptionHtmlclean text and HTMLcontent (unescaped) / descriptionPlain, description / descriptionPlain, descriptionHtml
scrapedAt, sourceUrl, extrasrun 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:

What they do:

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).

SystemCompanyTitleLocationRemotePostedSalary
greenhouseAirbnbBusiness Operations and Growth Lead - DACH & CEEBerlin, Germanyno2026-11-06-
greenhouseAirbnbSenior Manager, QualityGurugram, Indiano2026-11-06-
greenhouseAirbnbSenior Account Executive, HotelsTokyo, Japanno2026-11-05-
greenhouseAirbnbSenior Manager, Business Technology EnablementUnited Statesyes2026-11-04-
greenhouseDiscordSenior Machine Learning Engineer (Growth)San Francisco Bay Area-2026-11-04-
greenhouseDiscordSenior Manager, Global Indirect TaxSan Francisco Bay Area-2026-11-03-
greenhouseDiscordAssociate Commercial CounselSan Francisco Bay Area or Rem…yes2026-11-03-
leverOutreachStaff Software Engineer, Mobile (Android and iO…Hyderabadno2026-11-05-
leverOutreachStaff Software Engineer (Mobile, Android) - Hyd…Hyderabadno2026-11-05-
leverOutreachStaff Software Engineer (Mobile, iOS) - Hyderab…Hyderabadno2026-11-05-
leverOutreachSenior Staff Software Engineer, Mobile (Android…Hyderabadno2026-11-04-
leverZooxStaff Chassis Engineer, Steering and Braking Ac…Banbury, UKno2026-11-06111,000-153,000 GBP/year
leverZooxSenior/Staff Technical Program Manager - Proces…Foster City, CAno2026-11-05193,000-279,000 USD/year
leverZooxSecond Shift Lead, Senior Logistics Operations…Hayward, CAno2026-11-0443.25-52.5 USD/hour
ashbyRampAccount Executive, Inside SalesNew York, NY (HQ)yes2026-11-06165,000 USD/year
ashbyRampManager, Account Manager | GrowthNew York, NY (HQ)yes2026-11-05180,000-247,000 USD/year
ashbyRampRecruiting Coordinator (Fixed Term)New York, NY (HQ)yes2026-11-0475,000-100,000 USD/year
ashbyPosthogAnimator & Motion Graphics DesignerRemoteyes2026-10-14-
ashbyPosthogDeveloper Marketer - SalesRemoteyes2026-10-07-
ashbyPosthogProduct Manager, Observability (ex-founder or e…Remoteyes2026-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