# Ashby Jobs Scraper - Fast ATS API (`zet46/ashby-jobs-scraper`) Actor

Jobs from Ashby boards (OpenAI, Notion, Linear, Ramp, Perplexity, hundreds more). Structured salary + equity + bonus, filters + diff mode. New `report` mode: 1 row/company with hiringScore, velocity, priorityRoles, remoteShare. $0.50/1k jobs or $0.025/report.

- **URL**: https://apify.com/zet46/ashby-jobs-scraper.md
- **Developed by:** [An Tan Huynh](https://apify.com/zet46) (community)
- **Categories:** Jobs, AI, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

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

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Ashby Jobs Scraper – Fast ATS API

Scrape Ashby-hosted career pages via the public JSON API. One row per
job with **structured compensation** (salary min/max, equity, bonus),
`workplaceType`, department, team and `descriptionPlain` – no proxy
required.

**$0.50 per 1,000 jobs** (`$0.0005 / job`, pay-per-event).

### Who is this for?

Ashby is the ATS of choice for AI-first and modern SaaS startups.
Boards you can scrape today include OpenAI, Notion, Linear, Ramp,
Perplexity, Vercel, Supabase, Clay, Modal, Hex, Rippling, Mercury,
Loom and hundreds more. If a job board URL looks like
`https://jobs.ashbyhq.com/<company>`, this actor can scrape it.

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `boardSlugs` | string\[] | **required** | Company slugs or full board URLs. The actor extracts the slug automatically. Pass as many companies as you want in a single run. |
| `includeCompensation` | bool | `true` | Append `?includeCompensation=true` to pull structured salary + equity + bonus for boards that opt in. |
| `maxJobsPerBoard` | int | `5000` | Safety cap per board (OpenAI has ~800). |
| `titleKeywords` | string\[] | *(empty)* | Keep only jobs whose title contains any of these (case-insensitive). Example: `["engineer","research scientist","ml"]`. |
| `titleExclusions` | string\[] | *(empty)* | Drop jobs whose title contains any of these. Example: `["senior","manager","staff"]`. |
| `departments` | string\[] | *(empty)* | Case-insensitive substring match on job `department`. |
| `locations` | string\[] | *(empty)* | Case-insensitive substring match on `location` / country / region / locality / secondary locations. |
| `workplaceTypes` | string\[] | *(empty)* | Match normalised `workplaceType`: `remote`, `hybrid`, `onsite`. |
| `employmentTypes` | string\[] | *(empty)* | Match `employmentType`: `fulltime`, `parttime`, `contract`, `temporary`, `intern`. |
| `onlyWithSalary` | bool | `false` | Keep only jobs where the compensation has a numeric Salary component. |
| `onlyRemote` | bool | `false` | Shortcut – keep only jobs with workplaceType=Remote or isRemote=true. |
| `publishedAfter` | string | *(empty)* | ISO date / datetime cut-off on `publishedAt`. |
| `minSalary` | int | *(empty)* | Keep only jobs whose compensation annualises to at least this amount. Hourly rates are multiplied by 2080, monthly by 12. Jobs without a numeric salary are dropped. |
| `salaryCurrency` | string | *(empty)* | ISO 4217 currency filter (`USD`, `EUR`, `GBP`...). When set, jobs in other currencies are dropped. |
| `onlyNewSinceLastRun` | bool | `false` | **Diff mode.** The actor remembers (per `stateKey` + board) the set of `jobId`s seen on previous runs in a named key-value store, and on each subsequent run only pushes jobs that are new. Every pushed row carries `isNew: true`. Pair with Apify Scheduler for daily deltas. |
| `stateKey` | string | `default` | Optional label for the diff state bucket. Different `stateKey` values keep independent "seen" memories; shared values share memory. |
| `proxyConfiguration` | object | *(none)* | Optional. API is open; use proxy only if you hit rate limits. |

Example input:

```json
{
  "boardSlugs": ["openai", "notion", "linear"],
  "includeCompensation": true,
  "maxJobsPerBoard": 5000
}
```

### Output (one row per job)

```json
{
  "boardSlug": "openai",
  "jobId": "8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3",
  "title": "Technical Program Manager, Compute Infrastructure",
  "jobUrl": "https://jobs.ashbyhq.com/openai/8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3",
  "applyUrl": "https://jobs.ashbyhq.com/openai/8fb1615c-34bf-47c4-a1d1-b7b2f836bbd3/application",
  "location": "San Francisco",
  "secondaryLocations": [],
  "country": "United States",
  "region": "California",
  "locality": "San Francisco",
  "workplaceType": "Hybrid",
  "workplaceTypeRaw": null,
  "isRemote": false,
  "employmentType": "FullTime",
  "department": "Technical Program Management",
  "team": "Technical Program Management",
  "isListed": true,
  "publishedAt": "2026-03-12T16:38:15.322+00:00",
  "salaryMin": 257000,
  "salaryMax": 335000,
  "salaryCurrency": "USD",
  "salaryInterval": "1 YEAR",
  "equitySummary": "Offers Equity",
  "bonusSummary": null,
  "commissionSummary": null,
  "compensationSummary": "$257K – $335K • Offers Equity",
  "shouldDisplayCompensation": true,
  "compensation": {"...": "...raw compensation object..."},
  "descriptionPlain": "ABOUT THE TEAM\n\nThe compute infrastructure team...",
  "descriptionHtml": "<h3><strong>About the Team</strong></h3>...",
  "scrapedAt": "2026-10-15T21:04:00+00:00"
}
```

#### Workplace type normalisation

The actor maps `workplaceType` (and `isRemote` as fallback) to one of
`Remote`, `Hybrid`, `Onsite`, or leaves it null when the board doesn't
publish the attribute.

#### Compensation

Ashby stores compensation as a tiered object. The actor flattens the
primary `Salary` component into `salaryMin` / `salaryMax` /
`salaryCurrency` / `salaryInterval`, and surfaces human text for
`equitySummary`, `bonusSummary`, `commissionSummary` and the overall
`compensationSummary`. The full raw `compensation` object is kept too
for advanced consumers (multi-tier roles, geographic salary bands).

Boards that **do publish pay** (OpenAI, Ramp, Ashby itself) populate
salary fields on ~80–96 % of listings; boards that don't (Notion,
Linear) leave the compensation object empty.

### Smoke run numbers (2026-10-16)

| Input | Boards | Rows | Field coverage |
|---|---|---|---|
| Default smoke (3 boards, with compensation) | openai + notion + linear | **965** | 100 % jobId/title/jobUrl/location/employmentType/department/publishedAt/descriptionPlain/descriptionHtml; 69 % workplaceType; 67 % salaryMin |
| Filtered (engineering + remote/hybrid + onlyWithSalary) | openai + ramp | **48** | 100 % salary present, 34 Engineering + 14 Forward Deployed Engineering, 47 Hybrid + 1 Remote |
| publishedAfter 2026-09-01, employmentTypes=\[fulltime] | ramp | **69** | 0 violations, 100 % FullTime |

Per-board workplace coverage: OpenAI 68 %, Notion 64 %, Linear 100 %,
Ramp 100 %. Boards that don't tag workplace leave it null – no guess
is made.

### Pricing

Pay-per-event. Two charge types:

- `$0.0005` per `job` row pushed (default `mode: "jobs"`).
- `$0.025` per `report` row pushed (`mode: "report"`, one row per company).

Example runs:

- OpenAI + Notion + Linear (965 jobs, jobs mode) → **$0.48**.
- Ramp only, published after 2026-09-01, full-time (69 jobs) → **$0.03**.
- Filtered OpenAI + Ramp engineering roles with pay (48 jobs) → **$0.02**.
- OpenAI + Notion + Linear in **report mode** → **$0.075** (3 reports; OpenAI
  score 90 / 808 open roles, Notion 90 / 135, Linear 55 / 29).

Compute cost is almost zero (direct API, no proxy, ~1 request/board).

### Report mode (`mode: "report"`)

Instead of one row per job, emit **one hiring-signal report per company**.
Each report row carries:

- `totalOpenRoles` + `hiringScore` 0-100 + `hiringLabel`
  (`aggressive_hiring` / `active_hiring` / `steady_hiring` / `low_hiring` / `no_signal`).
- `hiringVelocity`: `{ recentWindowDays, netOpenRolesChange, hadPreviousRun, previousTotal }`
  — the delta vs the previous run of the same `stateKey`.
- `priorityRoles[]`: top 5 role families (engineering, data, research,
  product, design, sales, customer_success, support, marketing, operations,
  finance, legal, people_hr, security, other) with example titles.
- `departmentSignals[]`, `regionSignals[]` (us / canada / europe_uk /
  europe_eu / latam / mena / apac / africa / remote_global / remote_other /
  other / unknown), `remoteShare` 0.0-1.0, `seniorityMix` (junior / mid /
  senior / leadership).
- `keywordHits`: user-supplied strings mapped to how many open titles
  contain each (case-insensitive substring).
- `expansionSignals`: `newDepartments[]`, `newRegions[]`,
  `newRoleFamilies[]` compared to the previous run.
- `recommendedOutreach[]`: short English suggestions for BD / sales /
  talent outreach based on the above signals.
- `reportConfidence` 0.1-1.0 plus `warnings[]`.

Example input:

```json
{
  "boardSlugs": ["openai", "notion", "linear"],
  "mode": "report",
  "keywordHits": ["AI", "ML", "LLM"],
  "stateKey": "weekly-pipeline"
}
```

Previous-run deltas live in the named key-value store
`ashby-report-<stateKey>` on your account.

### Daily deltas (`onlyNewSinceLastRun`)

Pair with Apify Scheduler to get only newly-posted jobs each run. The
first run of a given `(stateKey, boardSlug)` records every open `jobId`
and pushes the first `maxJobsPerBoard` rows (all tagged `isNew: true`).
Every later run compares the current open set against the stored one
and emits only rows whose `jobId` is new.

```json
{
  "boardSlugs": ["openai", "notion", "linear"],
  "onlyNewSinceLastRun": true,
  "stateKey": "daily-pipeline",
  "titleKeywords": ["engineer"],
  "minSalary": 250000,
  "salaryCurrency": "USD"
}
```

The dedup state lives in a named key-value store `ashby-diff-<stateKey>`
on your account; delete it from the Apify Console to force the next run
to treat every job as new. Use different `stateKey` names for
independent downstream pipelines.

### Notes

- Ashby returns every open job on a board in one JSON response (no
  pagination). `maxJobsPerBoard` is a safety trim for big boards.
- `publishedAt` is ISO 8601 with timezone offset exactly as returned
  by the API.
- Jobs where `isListed: false` are skipped (they are present in the
  API but intentionally hidden by the employer).
- Unknown slugs return HTTP 404 – the actor logs and skips. Some
  companies that look Ashby-flavoured actually live on Greenhouse (eg
  anthropic, perplexity-ai) – see our companion Greenhouse scraper.
- You can paste the full board URL (`https://jobs.ashbyhq.com/openai`)
  instead of just the slug – the slug is extracted automatically.

# Actor input Schema

## `boardSlugs` (type: `array`):

Required unless you pass boardSlugsUrl. List of Ashby board slugs. The slug is the subdomain / path segment at https://jobs.ashbyhq.com/<slug>. Hundreds of AI-first and SaaS startups use Ashby: OpenAI, Notion, Linear, Ramp, Perplexity, Vercel, Supabase, Clay, Modal, Hex, Rippling, Mercury, Loom...

You can also paste the full board URL (https://jobs.ashbyhq.com/<slug>) and the slug will be extracted automatically. Provide at least one entry.

## `boardSlugsUrl` (type: `string`):

Optional. Download a list of board slugs from a public URL and merge with boardSlugs. Supports: direct .csv or .xlsx URL (first column of the first sheet is read), or a Google Sheets URL (viewing link works, e.g. https://docs.google.com/spreadsheets/d/<ID>/edit#gid=<GID> — the sheet must be shared 'Anyone with the link'). Great for feeding a 500+ company watchlist you maintain in a sheet.

## `mode` (type: `string`):

`jobs` (default) emits one row per open job. `report` emits one `hiring_signal_report` row per company with totalOpenRoles, hiringScore 0-100, hiringLabel, hiringVelocity vs the previous run, priorityRoles\[] by role family, departmentSignals\[], regionSignals\[], remoteShare, seniorityMix, keywordHits, expansionSignals and recommendedOutreach\[]. Report mode is priced per report ($0.025 each).

## `keywordHits` (type: `array`):

Only used when mode=report. For each entry we count how many open job titles contain the string (case-insensitive substring). Example: \['AI', 'LLM', 'GTM'] gives you `keywordHits: {AI: 12, LLM: 3, GTM: 5}` per company. Leave empty if you don't need keyword counts.

## `recentWindowDays` (type: `integer`):

Only used when mode=report. The hiringVelocity.recentWindowDays label we embed on each report row (purely informational — the actual delta is measured between the two latest runs stored in KVS).

## `includeCompensation` (type: `boolean`):

Append `?includeCompensation=true` so each job includes the structured `compensation` object. Boards that opt in to show pay (OpenAI, Ramp, Ashby itself) populate salaryMin/salaryMax + equity + bonus; boards that don't (Notion, Linear) still return the key but it stays empty.

## `maxJobsPerBoard` (type: `integer`):

Safety cap on jobs returned per board (Ashby returns all open jobs in one JSON response; OpenAI has ~800).

## `titleKeywords` (type: `array`):

Optional. Case-insensitive substring match on the job title. Example: \['engineer', 'research scientist', 'ml'] keeps only jobs whose title contains any of those. Leave empty to keep all titles.

## `titleExclusions` (type: `array`):

Optional. Case-insensitive substring match on the job title. Example: \['senior', 'manager', 'intern'] drops any job whose title contains those words.

## `departments` (type: `array`):

Optional. Case-insensitive substring match on the job's `department`. Example: \['engineering', 'research'] keeps jobs whose department contains either.

## `locations` (type: `array`):

Optional. Case-insensitive substring match on the job's `location` or `address.postalAddress`. Example: \['san francisco', 'remote', 'new york'].

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

Optional. Match the normalised workplaceType: `remote`, `hybrid`, `onsite`. Many boards tag workplace via Ashby's `workplaceType` field; those that don't fall back to `isRemote` boolean.

## `employmentTypes` (type: `array`):

Optional. Keep only jobs matching one of the canonical values: `full_time`, `part_time`, `contract`, `internship`. The filter accepts Ashby's raw `employmentType` (FullTime, PartTime, Contract, Intern...) and our normalised `aiEmploymentType` classified from the title.

## `seniorityLevels` (type: `array`):

Optional. Keep only jobs whose auto-classified seniority is in this list. Classifier reads the title and tags one of: intern, entry, associate, mid, senior, staff, principal, director, vp, svp, exec. Example: \['senior', 'staff', 'principal'] drops juniors and executives.

## `remoteTypes` (type: `array`):

Optional. Keep only jobs tagged as one of: 'remote', 'hybrid', 'on_site'. The classifier reads workplaceType + isRemote first, then falls back to title + location keywords (remote, WFH, hybrid, on-site, in-office).

## `onlyWithSalary` (type: `boolean`):

Keep only jobs where the compensation object has a numeric Salary component (salaryMin or salaryMax is present).

## `onlyRemote` (type: `boolean`):

Shortcut: keep only jobs with workplaceType=Remote or isRemote=true or location containing 'remote'.

## `publishedAfter` (type: `string`):

ISO date (YYYY-MM-DD) or ISO datetime. Keep only jobs whose publishedAt is on or after this. Useful for incremental refresh. Alias: 'postedAfter' (same semantics; our other ATS scrapers also accept 'postedAfter').

## `postedAfter` (type: `string`):

Alias of 'publishedAfter' for compatibility with our other ATS scrapers. ISO date (YYYY-MM-DD) or datetime. Ignored if 'publishedAfter' is also set.

## `minSalary` (type: `integer`):

Optional. Keep only jobs whose published salary (salaryMax, annualised from hourly/monthly) is at least this. Hourly salaries are multiplied by 2080, monthly by 12. Jobs without a numeric salary are dropped. Pair with salaryCurrency if you only want one currency.

## `salaryCurrency` (type: `string`):

Optional 3-letter ISO 4217 currency code (USD, EUR, GBP, CAD...). When set, jobs whose compensation is in a different currency are dropped. Leave empty to accept any currency.

## `onlyNewSinceLastRun` (type: `boolean`):

When enabled, the actor remembers the set of jobIds it saw per board in its default key-value store and on subsequent runs only pushes jobs whose jobId was not present before. Each pushed row carries `isNew: true`. Pair with a scheduler for daily deltas. Dedup state is scoped by (stateKey, boardSlug).

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

Optional label for the diff state record (defaults to 'default'). Use different names when running the same actor for different downstream pipelines so their 'seen' sets stay separate; use the same name across runs that should share memory.

## `proxyConfiguration` (type: `object`):

Optional. The Ashby public API usually serves direct from any IP; use Apify Proxy only if you hit rate limits.

## Actor input object example

```json
{
  "boardSlugs": [
    "openai",
    "notion",
    "linear"
  ],
  "mode": "jobs",
  "recentWindowDays": 7,
  "includeCompensation": true,
  "maxJobsPerBoard": 5000,
  "onlyWithSalary": false,
  "onlyRemote": false,
  "onlyNewSinceLastRun": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

All scraped Ashby jobs as JSON items (one per row).

## `jobsCsv` (type: `string`):

The same jobs as a CSV file.

# 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 = {
    "boardSlugs": [
        "openai",
        "notion",
        "linear"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("zet46/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 = {
    "boardSlugs": [
        "openai",
        "notion",
        "linear",
    ],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("zet46/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 '{
  "boardSlugs": [
    "openai",
    "notion",
    "linear"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call zet46/ashby-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,zet46/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/37dtws4O6KcKqs8dU/builds/ZCpmzX0GT8123ybNu/openapi.json
