How to scrape jobs from Greenhouse, Lever, Workday and 5 more ATS at once
Monitor open roles at a list of companies across 8 applicant tracking systems in one schema, with numeric salaries and a daily only-new feed.
datagrit › Guides › Ashby Salary Scraper - Startup Job Pay Ranges
GuidePull startup job postings with pay ranges from Ashby boards, annualised to yearly amounts, with currency and remote filters and an only-new feed.
Published 2026-10-01 · uses the Ashby Salary Scraper - Startup Job Pay Ranges Actor
Many startups, Ramp, OpenAI and Zapier among them, hire through Ashby. Their careers pages at jobs.ashbyhq.com/<company> often show a pay range next to each role, partly because employers in some regions must publish pay. On 30 September 2026, 677 of OpenAI's 838 postings and 148 of Ramp's 155 carried a salary. That makes Ashby boards a useful public source of real salary bands.
Who needs that data? Compensation analysts benchmarking bands for a new level. Recruiters who want a feed of new roles at a list of target companies. Job seekers who want to know what Ramp or OpenAI pays for a role before an interview. Sales teams who treat a burst of new postings as a buying signal. They all want the same thing: postings as rows, with pay normalised so a monthly range and a yearly range can sit in one column.
Ashby provides a public job board API for employers who publish roles: api.ashbyhq.com/posting-api/job-board/<board>, with includeCompensation=true to get pay data. No login, plain JSON. It looks easy, and for a single board it is. Turning it into a reliable dataset across many boards is where it gets fiddly:
isRemote: true on hybrid roles too. On the openai board, all 524 hybrid postings carried the flag. On that board, filtering on isRemote gives you mostly hybrid jobs.US matches "Australia" and "Houston".jobs.ashbyhq.com/the%20browser%20company), and boards that do not exist return "not found" rather than an empty list.The Ashby Salary Scraper Actor on Apify handles each of these. It annualises pay (monthly × 12, weekly × 52, daily × 260, hourly × 2080), filters on the currency of the headline range while listing every tier currency, decides "remote" by Ashby's workplaceType instead of isRemote, matches locations as whole words, and keeps an only-new memory in a storage on your own Apify account.
Engineering roles with a published salary at three companies, at least 150,000 per year in USD, US locations:
{
"companies": ["ramp", "openai", "zapier"],
"keywords": ["engineer"],
"departments": ["Engineering"],
"locations": ["US", "New York", "San Francisco"],
"remoteOnly": false,
"onlyWithSalary": true,
"minAnnualSalary": 150000,
"salaryCurrencies": ["USD"],
"publishedWithinDays": 30,
"onlyNewSinceLastRun": false,
"includeDescription": false,
"maxItems": 500
}
companies accepts board names or full Ashby URLs. Boards that do not exist are skipped and listed in the run status; if none can be read, the run fails rather than returning an empty dataset. keywords match the title only, departments the department and teams the level below it. Department names vary between employers (OpenAI files sales roles under "Go To Market"), so look at a first run before filtering on them.
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run_input = {
"companies": ["ramp", "openai", "zapier"],
"keywords": ["engineer"],
"onlyWithSalary": True,
"minAnnualSalary": 150000,
"salaryCurrencies": ["USD"],
"maxItems": 500,
}
run = client.actor("datagrit/ashby-salary-scraper").call(run_input=run_input)
postings = [p for p in client.dataset(run["defaultDatasetId"]).iterate_items() if p.get("found")]
for p in postings[:10]:
print(p["company"], p["title"], p["salaryAnnualMin"], p["salaryAnnualMax"], p["salaryCurrency"])
curl -X POST \
"https://api.apify.com/v2/acts/datagrit~ashby-salary-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"companies": ["ramp"], "onlyWithSalary": true, "maxItems": 50}'
One row per posting. The fields that matter most for pay analysis:
| Field | Example | Meaning |
|---|---|---|
company | zapier | Ashby board name |
title | Finance Manager, Marketing | Job title |
department | Finance | Department set by the employer |
workplaceType | Remote | Remote, Hybrid or OnSite |
salaryMin / salaryMax | 158300 / 237500 | Headline range as published |
salaryInterval | 1 YEAR | Pay period of the published range |
salaryAnnualMax | 237500 | Upper bound converted to a yearly amount |
salaryCurrency | USD | Currency of the headline range |
payTierCurrencies | USD, CAD | Currencies of all location pay tiers |
equityOffered | true | Compensation includes equity |
You also get bonusMentioned, commissionMentioned, salarySummary (the text shown on the posting), payTierSummaries when there are several tiers, allLocations, publishedAt, daysSincePublished, jobUrl and applyUrl. descriptionText (cut to 8,000 characters) is added only if you ask for it.
Set onlyNewSinceLastRun: true and schedule the Actor daily. The first run returns everything that matches; later runs return only postings not delivered before for the same companies and filters. Postings cut off by maxItems are not remembered, so they come back in the next run. If two schedules use identical filters but must each receive every posting (say, two recruiting teams), give them different stateKey values. Hook the run's webhook to Slack, Zapier or n8n and post each new row.
import pandas as pd
df = pd.DataFrame(postings)
usd = df[(df["hasSalary"]) & (df["salaryCurrency"] == "USD")]
bench = (
usd.groupby(["company", "department"])
.agg(roles=("id", "count"),
p50_min=("salaryAnnualMin", "median"),
p50_max=("salaryAnnualMax", "median"),
equity_share=("equityOffered", "mean"))
.sort_values("roles", ascending=False)
)
print(bench.head(20))
Keep the currency filter in the analysis: amounts are annualised but not converted between currencies.
Set remoteOnly: true. It keeps workplace type Remote and drops hybrid and on-site roles; when an employer did not set a workplace type, Ashby's isRemote flag decides, and without that the word "remote" in the primary location. On 30 September 2026 this returned 29 of 838 openai postings, while the 524 hybrid postings on that board also carried isRemote.
salaryAnnualMin/Max stay in the headline currency.USA, United States), so add both spellings to location filters.The Actor reads only the public job board API that Ashby offers employers for publishing roles. It does not log in or touch candidate data.
If your target companies hire through Ashby, this gives you a clean, comparable pay dataset and a daily new-roles feed; it is priced pay per result, listed on Apify, with every field documented in the documentation.
Monitor open roles at a list of companies across 8 applicant tracking systems in one schema, with numeric salaries and a daily only-new feed.
Build lists of French companies from the official Sirene register, filtered by net result, revenue, NAF code and location, using Python or cURL.
Collect Greenhouse job postings with published pay ranges per location zone, annualised and filtered by currency, using Python, cURL or pandas.