# Career Page Jobs Scraper: Greenhouse, Lever, Ashby & More (`charlesmarkette/career-page-jobs-scraper-greenhouse-lever-ashby`) Actor

Collect open jobs from company job boards on Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee or Personio via public job-board APIs. Normalized rows: title, location, remote, department, salary when published. Filters and only-new-jobs monitoring.

- **URL**: https://apify.com/charlesmarkette/career-page-jobs-scraper-greenhouse-lever-ashby.md
- **Developed by:** [Charles Markette](https://apify.com/charlesmarkette) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.05 / 1,000 jobs

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?

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

**Career Page Jobs Scraper** returns the open jobs a company publishes through a supported applicant tracking system (ATS) board (up to 10,000 per board): **Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee and Personio**. Give it career page URLs, ATS board URLs or board names. It reads each vendor's **public job-board API**, the same endpoints these vendors publish so companies can list jobs on their own websites. You get one row per job with the same columns for every vendor: **title, department, team, location, remote, employment type, salary range (when the employer publishes it), posted and updated dates, job and apply links**, and optionally the full description. Turn on **Only new jobs** and each run returns just the roles that appeared since the last run.

### Why this Actor

- **Official public APIs for the jobs.** Each board is read through the vendor's documented public job-board endpoint (Greenhouse Job Board API, Lever Postings API, Ashby Job Postings API, Workable's public jobs widget API, SmartRecruiters Posting API, Recruitee's careers-site offers API, Personio's XML job feed). These endpoints need no login and return exactly what the company has chosen to publish. When you give a company's own careers page instead of a board URL, that one page is read once only to find which board it links to.
- **Works from a company's own careers page.** If you give `https://www.acme.com/careers` instead of a board URL, the page is read **once** and the Actor looks for an embedded or linked board of a supported ATS: a Greenhouse embed script (`boards.greenhouse.io/embed/job_board?for=acme`), a Lever, Ashby, Workable, SmartRecruiters, Recruitee or Personio link, iframe or script. If the careers page redirects to a board, that works too.
- **One schema across vendors.** `remote` follows the ATS's workplace type when it has one (`true` for remote, `false` for hybrid and on-site, even if the vendor also sets a "remote allowed" flag); otherwise its remote flag; otherwise `true` when the location text says "Remote" (a label, not a guarantee the role is fully remote); `null` when the ATS gives nothing. Timestamps are ISO 8601 UTC; where an ATS gives only a date (Workable), you get the date. Employment codes such as `FullTime` or `fulltime_permanent` become `Full-time` / `Full-time, Permanent`.
- **Salary only from the employer.** `salaryMin`, `salaryMax`, `salaryCurrency` and `salaryPeriod` are filled only from the ATS's own compensation fields (Ashby compensation, Lever salary range, Recruitee salary, and Greenhouse pay-transparency ranges when its board API returns them). They are never guessed from the description text.
- **Monitoring built in.** *Only new jobs* remembers up to 20,000 job IDs per board, so a scheduled run returns only new roles. Filters by title keywords, location, department and remote let you watch exactly the roles you care about.
- **Polite and safe.** At most 2 requests at a time per ATS host, retries with backoff on 429 and 5xx (honouring `Retry-After`), timeouts, and a size cap on every response. The one careers page fetched per input is refused if it resolves to a private, local or cloud-metadata address, and every redirect hop is checked again.

### Use cases

- **Sales signals:** a company hiring its first "Head of Data" or five "Account Executives" is a buying signal. Run daily on your target accounts with *Only new jobs* and *Title keywords* `head of, vp, director`, and send new rows to Slack or your CRM.
- **Recruiting and talent intelligence:** see which competitors are hiring which roles, in which cities, at which published pay ranges.
- **Job boards and newsletters:** feed a niche job board (for example remote engineering roles at 300 startups) with *Remote jobs only* and *Departments* `engineering`.
- **Alerts for job seekers:** get an email when any of your 50 favourite companies posts a "Product Designer" role in Berlin or remote.
- **Market research:** count open roles by department over time to track a company's growth or hiring freeze.
- **Enrich a company list:** point it at a dataset of companies with a careers URL column and get every open job next to each company's own fields.

### How to use it

1. Add **Career page URLs** (board URLs like `https://boards.greenhouse.io/stripe`, `https://jobs.lever.co/palantir`, `https://jobs.ashbyhq.com/ramp`, or a company's own careers page), and/or **ATS boards** written as `ats:board` (`greenhouse:stripe`, `lever:netflix`, `ashby:ramp`, `workable:acme`, `smartrecruiters:Acme`, `recruitee:acme`, `personio:acme`; `greenhouse-eu:` and `lever-eu:` for boards on the EU instances).
2. Optional: set **Title keywords**, **Exclude title keywords**, **Locations**, **Departments** or **Remote jobs only**, and turn on **Only new jobs** for monitoring.
3. Run it, or schedule it. Export as CSV, Excel or JSON, or chain it with an integration (see Related Actors).

**Input example**

```json
{
  "startUrls": ["https://boards.greenhouse.io/stripe", "https://jobs.lever.co/palantir", "https://www.example.com/careers"],
  "boards": ["ashby:ramp", "personio:acme"],
  "keywords": ["engineer", "account executive"],
  "excludeKeywords": ["intern"],
  "locations": ["New York", "Remote"],
  "onlyNew": true,
  "maxJobsPerCompany": 200
}
```

**Output example**

```json
{
  "company": "Gamma",
  "ats": "ashby",
  "board": "gamma",
  "jobId": "1b2c3d4e-0000-4000-8000-000000000001",
  "title": "Product Designer",
  "department": "Design",
  "team": "Core Product",
  "location": "New York",
  "locations": ["New York", "San Francisco"],
  "remote": false,
  "workplaceType": "hybrid",
  "employmentType": "Full-time",
  "salaryMin": 140000,
  "salaryMax": 180000,
  "salaryCurrency": "USD",
  "salaryPeriod": "year",
  "postedAt": "2026-10-14T16:20:11Z",
  "updatedAt": null,
  "jobUrl": "https://jobs.ashbyhq.com/gamma/1b2c3d4e-0000-4000-8000-000000000001",
  "applyUrl": "https://jobs.ashbyhq.com/gamma/1b2c3d4e-0000-4000-8000-000000000001/application",
  "input": "https://jobs.ashbyhq.com/gamma",
  "discoveredFrom": null,
  "scrapedAt": "2026-10-17T09:00:00Z",
  "error": null
}
```

- `company` is the company name when the ATS gives one (Greenhouse, Workable, SmartRecruiters, Recruitee, Personio), otherwise the board name.
- `locations` lists every location of the job; `location` is the main one.
- `workplaceType` is `remote`, `hybrid` or `onsite` when the ATS has that field.
- `applyUrl` is set only when it differs from `jobUrl`.
- `discoveredFrom` is the careers page a board was found on.
- `postedAt` is a date without a time (`2026-10-09`) for Workable, which publishes only the day.
- With **Include job descriptions**, rows also get `descriptionText` (HTML stripped, whitespace collapsed) and `descriptionHtml`.
- An input that couldn't be read gives one row with `input` and `error` set and the job columns empty.
- A run summary with a line per company (jobs on the board, matching, already seen, output) is saved as `OUTPUT` in the key-value store.

### Supported and not supported

| Supported (public job-board API) | Board URL forms recognised |
|---|---|
| Greenhouse (US and EU) | `boards.greenhouse.io/{token}`, `job-boards.greenhouse.io/{token}`, `job-boards.eu.greenhouse.io/{token}`, embed `?for={token}` |
| Lever (US and EU) | `jobs.lever.co/{company}`, `jobs.eu.lever.co/{company}` |
| Ashby | `jobs.ashbyhq.com/{name}` |
| Workable | `apply.workable.com/{account}`, `{account}.workable.com` |
| SmartRecruiters | `jobs.smartrecruiters.com/{companyId}`, `careers.smartrecruiters.com/{companyId}` |
| Recruitee | `{company}.recruitee.com` |
| Personio | `{company}.jobs.personio.de`, `{company}.jobs.personio.com` |

**Not supported:** Workday, iCIMS, Taleo, SuccessFactors, LinkedIn, Indeed, Glassdoor, and company websites that don't use one of the boards above. The Actor doesn't log in, doesn't use undocumented internal APIs, and doesn't scrape HTML job pages. For those inputs you get a free error row saying "No supported ATS found on this page (supported: …)".

### How "Only new jobs" works

- The **first run** with *Only new jobs* outputs the board's current jobs (after your filters and *Max jobs per company*) and remembers them. Later runs output only jobs that weren't output before.
- A job is remembered **only once it has been output**. Jobs left out because of *Max jobs per company*, *Max output rows* or your cost limit can come in a later run if they're still listed then. Memory holds up to 20,000 IDs per board and drops the oldest; resetting the memory or changing filters starts fresh, so previously collected jobs can appear (and be charged) again.
- The memory lives in the key-value store **`career-page-jobs-state`** in your account: up to 20,000 job IDs per board (the oldest are dropped first).
- Each saved task has its own memory, and changing the filters starts a fresh one. Set **Memory name** to choose it yourself: runs with the same memory name share it.

### How much does it cost?

Pay per event: **$1.50 per 1,000 jobs** ($0.0015 per output job row), Apify platform usage included, plus **$0.005 per run start**. These are free: error rows (unknown ATS, board not found, page couldn't be read), jobs left out by your filters, and jobs you've already seen with *Only new jobs*.

**Apify Store discounts:** the price above is for Apify's Free plan. Paid plans pay less per 1,000 jobs: $1.35 on Starter, $1.20 on Scale, $1.05 on Business. The "from" price at the top of the Store page is the Business price.

**Worked examples:**

*Examples use the Free-plan rate. Where an example doesn't add it, each run also has its run-start charge, counted per GB of Actor memory (minimum one; one at the default memory); Apify Proxy (where used) is billed separately.*

- 50 companies with about 40 open jobs each: 2,000 × $0.0015 + $0.005 = **$3.005**.
- Recurring examples assume the initial matching jobs were already collected, 30 daily runs and one minimum start unit per run. A daily monitor of 100 companies with *Only new jobs* that finds about 30 new roles a day: $1.35 in job charges + $0.15 in start charges = **$1.50 per 30 days**. The first run also charges for the current matching jobs it outputs (for example 4,000 jobs = $6.00).
- The same 100 companies, filtered to sales leadership titles, about 3 new roles a day: $0.135 in job charges + $0.15 in start charges = about **$0.29 per 30 days**.

If you set a maximum cost per run, the Actor stops cleanly at that limit, and with *Only new jobs*, jobs not yet output may appear in a later run if they are still listed and fit that run's filters and limits.

### FAQ and limits

#### Why is the salary empty for most jobs?

Salary is filled only when the employer publishes a structured range in the ATS (common in US states with pay-transparency laws). The Actor never reads numbers out of the description text.

#### Why is `remote` null?

The ATS didn't say. Greenhouse and Personio have no remote field, so `remote` is `true` only when the location says "Remote", and `null` otherwise.

#### My company's careers page isn't recognised.

The board must be visible in the page's HTML: a link, an iframe, a script or an embed setting that points at one of the supported boards. Pages that load the board only after JavaScript runs, or that use an unsupported ATS (Workday, iCIMS, Taleo…), aren't recognised. Find the board URL (often the "View all jobs" link) and use it directly, or write it as `ats:board`. If a page links several boards, the one referenced most often is used.

#### Does the same board twice cost twice?

No. Each board is read once per run; repeats (for example the same company in two dataset items) are skipped.

#### Other limits

- Up to 10,000 jobs are read per board.
- Descriptions are cut at 200,000 characters.
- SmartRecruiters needs one extra request per job for descriptions, which makes runs with *Include job descriptions* slower on big SmartRecruiters boards.
- Rows within a board are newest first when the ATS gives a publish date.
- Some vendors (Lever in particular) don't publish a company display name, so `company` is the board name there.

### Related Actors

| Actor | Use it to |
|---|---|
| [Send Email Report & Slack, Discord, Teams, Telegram Alerts](https://apify.com/charlesmarkette/send-email-report-slack-discord-teams-telegram-alerts) | Get the new jobs as an email or chat message after each run. |
| [Google Sheets Sync](https://apify.com/charlesmarkette/google-sheets-sync) | Append the jobs to a Google Sheet. |
| [Merge & Deduplicate Datasets](https://apify.com/charlesmarkette/merge-deduplicate-datasets-only-new-items) | Merge jobs from several runs or sources and remove duplicates. |
| [Dataset Filter & Transform](https://apify.com/charlesmarkette/dataset-filter-transform-rename-flatten-fields) | Filter jobs by any column, or rename and drop columns before export. |
| [Website Contact Info Scraper](https://apify.com/charlesmarkette/website-contact-info-scraper-emails-phones-socials) | Find the published emails, phones and social profiles of the hiring companies. |
| [Domain WHOIS & DNS Lookup](https://apify.com/charlesmarkette/domain-whois-dns-lookup-bulk-rdap) | Add company domain age and email provider to your account list. |

# Actor input Schema

## `startUrls` (type: `array`):

One per line. ATS board URLs (boards.greenhouse.io/stripe, jobs.lever.co/palantir, jobs.ashbyhq.com/ramp, apply.workable.com/acme, jobs.smartrecruiters.com/Acme, acme.recruitee.com, acme.jobs.personio.de) or a company's own careers page: it is read once to find the embedded or linked board of a supported ATS.

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

Optional. Boards written as ats:board, one per line: greenhouse:stripe, lever:netflix, ashby:ramp, workable:acme, smartrecruiters:Acme, recruitee:acme, personio:acme. Use greenhouse-eu: or lever-eu: for boards on the EU instances.

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

Optional. Keep only jobs whose title contains any of these (case-insensitive, plain substring match: 'engineer' also matches 'Engineering Manager').

## `excludeKeywords` (type: `array`):

Optional. Leave out jobs whose title contains any of these (case-insensitive), e.g. intern, senior.

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

Optional. Keep only jobs whose location text contains any of these (case-insensitive), e.g. London, Germany, Remote.

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

Keep only jobs with remote = true: the ATS's workplace type decides when it has one (hybrid and on-site are excluded); otherwise its remote flag; otherwise 'Remote' in the location.

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

Optional. Keep only jobs whose department or team contains any of these (case-insensitive), e.g. Sales, Engineering.

## `includeDescription` (type: `boolean`):

Add descriptionText (plain text) and descriptionHtml columns. Rows get much larger, and SmartRecruiters needs one extra request per job.

## `maxJobsPerCompany` (type: `integer`):

The newest jobs to output from each board (after the filters).

## `maxResults` (type: `integer`):

Optional cap on output rows for the whole run (jobs plus any error rows).

## `maxConcurrency` (type: `integer`):

How many boards to read at the same time (1–20). Each ATS host still gets at most 2 requests at a time.

## `onlyNew` (type: `boolean`):

Output only jobs not output by an earlier run with this setting. The first run outputs the current jobs. Memory: up to 20,000 job IDs per board, in the key-value store 'career-page-jobs-state'.

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

Optional. Runs with the same memory name share their 'only new' memory. By default each saved task has its own memory (and changing the filters starts a fresh one). Letters, digits, '-', '\_' and '.'.

## `datasetId` (type: `string`):

Optional. A dataset of companies (from a lead list, a directory scraper, a CSV import…). Each item's career page or board (see URL field) is read and every job becomes a row with the item's fields next to the job columns. Use {{resource.defaultDatasetId}} in an integration.

## `urlField` (type: `string`):

The item field that holds the career page URL, board URL or ats:board value. Dotted paths work (company.careersUrl). If it holds a list, the first value is used.

## `keepItemFields` (type: `boolean`):

On: each job row is the original item plus the job columns. Off: only the job columns.

## Actor input object example

```json
{
  "startUrls": [
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "remoteOnly": false,
  "includeDescription": false,
  "maxJobsPerCompany": 20,
  "maxConcurrency": 4,
  "onlyNew": false,
  "urlField": "url",
  "keepItemFields": true
}
```

# Actor output Schema

## `items` (type: `string`):

No description

## `summary` (type: `string`):

No description

# 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 = {
    "startUrls": [
        "https://boards.greenhouse.io/stripe",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp"
    ],
    "maxJobsPerCompany": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("charlesmarkette/career-page-jobs-scraper-greenhouse-lever-ashby").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 = {
    "startUrls": [
        "https://boards.greenhouse.io/stripe",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/ramp",
    ],
    "maxJobsPerCompany": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("charlesmarkette/career-page-jobs-scraper-greenhouse-lever-ashby").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 '{
  "startUrls": [
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/ramp"
  ],
  "maxJobsPerCompany": 20
}' |
apify call charlesmarkette/career-page-jobs-scraper-greenhouse-lever-ashby --silent --output-dataset

```

## MCP server setup

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

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/sa4rHfAwABG0cro5Y/builds/v21Pvt9tZm7onY8CQ/openapi.json
