# Ashby Jobs Scraper: Salary, Equity & Correct Remote Flags (`firstpartydata/ashby-jobs-scraper`) Actor

Scrapes job postings from any Ashby job board (jobs.ashbyhq.com) through Ashby's public API. Reads compensation correctly, including the salary ranges a naive parse silently drops. Separates genuinely remote roles from hybrid ones, and ships a verified list of live Ashby boards.

- **URL**: https://apify.com/firstpartydata/ashby-jobs-scraper.md
- **Developed by:** [First Party Data](https://apify.com/firstpartydata) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 job posting returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Ashby Jobs Scraper: Salary, Equity & Correct Remote Flags

Job postings from any Ashby board (`jobs.ashbyhq.com/...`) through Ashby's own
public API. No login, no proxy, no browser.

Ashby is the most generous of the major applicant tracking systems about what it
publishes. It is the one that routinely gives you a real pay range, and also the
one that is easiest to read wrongly. Both of those are the point of this
actor, and both are measured below rather than asserted.

***

### Ashby's pay data is the densest anywhere, and a third of it is easy to lose

Ashby attaches several **compensation components** to a posting: a salary range,
an equity line, sometimes a bonus. They are returned as a list, and **the list is
not in a fixed order.**

Ramp publishes a pay range on 95% of its roles. On only 25% of them is that range
the first component: usually the equity line comes first, and an equity line
carries no numbers at all. So the obvious implementation, take the first
component, reports Ramp as mostly not publishing pay.

Measured across **300 Ashby boards and 19,267 live postings**:

| | |
|---|---:|
| Postings that publish a real pay range | **38.4%** |
| …that taking the first component silently misses | **30.0%** of them (2,217 postings) |
| Postings advertising equity | **24.1%** |

So a naive Ashby scraper reports roughly 27% salary coverage. The true figure is
**38.4%**. This actor picks the component that is actually a pay range, exposes
`offersEquity` separately, and normalises Ashby's `"1 YEAR"` interval notation to
`YEARLY` so ranges are comparable across boards.

```json
"compensation": {
  "min": 211400, "max": 290600, "currency": "USD",
  "interval": "YEARLY", "raw": "$211.4K - $290.6K"
},
"offersEquity": true
```

Filter to **only jobs with published pay** and you get a genuinely dense salary
dataset, which on most other applicant tracking systems is simply not available.

***

### `isRemote` does not mean remote, and it is wrong about half the time

This is Ashby's sharpest trap. The API returns a boolean field called `isRemote`.
It reads as though it means the role is remote. It does not. It appears to mean
"not strictly on-site", so it is `true` for hybrid roles too.

Measured across **300 Ashby boards and 19,267 live postings**:

| | |
|---:|---|
| **51.7%** | of postings have `isRemote: true` |
| **23.6%** | are actually remote, per Ashby's own `workplaceType` |
| **54.6%** | of the field's `true` values are Hybrid or OnSite |
| **2.19×** | how much you overstate remote work by trusting it |

More than half of everything Ashby marks `isRemote: true` is not a remote job.
Anything built downstream of that field inherits the error, including an "AI work
arrangement" filter, which will confidently label hybrid roles remote.

This actor treats **`workplaceType` as authoritative** and falls back to
`isRemote` only where `workplaceType` is absent. You get both:

- `workplaceType`: `Remote`, `Hybrid`, `OnSite`, or `null`
- `isRemote` is three-valued: `true`, `false`, or `null` when the board did not say

Filter with **Work arrangement** to pick exactly what you want, including
`Hybrid` on its own, which no boolean can express. **Remote roles only** means
genuinely remote, and excludes unknowns rather than guessing.

Distribution across that sample: OnSite 31.7%, Hybrid 28.3%, Remote 23.6%,
unstated 16.4%.

***

### Input

Paste board URLs, or just name the company:

```
https://jobs.ashbyhq.com/openai
https://jobs.ashbyhq.com/ramp
openai
```

The board identifier is the last path segment. Ashby publishes no company display
name anywhere in its API, so `company` is derived from that identifier while
`companyToken` always holds the exact, unmodified one.

Or switch on **Search the built-in company list**: this actor ships
**3,213 verified live Ashby boards carrying 54,822 open postings**,
ordered largest first, so a sample of a few hundred already covers most of them.

**The ten largest Ashby boards in the bundled list**

1. **Airapps**: 498 roles
2. **Lilt Production**: 413 roles
3. **Alpacahealth**: 405 roles
4. **Renuity**: 383 roles
5. **Crusoe**: 374 roles
6. **Snowflake**: 368 roles
7. **Harvey**: 332 roles
8. **Applied**: 291 roles
9. **Legora**: 282 roles
10. **Clera**: 260 roles

Every board was verified live at build time (2026-09-15): it answered Ashby's API
with at least one open posting. Dead and private boards are not shipped.

***

### What a row looks like

```json
{
  "id": "ashby:openai:8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3",
  "title": "Technical Program Manager, Compute Infrastructure",
  "company": "OpenAI",
  "companyToken": "openai",
  "department": "Technical Program Management",
  "team": "Technical Program Management",
  "employmentType": "FULL_TIME",
  "employmentTypeRaw": "FullTime",
  "seniorityLevel": null,
  "seniorityInferred": false,
  "locationRaw": "San Francisco",
  "locations": ["San Francisco"],
  "city": "San Francisco",
  "region": "California",
  "country": "United States",
  "workplaceType": "Hybrid",
  "isRemote": false,
  "compensation": { "min": 257000, "max": 335000, "currency": "USD", "interval": "YEARLY", "raw": "$257K - $335K" },
  "offersEquity": true,
  "descriptionText": "About the Team...",
  "applyUrl": "https://jobs.ashbyhq.com/openai/8fb1615c-.../application",
  "postedAt": "2026-03-12T16:38:15.322Z",
  "scrapedAt": "2026-09-11T06:20:11.004Z"
}
```

Note `workplaceType: "Hybrid"` alongside `isRemote: false`. That same posting
carries `isRemote: true` in Ashby's raw response.

`seniorityLevel` is `null` here on purpose. "Technical Program Manager" is a
role, not a level: program managers, account managers and project managers are
individual contributors, and across 20,000 titles "manager" was the single most
misleading word to read as seniority. Where a level really is stated, the field
carries one of `INTERN` `JUNIOR` `MID` `SENIOR` `MANAGER` `PRINCIPAL` `DIRECTOR`
`EXECUTIVE`, and `seniorityInferred` says whether it came from the employer's own
field or from the title.

`id` is stable between runs, so an incremental pipeline can dedupe on it.
Multi-location postings keep every location in `locations[]` rather than only the
primary one.

**Location is structured, not just a string.** Ashby publishes a real postal
address on 793 of 795 OpenAI postings, so `city`, `region` and `country` come out
as separate fields. That makes the **Country** filter exact: you are matching a
country, not string-searching "Germany" against a location line that might say
"Berlin" or "Munich". An ISO code works too.

***

### Recipes

**Every remote engineering role that publishes pay**

```json
{
  "useBuiltInCorpus": true,
  "corpusSample": 500,
  "titleInclude": ["engineer"],
  "workplaceTypes": ["Remote"],
  "withSalaryOnly": true
}
```

**Salary benchmarking across the startup market**

```json
{ "useBuiltInCorpus": true, "corpusSample": 0, "withSalaryOnly": true, "includeDescriptions": false }
```

Ashby's structured pay plus a wide board list is the cheapest compensation
dataset available here. Descriptions off keeps it small.

**Roles offering equity**

```json
{ "useBuiltInCorpus": true, "corpusSample": 300, "offersEquityOnly": true }
```

**A daily feed of one company**

```json
{ "companies": ["ramp"], "onlyNewOrChanged": true }
```

***

### Running it as a feed

**Only new or changed jobs** remembers what the last run returned and bills only
what moved: a changed title, a changed pay range, a changed work arrangement.
Unchanged postings are skipped without being billed. Roles that disappeared are
listed in the `REMOVED_SINCE_LAST_RUN` record so you can close them out.

The first run establishes the baseline and returns everything.

***

### Honest limits

- **Ashby publishes no company name.** Not in the postings, not on the board.
  `company` is derived from the board identifier, so a glued-together name comes
  back as it was written: `billiontoone` is `Billiontoone` and `northwoodspace`
  is `Northwoodspace`. Treat `companyToken` as the real key.
- **`compensation` is null more often than you would like.** 38.4% coverage is
  excellent for this industry and still means most postings have none. Where a
  range appears only in the description prose it is parsed out, but only when it
  is unambiguous and currency-marked.
- **`workplaceType` is unset on about 16% of postings.** Those get
  `isRemote: null`. Filtering on work arrangement excludes them; that is
  deliberate, because the alternative is inventing an answer.
- **One request per board, no paging.** Ashby returns the whole board at once,
  which is why this is the fastest of the major systems to read, a 795-posting
  board lands in about 210 ms. Large runs are limited by how many boards you ask
  for, not by Ashby.
- **Public data only.** The endpoint is Ashby's documented public job-board API,
  the one that exists so companies can host their own careers page. Nothing
  authenticates, submits an application, or touches candidate data.

Unofficial: this actor is not affiliated with or endorsed by Ashby.

# Actor input Schema

## `companies` (type: `array`):

One or more Ashby job boards. A full URL such as https://jobs.ashbyhq.com/openai works, and so does the bare board identifier (openai) or the company domain. The identifier is the last path segment of the board URL. Leave empty and switch on the built-in list below to read the bundled Ashby boards instead.

## `useBuiltInCorpus` (type: `boolean`):

Read the bundled list of verified live Ashby boards instead of, or as well as, the companies above. Ordered largest first. Ashby returns a whole board in one request, so this is fast even across hundreds of them.

## `corpusSample` (type: `integer`):

How many bundled Ashby boards to read, largest first. Because boards are size-ordered, a few hundred already covers most postings in the list. 0 means every board.

## `withSalaryOnly` (type: `boolean`):

Keep only postings with a real compensation range. Ashby is unusually generous here: 38.4% of postings across a 19,267-posting sample publish one, far more than any other applicant tracking system. This actor reads the pay component correctly rather than taking the first one, which alone recovers 30% of the ranges on offer.

## `offersEquityOnly` (type: `boolean`):

Keep only postings with an equity component. Ashby publishes equity as a separate compensation line carrying no numbers; 24.1% of postings across a 19,267-posting sample had one. Useful for startup compensation research.

## `workplaceTypes` (type: `array`):

Keep only postings matching Ashby's own workplaceType, the field that is actually correct. Choose Hybrid on its own if that is what you want, which a remote/not-remote boolean cannot express. Postings where the employer left it unset (about 16%) are excluded rather than guessed at.

## `remoteOnly` (type: `boolean`):

A shortcut for work arrangement = Remote. Note this is genuinely remote: Ashby's raw isRemote field is true for hybrid roles too and overstates remote work by 2.1x, so this filter deliberately disagrees with it.

## `titleInclude` (type: `array`):

Keep only postings whose title contains one of these, case-insensitively. Example: engineer, designer.

## `titleExclude` (type: `array`):

Drop postings whose title contains any of these. Example: intern, staff, director.

## `keywords` (type: `array`):

Keep only postings containing one of these anywhere in the title or description. Ashby returns full descriptions with every posting at no extra cost, so this is cheap here. Example: kubernetes, rust.

## `keywordsMatchAll` (type: `boolean`):

When on, a posting must contain all of the keywords rather than any one of them.

## `location` (type: `array`):

Keep only postings whose location contains one of these. Ashby postings can carry several locations and all of them are matched, not just the primary. Example: Berlin, Remote - US.

## `countries` (type: `array`):

Keep only postings in these countries. Ashby publishes a structured address on almost every posting, so this is exact rather than a string match against the location text. Accepts an ISO code or a name: DE and Germany both work.

## `postedWithinDays` (type: `integer`):

0 disables the filter. Ashby publishes a real publication timestamp on every posting, so this filter is reliable here, unlike on systems that only give a relative date.

## `includeDescriptions` (type: `boolean`):

Return the description as both HTML and plain text. Ashby includes it in the same response, so turning this off saves payload size but not time. Leave it on unless you are building a listings table.

## `onlyNewOrChanged` (type: `boolean`):

Return only postings that are new or that have changed since the last run with the same settings: a changed title, pay range or work arrangement. Unchanged rows are skipped and not billed. The first run establishes the baseline and returns everything.

## `stateKey` (type: `string`):

Names the history incremental mode reads and writes. Leave blank and one is derived from your settings, so two differently-filtered feeds never contaminate each other.

## `maxItems` (type: `integer`):

Hard ceiling on results, so an over-broad filter cannot produce a surprise bill. 0 means no limit.

## `maxPerCompany` (type: `integer`):

Stops a single large board filling the whole result. Leave at 0 and a market-wide run with a ceiling spreads it over at least 40 boards on its own: without that, the bare default returned 1,000 rows from one or two companies, because one Greenhouse board arrives in a single page and takes the whole ceiling. Counted per board, not per employer, so a company that runs two boards can return twice this. It is never applied when you name companies yourself, or when "maxItems" is 0.

## `concurrency` (type: `integer`):

How many Ashby boards to read at once. Ashby is fast and tolerant, so the default is higher than for other systems.

## `atsPlatforms` (type: `array`):

Fixed to Ashby for this Actor.

## Actor input object example

```json
{
  "companies": [
    "https://jobs.ashbyhq.com/openai",
    "ramp"
  ],
  "useBuiltInCorpus": false,
  "corpusSample": 300,
  "withSalaryOnly": false,
  "offersEquityOnly": false,
  "workplaceTypes": [],
  "remoteOnly": false,
  "keywordsMatchAll": false,
  "postedWithinDays": 0,
  "includeDescriptions": true,
  "onlyNewOrChanged": false,
  "stateKey": "",
  "maxItems": 1000,
  "maxPerCompany": 0,
  "concurrency": 25,
  "atsPlatforms": [
    "ashby"
  ]
}
```

# Actor output Schema

## `jobs` (type: `string`):

One item per posting, in a single schema across all supported applicant tracking systems.

## `runSummary` (type: `string`):

Counts of boards read, not found, rate-limited, forbidden and read with nothing open, postings scanned, the filters applied, and any diagnostics explaining an empty result.

## `removedSinceLastRun` (type: `string`):

Written on every incremental run, empty array included: the ids your previous run returned that this one did not. Usually the employer took the posting down, but a posting that ages past postedWithinDays or is retitled out of a keyword match leaves the same way. Boards this run could not read are excluded, so a vendor outage does not empty your feed on paper.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companies": [
        "https://jobs.ashbyhq.com/openai",
        "ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("firstpartydata/ashby-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "companies": [
        "https://jobs.ashbyhq.com/openai",
        "ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("firstpartydata/ashby-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "https://jobs.ashbyhq.com/openai",
    "ramp"
  ]
}' |
apify call firstpartydata/ashby-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,firstpartydata/ashby-jobs-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/TdVhuLj1rZk8a6RSe/builds/3ax9jYMbVZxa6sb9A/openapi.json
