Salary & Compensation

Greenhouse’s job board API has no salary field. Lever’s does.

Hari Priya Vemula Hari Priya Vemula August 2, 2026 6 min read
Greenhouse’s job board API has no salary field. Lever’s does.

Every job posting on Stripe’s careers page carries a URL where you can ask not to be evaluated by AI. All 548 of them, each one distinct. Not one of those 548 postings carries a salary field.

I found this while building a job index, and it turned into a small lesson about what job boards choose to make machine readable. Both Greenhouse and Lever run fully public job board APIs. No account, no key, no OAuth dance. You can reproduce everything below with curl right now.

Both APIs are open, and almost nobody mentions it

Greenhouse documents a Job Board API that takes a board token in the path:

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

Lever’s postings API works the same way:

curl "https://api.lever.co/v0/postings/zoox?mode=json"

The board token is usually just the company name, lowercased. That is the whole authentication story. If a company hosts its careers page on either platform, its open roles are already a public JSON feed, and most people scrape the HTML anyway.

Lever gives you a typed object

Here is what a Lever posting hands back when the employer fills in pay:

{
  "min": 143000,
  "max": 177000,
  "currency": "USD",
  "interval": "per-year-salary"
}

That is salaryRange, a first-class field on the posting. Zoox populates it on 217 of its 226 open roles, which is 96%. You can sort by it. You can filter on it. You can convert currencies without touching a single regex.

Greenhouse gives you an HTML blob

I pulled all 548 Stripe postings and took the union of every key across every one of them, in case some roles carried fields others didn’t. The complete list:

absolute_url, ai_disclaimer, ai_opt_out_request_url, application_deadline, company_name, content, data_compliance, departments, education, first_published, id, include_ai_disclaimer, internal_job_id, language, location, metadata, offices, requisition_id, title, updated_at

There is no pay field. There is no compensation field. The metadata array, which sounds like the obvious escape hatch, came back empty on all 548.

So pay, when it appears at all, is buried in content as escaped HTML:

Salary: $182,208 - $236,580/yr.
This salary range represents the base salary range for the...

To get that out you write a regex. Mine looks for a dollar figure in the thousands, or one of the phrases “salary range”, “base pay range”, “compensation range”, “pay range”. On Stripe’s board it matches 19 postings out of 548, about 3.5%. That regex under-counts for sure, and I don’t know by how much. It is tuned for dollars and English phrasing, so a role quoting rupees or euros without a keyword slips straight past it.

The part I did not expect

Greenhouse has no field for pay. It does have ai_opt_out_request_url, populated on all 548 postings, with a distinct URL for each one.

Read that ordering again. The schema found room for AI evaluation opt-out plumbing before it found room for a number.

Checking any board yourself

This is short enough to paste into a terminal. It prints the structured-pay rate for one Lever board:

python3 - <<'EOF'
import urllib.request, json
board = "zoox"
url = f"https://api.lever.co/v0/postings/{board}?mode=json"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
posts = json.loads(urllib.request.urlopen(req, timeout=180).read())
n = sum(1 for p in posts if p.get("salaryRange"))
print(f"{board}: {n}/{len(posts)} postings carry salaryRange")
EOF

One warning from experience. These responses get large, several megabytes for a big board, and curl quietly handed me a truncated file more than once until I raised the timeout. If your JSON parser complains about an unterminated string, that is what happened. You did not hit a malformed API.

Swap the URL for the Greenhouse one and there is nothing to count. That is rather the point.

What it looks like across 101,432 postings

Stripe and Zoox are two boards. We run a live index of open postings collected from both platforms, and as of the 1 August 2026 snapshot it held 101,432 postings from 2,704 companies. That is a big enough sample to check whether the pattern holds.

Greenhouse Lever
Postings 49,722 51,710
Structured pay field 0 (0.0%) 15,060 (29.1%)
Pay stated anywhere, including prose 37.4% 30.3%

The last row is the one that surprised me. Greenhouse employers state pay more often than Lever employers do. They just have nowhere structured to put it.

So the disclosure gap is not really about employers being secretive. On these two platforms it is substantially a schema problem. The willingness is there and the field is not.

A gotcha if you build on this

Lever omits salaryRange entirely when it is unset. It does not send null. The key is simply absent.

Palantir’s board is a clean example: 302 open postings, zero with the key. Compare that to Zoox at 217 of 226. From the JSON alone you cannot distinguish “this employer declined to publish pay” from “this employer published it in the description instead”. Both look identical, which is a missing key.

If you are computing a disclosure rate, that distinction matters a lot, and the API will not help you draw it. Treat every disclosure number you produce as a lower bound. I do.

Why any of this matters

Google’s JobPosting structured data guidelines have supported baseSalary for years, and recommend it. A board that cannot emit the field cannot emit the markup properly either, no matter what the employer typed into the description box.

Which means the pay-transparency laws now on the books in several US states run into a plumbing limit before they run into an employer’s willingness. You can mandate the disclosure. You cannot mandate that it be queryable.

The full aggregate dataset behind the table above is published as CSV under CC BY 4.0, with a DOI and the method notes, at the job posting pay transparency study. Both APIs are public, so you can check the two-request version yourself in about a minute.

FAQ

Do the Greenhouse and Lever job board APIs need an API key?

No. Both are public read-only endpoints that need no account, key, or token. Greenhouse uses boards-api.greenhouse.io/v1/boards/<board>/jobs and Lever uses api.lever.co/v0/postings/<board>. The board name in the path is typically the company name in lowercase. Greenhouse’s separate Harvest API, which reads candidate data, does require authentication.

Does the Greenhouse job board API return salary information?

Not as a structured field. Across 548 Stripe postings checked on 2 August 2026, no pay or compensation key existed on any posting, and the metadata array was empty throughout. Any pay information sits inside the content field as HTML and has to be parsed out of prose. About 3.5% of that board’s postings mention pay this way.

What is the Lever salaryRange field?

An object with min, max, currency, and interval, attached to a posting when the employer fills it in. Zoox populates it on 96% of its open roles. Note that Lever omits the key completely rather than sending null when it is unset, so absence does not prove the employer declined to disclose.

Can I use these APIs to build a job board?

Technically yes, and many aggregators do. Check each platform’s terms first, keep a sane request rate, and cache. Also note that a posting’s presence in the feed is not proof the role is genuinely open. In our index 13,636 postings had been listed for over a year.

Why do so few job postings include salary in the API?

Mostly because one of the two platforms has no field for it. Greenhouse employers actually mention pay in the description slightly more often than Lever employers do, 37.4% against 30.3%, but none of it lands anywhere a machine can filter on. The gap is structural rather than behavioural.

Hari Priya Vemula

Written by

Hari Priya Vemula

Covers interview preparation and the candidate experience, from the first screen through to the final round.