How many job postings show a salary? We measured 6,422 jobs on Greenhouse, Lever and Ashby
Last updated: 4 November 2026 · By Quicksloth
Greenhouse, Lever and Ashby, three of the most used applicant tracking systems, each have a public API with a salary field. We read every open job on 26 company job boards on 4 November 2026 and counted how often that field is filled, and how often the pay range is only written in the job text. This page gives the numbers by system and by company, the exact fields, and a free script that repeats the count on the boards you choose.
Short version: 34% of the 6,422 jobs had a salary in a field of its own, and another 13% had a pay range only in the description: 47% in all. The system matters less than the company. Ashby boards filled the field most often (67% of jobs), Greenhouse 38%, Lever 3%, but on every system some companies were at 0% and others at 100%. Lever employers mostly write the range in the text: 26% of Lever jobs had it there.
What we measured
- Sample: 26 job boards of well-known software and online-service companies in the United States and Europe, 10 on Greenhouse, 8 on Lever and 8 on Ashby. We left out defense, aerospace, aviation and drone companies. It is a convenience sample of companies we knew used each system, not a random one: read the shares as an indication, not as a market statistic.
- One request per board on the afternoon of 4 November 2026 (UTC), to each system's public job board API with salaries switched on:
pay_transparency=truefor Greenhouse,includeCompensation=truefor Ashby (Lever returnssalaryRangeby default). - "With a salary field": the job has a minimum or a maximum in the API's salary field. "Only in the text": no salary field, but the description has a range such as "$120,000 - $150,000" or "€54,000 - €75,000" (a currency sign or code, a number, a dash or "to", a number). We read 24 of these matches at random and all 24 were real pay ranges.
- Empty boards: four boards answered HTTP 200 with no jobs at all (three on Lever, one on Ashby), so we left them out. A 200 does not prove a company still posts there.
Share of jobs with a salary, by system
| System | Boards | Jobs | With a salary field | Only in the text | A range in either |
|---|---|---|---|---|---|
| Greenhouse | 10 | 3,153 | 1,201 (38%) | 230 (7%) | 45% |
| Lever | 8 | 1,903 | 50 (3%) | 488 (26%) | 28% |
| Ashby | 8 | 1,366 | 922 (67%) | 107 (8%) | 75% |
| All | 26 | 6,422 | 2,173 (34%) | 825 (13%) | 47% |
Big boards weigh a lot in these totals: Lever's 3% comes mostly from Veeva (861 jobs) and Gopuff (779), with almost no salary fields between them. Counted board by board instead, the middle board (the median) had a salary field on 48% of its jobs, and a range somewhere on 62%.
By company: from 0% to 100%
| Company | System | Jobs | With a salary field | Only in the text |
|---|---|---|---|---|
| Duolingo | Greenhouse | 55 | 55 (100%) | 0 |
| Airbnb | Greenhouse | 211 | 177 (84%) | 0 |
| Greenhouse | 162 | 130 (80%) | 0 | |
| Dropbox | Greenhouse | 31 | 24 (77%) | 0 |
| Greenhouse | 168 | 129 (77%) | 4 | |
| Figma | Greenhouse | 145 | 94 (65%) | 0 |
| Databricks | Greenhouse | 920 | 487 (53%) | 1 |
| GitLab | Greenhouse | 240 | 105 (44%) | 0 |
| Cloudflare | Greenhouse | 464 | 0 | 202 (44%) |
| Stripe | Greenhouse | 757 | 0 | 23 (3%) |
| Outreach | Lever | 19 | 12 (63%) | 0 |
| Match Group | Lever | 60 | 36 (60%) | 0 |
| Gopuff | Lever | 779 | 2 (0.3%) | 81 (10%) |
| Ro | Lever | 52 | 0 | 45 (87%) |
| Veeva | Lever | 861 | 0 | 337 (39%) |
| Spotify | Lever | 85 | 0 | 21 (25%) |
| JumpCloud | Lever | 42 | 0 | 4 (10%) |
| Binance | Lever | 5 | 0 | 0 |
| Zapier | Ashby | 11 | 11 (100%) | 0 |
| Ramp | Ashby | 168 | 162 (96%) | 0 |
| OpenAI | Ashby | 815 | 649 (80%) | 0 |
| Cohere | Ashby | 117 | 86 (74%) | 0 |
| Linear | Ashby | 31 | 13 (42%) | 2 |
| Supabase | Ashby | 70 | 1 (1%) | 0 |
| Notion | Ashby | 146 | 0 | 103 (71%) |
| PostHog | Ashby | 8 | 0 | 2 (25%) |
Nine of the 26 boards had no salary field on any job, and six of those nine wrote ranges in the text for a quarter or more of their jobs. A company that publishes a range for its US jobs often publishes none for other countries: Figma's "Account Executive, Enterprise" had $165,000–$190,000, while "Account Executive, Enterprise (Bengaluru, India)" had no salary.
The fields, and what they leave out
Each guide below covers its API in full; here is only the salary part, as it came back on 4 November 2026.
| System | Field and switch | Period | Currencies we saw |
|---|---|---|---|
| Greenhouse | pay_input_ranges with pay_transparency=true: min_cents, max_cents, currency_type, a title | none: only the range's title may say "Annual" | USD 1,076, CAD 42, EUR 39, GBP 12, INR 8, CNY 7, MXN 6, BRL 5 and a few more |
| Lever | salaryRange, always on: min, max, currency, interval | per-year-salary 49, per-hour-wage 1 | USD 46, CAD 4 |
| Ashby | compensation with includeCompensation=true: in summaryComponents, the part with compensationType "Salary" has minValue, maxValue, currencyCode, interval | 1 YEAR 911, 1 MONTH 5, 1 HOUR 5, 1 WEEK 1 | USD 847, CAD 58, GBP 9, SGD 2, EUR 2, SEK, KRW and SAR 1 each |
Three things to handle when you merge them. Greenhouse gives cents, so divide by 100. Greenhouse has no period field at all, so a yearly and an hourly range look the same unless you read the title. And a job can carry more than one value (Greenhouse can list several ranges, Ashby adds components such as equity or bonus): our counts use the first Greenhouse range and Ashby's "Salary" part only.
When the salary is only in the text
On 825 jobs the pay range was in the description and nowhere else. The wording follows a template per company, which makes it easy to find. Examples from the 24 matches we read:
- Stripe: "Salary: $193,232 - 288,000/yr"
- Cloudflare: "Estimated annual salary of $154,000 - $211,000", and for Portugal "€54,000 - €75,000"
- Notion: "the estimated base salary range for this role is $130,000-$150,000"
- Spotify: "The United States base range for this position is $132,948 - $189,927 plus equity"
- Veeva: "Base pay: $75,000 - $135,000"
- Ro: "The target hourly rate for this position ranges from $23 to $30"
Two limits of a text match: one job often lists several ranges (Cloudflare gives one per US region), and the period is in the words around it ("annual", "/yr", "hourly"), so a parser has to pick the right one. For counting, as on this page, finding one range is enough.
A free Python script to repeat the count
The script reads the boards you name as system:board, one request each, and prints how many jobs have a salary field and how many have a range only in the text. It also writes salaries.csv with one row per job: system, board, title, minimum, maximum, currency, period, whether the text has a range, and the job link. Standard library only (Python 3.8 or later).
"""Count how many job postings carry a structured salary on Greenhouse, Lever and Ashby boards, and save them to CSV."""
import csv
import html
import json
import re
import sys
import time
import urllib.request
from collections import Counter
BOARDS = sys.argv[1:] or ["greenhouse:figma", "lever:outreach", "ashby:ramp"]
HEADERS = {"User-Agent": "salary-study-script"}
# a pay range written in the text: "$120,000 - $150,000", "€60K–€80K", "USD 90,000 to 110,000"
IN_TEXT = re.compile(r"(?:[$€£]|\b(?:USD|EUR|GBP|CAD|AUD)\s?)\d[\d,.]*\s?[kK]?\s?(?:-|–|—|to)\s?(?:[$€£]|\b(?:USD|EUR|GBP|CAD|AUD)\s?)?\d")
def get(url):
with urllib.request.urlopen(urllib.request.Request(url, headers=HEADERS), timeout=120) as response:
return json.load(response)
def greenhouse(board):
data = get(f"https://boards-api.greenhouse.io/v1/boards/{board}/jobs?content=true&pay_transparency=true")
for j in data["jobs"]:
pay = (j.get("pay_input_ranges") or [None])[0] # the first range (a job can list more than one)
text = html.unescape(html.unescape(j.get("content") or "")) # the content is HTML escaped as text
if pay:
yield j["title"], pay["min_cents"] / 100, pay["max_cents"] / 100, pay["currency_type"], "", text, j["absolute_url"]
else:
yield j["title"], None, None, None, None, text, j["absolute_url"]
def lever(board):
for j in get(f"https://api.lever.co/v0/postings/{board}?mode=json"):
pay = j.get("salaryRange") or {}
text = " ".join([j.get("descriptionPlain") or "", j.get("additionalPlain") or "", j.get("salaryDescriptionPlain") or ""]
+ [x.get("content") or "" for x in j.get("lists") or []])
yield j["text"], pay.get("min"), pay.get("max"), pay.get("currency"), pay.get("interval"), text, j["hostedUrl"]
def ashby(board):
for j in get(f"https://api.ashbyhq.com/posting-api/job-board/{board}?includeCompensation=true")["jobs"]:
parts = (j.get("compensation") or {}).get("summaryComponents") or []
pay = next((p for p in parts if p.get("compensationType") == "Salary"), {})
yield (j["title"], pay.get("minValue"), pay.get("maxValue"), pay.get("currencyCode"), pay.get("interval"),
j.get("descriptionPlain") or "", j["jobUrl"])
READERS = {"greenhouse": greenhouse, "lever": lever, "ashby": ashby}
totals = Counter()
with open("salaries.csv", "w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow(["system", "board", "title", "salaryMin", "salaryMax", "currency", "interval", "rangeInText", "url"])
for entry in BOARDS:
system, board = entry.split(":", 1)
jobs = list(READERS[system](board))
structured = sum(1 for j in jobs if j[1] is not None or j[2] is not None)
in_text = sum(1 for j in jobs if j[1] is None and j[2] is None and IN_TEXT.search(j[5]))
for title, low, high, currency, interval, text, url in jobs:
writer.writerow([system, board, title, low, high, currency, interval, bool(IN_TEXT.search(text)), url])
totals[system, "jobs"] += len(jobs)
totals[system, "structured"] += structured
totals[system, "in_text"] += in_text
share = f"{structured / len(jobs):.0%}" if jobs else "-"
print(f"{system}:{board}: {len(jobs)} jobs, {structured} with a salary field ({share}), {in_text} more with a range in the text")
time.sleep(1) # one request per board, a pause between boards
for system in READERS:
n = totals[system, "jobs"]
if n:
print(f"{system}: {n} jobs, {totals[system, 'structured'] / n:.0%} with a salary field, "
f"{totals[system, 'in_text'] / n:.0%} more with a range only in the text")
Run with no arguments, it reads one board of each system:
$ python3 salary.py
greenhouse:figma: 145 jobs, 94 with a salary field (65%), 0 more with a range in the text
lever:outreach: 19 jobs, 12 with a salary field (63%), 0 more with a range in the text
ashby:ramp: 168 jobs, 162 with a salary field (96%), 0 more with a range in the text
greenhouse: 145 jobs, 65% with a salary field, 0% more with a range only in the text
lever: 19 jobs, 63% with a salary field, 0% more with a range only in the text
ashby: 168 jobs, 96% with a salary field, 0% more with a range only in the text
What we got on 4 November 2026: 332 jobs in about 5 seconds. For the study we ran it on all 26 boards (and four empty ones) with the board names in the table above; that took about a minute and a half for 6,422 jobs. A wrong board name stops the script with HTTP Error 404. To find the board name of a company, see how to monitor new jobs at a list of companies.
When the Apify Actor is worth it
We make the ATS Jobs Scraper on the Apify Store. It reads the same three APIs for up to 1,000 companies per run and returns one schema for all of them, with salaryMin, salaryMax, salaryCurrency, salaryInterval and salaryText where the employer publishes a salary field. The interval comes as year, month, week, day or hour, and it is empty for Greenhouse, which does not state it. It does not read ranges out of the description; with Include job description on, the text comes with each job and you can search it as above. With Only new or changed jobs on, a scheduled run returns only jobs that are new or changed, a changed salary included. Prices are in the Pricing tab of the Actor's page.
Questions and answers
- Which system shows salaries most often?
- In our sample, Ashby: 67% of its jobs had a salary field, against 38% on Greenhouse and 3% on Lever. But the company decides, not the system: every system had boards at 0% and boards above 60%.
- Why do some companies show no salary at all?
- The employer chooses whether to fill the field, and some write the range in the text instead (Cloudflare, Notion, Veeva, Ro). We did not check the reasons of any company.
- Do I need a key for these APIs?
- No. All three answer public job board requests without a key. Each guide covers the terms of its API: Greenhouse, Lever, Ashby.
- Are these numbers valid for the whole job market?
- No. They describe 26 tech and online-service boards on one day. Job boards that aggregate many employers measure differently; for remote job boards with salary fields, see free remote job APIs compared.
- Can I publish the salary data I collect?
- Counting, as on this page, is one thing; republishing other companies' job texts is another. Read each API's terms in the guides above, and check that your use of the data is allowed where you are.
Measured by Quicksloth on 4 November 2026 with the script on this page, one request per board, on the public Greenhouse, Lever and Ashby job board APIs; job counts and salaries change every day. Written with AI tools and checked by Quicksloth. Quicksloth is an independent developer and is not affiliated with Greenhouse, Lever, Ashby, Apify or the companies named. Greenhouse, Lever, Ashby and Apify are trademarks of their respective owners.