# Greenhouse, Lever & Ashby Jobs Scraper (`kaz_kakyo/greenhouse-lever-ashby-jobs-scraper`) Actor

Scrape jobs from Greenhouse, Lever and Ashby career pages. Get titles, locations, remote flags, salary where available, descriptions and apply links. Filter before billing. No ATS API key or proxy. $1 per 1,000 jobs plus the platform start charge.

- **URL**: https://apify.com/kaz\_kakyo/greenhouse-lever-ashby-jobs-scraper.md
- **Developed by:** [Heim AI](https://apify.com/kaz_kakyo) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 job delivereds

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

## Greenhouse, Lever & Ashby Jobs Scraper - Company Career Pages

**Export public job postings from company career pages on Greenhouse, Lever and Ashby.** Paste careers URLs or board tokens, get one clean JSON row per job: title, location, department, remote flag, salary when the employer publishes it, posted date, apply URL and a plain-text description. No ATS API key, ATS login or proxy required. **$1 per 1,000 jobs** plus $0.00005 per run. Filters and limits let you choose which jobs to receive.

| | |
|---|---|
| **Actor id** | `kaz_kakyo/greenhouse-lever-ashby-jobs-scraper` |
| **Minimal input** | `{ "boards": ["https://boards.greenhouse.io/airbnb"] }` |
| **Cost** | **$0.001** per job delivered (+ a nominal $0.00005 per run) |
| **Output** | Jobs-only default dataset; free summaries, errors and notices in a separate status dataset |

### What it does

Companies that hire through Greenhouse, Lever or Ashby publish their open roles on a public careers page (`boards.greenhouse.io/<company>`, `jobs.lever.co/<company>`, `jobs.ashbyhq.com/<company>`). This actor reads those boards through the vendors' public job-board endpoints — the same ones the careers pages use — and normalises the postings of all three vendors into one schema. Give it the companies you care about; it returns their live jobs.

Typical uses: watch the careers pages of target accounts (hiring signals for sales and recruiting), build a niche job board from a list of companies, feed an AI agent or an n8n / Make workflow with structured postings, or export a company's whole board to a sheet.

**It is not a cross-company jobs search engine.** You supply the boards; it fetches them. Use `(ats, region, board, jobId)` to compare results across runs for "new since last time".

### Input

| Field | Default | Description |
|---|---|---|
| `boards` | (required) | Careers URLs (`https://boards.greenhouse.io/<slug>`, `https://job-boards.greenhouse.io/<slug>`, `https://jobs.lever.co/<slug>`, `https://jobs.ashbyhq.com/<slug>`, EU hosts too), vendor tokens (`greenhouse/<slug>`, `lever/<slug>`, `ashby/<slug>`) or a bare `<slug>` (all three vendors are tried). Max 200 per run |
| `maxJobsPerBoard` | `500` | Cap per board, applied after filters (max 5000) |
| `descriptionFormat` | `text` | `text` (plain text, compact), `html` (as published) or `none` (`description` is `null`, everything else kept) |
| `keyword` | — | Case-insensitive substring match on the job title |
| `location` | — | Case-insensitive substring match on location text (primary + secondary locations, country) |
| `remoteOnly` | `false` | Ashby/Lever remote flags; Greenhouse title or location containing the word remote |
| `postedAfter` | — | ISO date; keeps jobs posted on or after it (Greenhouse: first-published date when present, else last updated; postings with no date at all are excluded) |
| `maxTotalJobs` | `5000` | Run-wide cap (max 20000) |
| `includeBoardSummary` | `true` | Write one free summary per resolved board in the status dataset; errors and notices remain available when false |

Filters and caps are applied **before** billing — you pay only for delivered `job` rows.

### Output

The default dataset and **Jobs** output contain only job rows, with all fields including `applyUrl` and `description`. The separate, run-scoped `status` dataset and **Boards & errors** output contain board summaries, errors and notices. Both output URLs are available in the run API response.

| `type` | Charged? | Meaning |
|---|---|---|
| `job` | Yes ($0.001) | One job posting |
| `board` | No | Per-board status: `ok`, `empty` (the board exists but publishes nothing through its public API, or only unlisted postings), `duplicate` (the same board reached through two entries — delivered once), with `totalJobs` / `filteredOut` / `deliveredJobs` / `skippedJobs` counts and a `note` when a cap or the budget cut the board short |
| `error` | No | A board that could not be fetched (`not-found`, `blocked`, `rate-limited`, `upstream-error`, `timeout`, `network-error`, `bad-json`, `too-large`, `pagination-limit`) or an invalid input entry (`invalid-input`) |
| `notice` | No | Run-level events: `max_total_jobs_reached`, `boards_capped`, `invalid_entries_capped`, `charge_limit_reached`, `billing_state_unknown`, `run_interrupted`; `detail` carries the explanation |

Example job row (illustrative values):

```json
{
  "type": "job",
  "ats": "ashby",
  "board": "ramp",
  "region": "global",
  "boardUrl": "https://jobs.ashbyhq.com/ramp",
  "jobId": "a1b2c3d4-0000-4000-8000-000000000001",
  "title": "Product Designer",
  "location": "New York",
  "secondaryLocations": ["Remote (US)"],
  "department": "Design",
  "team": "Growth",
  "employmentType": "FullTime",
  "workplaceType": "onsite",
  "isRemote": false,
  "country": "United States",
  "salary": { "min": 150000, "max": 190000, "currency": "USD", "interval": "1 YEAR" },
  "salaryText": "$150K - $190K",
  "postedAt": "2026-08-20T00:00:00.000Z",
  "updatedAt": null,
  "url": "https://jobs.ashbyhq.com/ramp/a1b2c3d4-0000-4000-8000-000000000001",
  "applyUrl": "https://jobs.ashbyhq.com/ramp/a1b2c3d4-0000-4000-8000-000000000001/application",
  "requisitionId": null,
  "description": "Design great things.",
  "descriptionTruncated": false,
  "fetchedAt": "2026-09-02T00:00:00.000Z"
}
```

Field notes, by vendor:

- **Greenhouse** — `location` is the posting's location; `secondaryLocations` are its offices; `department` is the first department. `postedAt` uses `first_published` when the list response includes it; otherwise it is `null` and `postedAfter` falls back to `updatedAt`. Structured `salary` is returned only when the list response includes pay ranges; it is not guaranteed for every board. `applyUrl` is the job page.
- **Lever** — `employmentType` is the commitment (Full-time, Contract…), `workplaceType` is `remote` / `hybrid` / `onsite` as set by the employer, `salary` comes from the posting's salary range, `postedAt` is the posting's creation time. Use the EU careers URL for an EU board; an identical slug on global and EU hosts identifies separate boards.
- **Ashby** — `isRemote` and `employmentType` are the employer's flags, `salary` is the published salary component (equity components are ignored), `salaryText` is the employer's summary string; both are `null` when the employer switched compensation display off. Unlisted postings are never returned. `postedAt` is the last publication time reported by Ashby; `isRemote` can be true on a hybrid posting if the employer permits remote locations.

Salary is **never** parsed out of description text — it is only present when the employer published it as structured data.

### Pricing

| Event | Price |
|---|---|
| Job delivered (`job-delivered`) | **$0.001** per `job` row |
| Run start (`apify-actor-start`) | $0.00005 (nominal) |

Worked examples: a board with 120 open roles costs about **$0.12**; 50 boards averaging 40 roles cost about **$2**. Board, error and notice rows are free.

Cap spend with `maxTotalChargeUsd` on the run. When the charge budget is exhausted, delivery stops cleanly — the remaining jobs of the current board and the remaining boards are skipped, the board row says so in `note`, and a `charge_limit_reached` notice is written. If the actor cannot read its own billing state on the platform it stops instead of delivering unbilled data and writes a `billing_state_unknown` notice (fail-closed).

### Quick start (Console)

1. Open the actor in [Apify Console](https://console.apify.com).
2. Keep the prefilled boards or paste your own careers URLs.
3. Click **Start**. Open **Jobs** for postings and **Boards & errors** for results from each board. An empty Jobs result can mean filters matched nothing or a board failed; check the status output before deciding.

### Example inputs

**Export a company board:** request up to 5,000 Airbnb jobs with full text descriptions. Check the board summary for any cap or error before treating the export as complete.

```json
{
  "boards": ["https://boards.greenhouse.io/airbnb"],
  "maxJobsPerBoard": 5000,
  "maxTotalJobs": 5000,
  "descriptionFormat": "text"
}
```

**Find remote engineering roles:** filter Ramp's supplied board by title and remote flag, returning at most 100 matches. This does not search other companies; "remote" may still have location restrictions.

```json
{
  "boards": ["https://jobs.ashbyhq.com/ramp"],
  "keyword": "engineer",
  "remoteOnly": true,
  "maxJobsPerBoard": 100,
  "maxTotalJobs": 100
}
```

### API

Apify API calls require your Apify API token; the source job boards require no separate ATS credentials.

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/kaz_kakyo~greenhouse-lever-ashby-jobs-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"boards":["https://boards.greenhouse.io/airbnb","https://jobs.ashbyhq.com/ramp"],"keyword":"engineer","maxJobsPerBoard":100}'
```

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('kaz_kakyo/greenhouse-lever-ashby-jobs-scraper').call(
  { boards: ['https://jobs.ashbyhq.com/ramp'], keyword: 'engineer', remoteOnly: true, maxJobsPerBoard: 100 },
  { maxTotalChargeUsd: 2 },
);
// Page through the jobs-only default dataset for large exports.
const dataset = client.dataset(run.defaultDatasetId);
for (let offset = 0; ; offset += 1000) {
  const { items } = await dataset.listItems({ offset, limit: 1000 });
  for (const job of items) console.log(job.title, job.applyUrl);
  if (items.length < 1000) break;
}
// Follow run.output.boards for board summaries, errors and notices.
```

### MCP / agents

Callable through the **Apify MCP server** as a tool. Pass `boards` (careers URLs are fine — an agent usually already has them) and optional `keyword` / `location` / `remoteOnly` / `maxJobsPerBoard`; read the Jobs output for complete job rows and Boards & errors for per-board outcomes. Use `descriptionFormat: "none"` for the smallest payload (every other field, including Greenhouse departments and offices, is still delivered) and `maxJobsPerBoard` to bound tokens. Input that violates the schema is rejected with HTTP 400 before a run is created; input that passes the schema but is unusable produces uncharged error rows without failing the run.

### Scheduling

Use **Apify Schedules** to re-fetch a list of boards daily or hourly, attach a webhook on `SUCCEEDED`, and compare `(ats, region, board, jobId)` against the previous run to detect changes. Only infer a closed job after a complete, successful fetch with the same filters; a cap, failed board or changed filter can also make a job disappear. Job ids are stable between fetches; Greenhouse appears to mint a new id when a role is reposted (observed, not documented).

### Limits (honest)

- Only Greenhouse, Lever and Ashby boards are supported (careers URLs may be pasted with or without `https://`). Workday, iCIMS, Taleo, SuccessFactors, Workable, SmartRecruiters and other ATSs are not — unsupported URLs produce `invalid-input`; a missing board on a supported vendor produces `not-found`.
- Some companies publish **zero** jobs through the public API while their site shows openings (they hide roles from the API on purpose). Those boards get a `board` row with `status: "empty"` — that is the correct reading, not a bug.
- Dates can be missing. Greenhouse uses first-published time when provided, falling back to last-updated time for the `postedAfter` filter; jobs without either date are excluded by that filter.
- Lever global and EU demo boards passed live verification. Broader coverage remains provisional; a later-page fetch failure produces an error row and no partial board.
- Lever boards that return 50 full pages (5,000 postings) produce an uncharged `pagination-limit` error; no potentially incomplete board is delivered. The run still succeeds. Filters and delivery caps cannot turn an incomplete fetch into a complete result.
- Lever EU and global boards are separate namespaces. The `region` field distinguishes them; compare `(ats, region, board, jobId)` across runs.
- Fetches use a 15 s timeout, one retry on rate limits, upstream errors, timeouts and network errors, three boards in parallel with a per-host spacing. Very large boards (thousands of roles with descriptions) can take a minute or two.
- EU boards: Lever EU (`jobs.eu.lever.co`) is supported; a Greenhouse EU careers URL is tried on the documented API host and then, best-effort, on an EU host — if neither answers it is reported `not-found`.
- Descriptions are capped at 20,000 characters (`descriptionTruncated: true` when cut). Individual API responses above 16 MB are refused (`too-large`).
- Max 200 boards per run, 5,000 jobs per board, 20,000 jobs per run. Default run timeout 3,600 s; 90 s before its timeout the actor stops dispatching boards, clamps in-flight fetches to the time left and stops charging, and reports what it left out in a `run_interrupted` notice to leave time for a clean finish. Platform failures or delivery failures can still fail a run.
- The actor extracts **job postings only** — no recruiter or contact fields are read from the ATS payloads; descriptions are delivered as the employer published them and may contain whatever the employer wrote. Your board entries are the only input echoed back (in `board` / `error` rows). Data requests go only to the supported vendors' API hosts; optional usage telemetry is described below. The careers URLs you paste are parsed for the board slug and never fetched themselves (scheme-less hosts such as `jobs.lever.co/netflix` are accepted).

### Telemetry

When optional usage telemetry is enabled, the actor attempts a start record and a charge summary in the developer's telemetry dataset. Records contain the run ID, a salted account hash (not the raw account ID), run origin, timestamps, paying-account flag and charged-event counts. Inputs and job results are not sent. Telemetry is optional and does not affect extraction or billing.

Looking for transcripts of a company's podcasts or talks instead? See [Audio Transcription](https://apify.com/kaz_kakyo/audio-transcriber) and [YouTube Transcripts](https://apify.com/kaz_kakyo/youtube-transcripts) from the same developer.

# Actor input Schema

## `boards` (type: `array`):

Required. One entry per company board. Accepts careers URLs, with or without https:// (https://boards.greenhouse.io/COMPANY, https://job-boards.greenhouse.io/COMPANY, https://jobs.lever.co/COMPANY, https://jobs.ashbyhq.com/COMPANY; Lever EU boards on jobs.eu.lever.co are supported, Greenhouse EU boards are best-effort), vendor tokens (greenhouse/COMPANY, lever/COMPANY, ashby/COMPANY) or a bare COMPANY slug (all three vendors are tried; the first that resolves is delivered; a bare slug that duplicates an explicit global entry is ignored; Lever EU remains separate). Unknown or unreachable boards become uncharged error rows — the run still succeeds. Max 200 boards per run (excess recorded as a notice).

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

Cap on jobs delivered per board, applied after filters, in the board's native order. The prefill (10) keeps a test run cheap; raise it to 5000 for larger boards. Lever returns a pagination-limit error if all 50 fetched pages are full, rather than silently delivering an incomplete board.

## `descriptionFormat` (type: `string`):

text = plain-text description (HTML stripped, entities decoded — compact for LLM/agent use); html = the description HTML as published; none = description is null (smallest rows; all other fields, including Greenhouse departments and offices, are still delivered).

## `keyword` (type: `string`):

Optional. Case-insensitive substring match on the job title (e.g. engineer, sales, designer). Applied before billing and before maxJobsPerBoard.

## `location` (type: `string`):

Optional. Case-insensitive substring match on the job's location text (primary and secondary locations, country), e.g. London, Remote, United States.

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

Deliver only jobs the ATS flags as remote (Ashby isRemote, Lever workplaceType = remote, Greenhouse location or title containing the word remote).

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

Optional ISO 8601 date (e.g. 2026-08-01). Keeps jobs whose posted date (Greenhouse: first-published date when its list API includes it, else last-updated date; Lever: createdAt; Ashby: publishedAt) is on or after this date; postings that carry no date at all are excluded. An unparsable value becomes an uncharged error row and the run stops.

## `maxTotalJobs` (type: `integer`):

Hard cap on delivered jobs across all boards (budget protection). Also cap spend with the run's max total charge.

## `includeBoardSummary` (type: `boolean`):

In the separate status dataset, also write one uncharged type: board row per successfully fetched board with status (ok | empty | duplicate), total, filtered and delivered counts, and a note when a cap or the budget cut it short. Boards that could not be fetched get an uncharged type: error row (not-found, blocked, rate-limited, …) instead. This is how an empty-but-valid board (a company that publishes nothing through the public API) is told apart from a wrong slug.

## Actor input object example

```json
{
  "boards": [
    "https://boards.greenhouse.io/airbnb",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "maxJobsPerBoard": 10,
  "descriptionFormat": "text",
  "remoteOnly": false,
  "maxTotalJobs": 5000,
  "includeBoardSummary": true
}
```

# Actor output Schema

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

Jobs only, with all fields: ATS, board, region, stable job ID, title, locations, department, remote flag, optional structured salary and dates, apply URL and description.

## `boards` (type: `string`):

Separate uncharged status dataset: board summaries when includeBoardSummary is true, errors (including pagination-limit) and notices. Check it for failures or caps even when Jobs is empty or the run succeeds.

# 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 = {
    "boards": [
        "https://boards.greenhouse.io/airbnb",
        "https://jobs.ashbyhq.com/ramp"
    ],
    "maxJobsPerBoard": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("kaz_kakyo/greenhouse-lever-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 = {
    "boards": [
        "https://boards.greenhouse.io/airbnb",
        "https://jobs.ashbyhq.com/ramp",
    ],
    "maxJobsPerBoard": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("kaz_kakyo/greenhouse-lever-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 '{
  "boards": [
    "https://boards.greenhouse.io/airbnb",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "maxJobsPerBoard": 10
}' |
apify call kaz_kakyo/greenhouse-lever-ashby-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kaz_kakyo/greenhouse-lever-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/OVkgS3XZYlLxkYVJy/builds/s4WWjTgjJmbOvNQus/openapi.json
