# Career Site Jobs Scraper: Greenhouse, Lever, Ashby, Workable (`weio/career-site-jobs-scraper`) Actor

Open jobs from company career sites on Greenhouse, Lever, Ashby, Workable and Recruitee via their official public job-board APIs. Pay per job.

- **URL**: https://apify.com/weio/career-site-jobs-scraper.md
- **Developed by:** [Weio, Inc.](https://apify.com/weio) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.50 / 1,000 job listings

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

## Career Site Jobs Scraper: Greenhouse, Lever, Ashby, Workable and Recruitee

Get the **open jobs from company career sites**: title, location, remote or hybrid, department, salary where
the employer publishes it, posting date, job and apply links, and the full job description. Give it a careers
page, a company website or just a company name, and it finds the company's job board and returns every open
role as one clean, identical row, whichever hiring platform the company uses.

It reads the **official public job-board APIs** of five applicant-tracking systems (Greenhouse, Lever, Ashby,
Workable and Recruitee), so there is no browser, no proxy, no login and no breakage when a careers page is
redesigned.

You pay **$1.50 per 1,000 jobs**. Companies that cannot be found, errors and jobs your filters remove are free.

### What you can do with it

- **Sales prospecting from hiring signals.** Companies hiring "data engineers", "SDRs" or "security engineers"
  are telling you what they are investing in. Pull every company's open roles, filter by title, department,
  location or posting date, and turn hiring into a lead list.
- **Job boards and aggregators.** Keep a niche job board fresh with jobs straight from employers, with the
  direct job and apply links.
- **Recruiting and labour-market research.** Track which roles a set of competitors is hiring, where, and how
  fast the list changes. Run it on a schedule and filter on "posted in the last N days".
- **Salary research.** Where the platform publishes pay (Lever, Ashby and Recruitee do, when the employer
  fills it in) the `salary` field carries it, for example `EUR 3600-5000 per month`.

### Two modes

**Companies mode.** You list companies. For each one the actor finds its job board, reads the open
jobs live, applies your filters and returns the matches.

**Search mode.** You list no companies. The actor filters a daily-refreshed index of career-site jobs from many
companies by keyword, location, department, remote and posting date, and returns the newest matches first.
The index only covers companies it has indexed: on 3 October 2026 that was about 1,600 companies and 47,000
open jobs, and it grows as more company boards are added. Each row says when the index was built
(`index_generated_at`). For a specific company that is not in the index, use companies mode.

### Supported platforms

| Platform | Example career page you can paste | What the rows include |
| --- | --- | --- |
| Greenhouse | `https://boards.greenhouse.io/stripe` (also `job-boards.greenhouse.io`, `boards.eu.greenhouse.io`) | company name, department, location, posting date, description |
| Lever | `https://jobs.lever.co/palantir` (EU boards: `jobs.eu.lever.co`) | team, department, employment type, workplace type (remote, hybrid, on-site), salary range when published |
| Ashby | `https://jobs.ashbyhq.com/openai` | department, team, employment type, secondary locations, remote flag, workplace type, compensation summary when published |
| Workable | `https://apply.workable.com/huggingface/` (or `huggingface.workable.com`) | company name, department, employment type, country, remote flag |
| Recruitee | `https://channable.recruitee.com/` | company name, department, employment type, remote, hybrid or on-site, salary when published |

Other platforms (SmartRecruiters, Workday, iCIMS, Taleo and so on) and job aggregators (LinkedIn, Indeed) are
**not** supported and are never contacted: an entry that points to one, or a careers page that redirects to one,
is reported as unresolved (free).

Every row has the same fields whichever platform it came from. A value the platform does not publish is `null`.
Nothing is guessed.

### Input

| Field | What it does |
| --- | --- |
| `mode` | Required: `companies` or `search`. There is no default, so a run with no input is rejected by Apify before it starts and nothing is billed. |
| `companies` | Companies mode. One entry per company: a careers page or job-board URL, a company website, a prefix such as `greenhouse:stripe`, or a company name. Up to 2,000 entries. Required in companies mode: there is no default list, so a run without it stops before anything is fetched or charged. |
| `keywords` | Keep jobs whose **title** contains any of these (not case sensitive, substring match). |
| `searchKeywordsInDescription` | Companies mode. Also match the keywords in the job description. |
| `excludeKeywords` | Drop jobs whose title contains any of these. |
| `locations` | Keep jobs whose location or country contains any of these texts, for example `Berlin`, `Remote`, `United States`. |
| `remoteOnly` | Keep only jobs the company marks as remote. Jobs with no stated remote status are left out. |
| `postedWithinDays` | Keep jobs posted in the last N days. Jobs with no posting date are left out when this is set. Where a platform gives only a date (Workable), a job dated N days ago counts. |
| `departments` | Keep jobs whose department or team contains any of these texts. |
| `includeDescription` | Add the plain-text description to each row. On by default in companies mode, off in search mode. |
| `maxJobsPerCompany` | At most this many jobs from each company, newest first. `0` means no limit. |
| `maxItems` | Total jobs returned (and paid for) in one run. Default 50, maximum 100,000. |

Keyword, location and department lists match if **any** entry matches. All the different filters must pass.

#### Example: jobs from a list of companies

```json
{
  "mode": "companies",
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "huggingface.co",
    "Palantir",
    "greenhouse:airbnb"
  ],
  "keywords": ["data engineer", "machine learning"],
  "locations": ["remote", "united states"],
  "postedWithinDays": 30,
  "maxJobsPerCompany": 25,
  "maxItems": 500
}
```

#### Example: search the index, newest first

```json
{
  "mode": "search",
  "keywords": ["account executive", "sales development"],
  "remoteOnly": true,
  "postedWithinDays": 7,
  "maxItems": 1000
}
```

### How companies are found

For every entry, in this order:

1. **A job-board URL or prefix** (`https://jobs.lever.co/palantir`, `greenhouse:stripe`) goes straight to that
   board. This is the most reliable input.
2. **A company website** (`stripe.com`, `https://acme.com/careers`): the actor reads the page you gave (or the
   homepage), then the site's careers page and `/jobs` (three pages at most) and looks for links, embeds and
   redirects to one of the five platforms.
3. **Otherwise**, and for plain company names, it tries the likely board names (for example `huggingface` and
   `hugging-face`) on Greenhouse, Lever, Ashby, Workable and Recruitee in that order, and takes the first board
   that exists and has at least one open job. Where the platform publishes the company's name (Greenhouse,
   Workable, Recruitee), that name must match what you entered; a board that belongs to a differently named
   company is not used and is listed in `SUMMARY` as a possible match. For a site on shared hosting
   (`acme.notion.site`, `acme.vercel.app`, `acme.webflow.io`) it tries `acme`, never the hosting provider's
   own name.

A company name is still a best guess, more so on Lever and Ashby, which publish no company name to check: a
different company can share the same board name. Check `company_slug` and `careers_url` in the output, or paste
the careers URL when you need certainty. Inputs that cannot be resolved are
not returned as rows and are not charged. They are listed with the reason in the `SUMMARY` record and in the
run's status message.

### Output

One row per open job. The same fields for every platform and both modes:

| Field | Meaning |
| --- | --- |
| `ats` | `greenhouse`, `lever`, `ashby`, `workable` or `recruitee` |
| `company_slug`, `company_name` | board name on the platform, and the company's name when the platform or your input provides it |
| `job_id` | the platform's job id (text) |
| `title`, `location`, `country` | as the platform publishes them (`country` is a country code or name, depending on the platform) |
| `remote` | `true`, `false` or `null`. `false` only when the platform says so: a hybrid or on-site workplace type, or a not-remote flag (Workable, Recruitee). |
| `workplace_type` | `remote`, `hybrid`, `onsite` or `null` |
| `department`, `team`, `employment_type` | when published |
| `salary` | pay as published, for example `USD 120000-150000 per year`, otherwise `null` |
| `posted_at`, `updated_at` | ISO 8601 UTC. Some platforms only give a date, which becomes midnight UTC |
| `job_url`, `apply_url`, `careers_url` | the job page, the application page, and the company's board |
| `description_text` | plain-text description, when requested |
| `source_api` | the API address the row was read from |
| `scraped_at` | when it was read (search mode: when the index was built) |
| `index_generated_at` | search mode only: when the index was built |

A real row (description shortened):

```json
{
  "ats": "recruitee",
  "company_slug": "channable",
  "company_name": "Channable",
  "job_id": "2548164",
  "title": "Python Software Engineer - Product team",
  "location": "Utrecht, Utrecht, Netherlands",
  "country": "NL",
  "remote": false,
  "workplace_type": "hybrid",
  "department": "Engineering",
  "team": null,
  "employment_type": "fulltime_permanent",
  "salary": "EUR 3600-5000 per month",
  "posted_at": "2026-07-28T07:21:13Z",
  "updated_at": "2026-09-28T07:43:26Z",
  "job_url": "https://jobs.channable.com/o/python-software-engineer-product-team-1",
  "apply_url": "https://jobs.channable.com/o/python-software-engineer-product-team-1/c/new",
  "careers_url": "https://channable.recruitee.com/",
  "description_text": "What's in it for you (to start with):\n\n- Gross monthly salary ranging from EUR 3.600 to EUR 5.000 based on a 40-hour work week. [...]",
  "source_api": "https://channable.recruitee.com/api/offers/",
  "scraped_at": "2026-10-03T04:51:46Z"
}
```

Besides the dataset, every run saves a **`SUMMARY`** record: each board that was read (platform, name, how it
was found, jobs seen and jobs returned), the inputs that could not be resolved and why, boards that returned an
error, the number of jobs returned and charged, and why the run stopped.

### Pricing

**$1.50 per 1,000 jobs** (pay-per-event, event `job`: one job row returned). Unresolved companies, errors and
filtered jobs are free. So are duplicates (the same job found twice) and, in search mode, jobs that turned out to
be gone when their description was fetched. Set **Maximum jobs in total** (`maxItems`) to cap what a run can
cost. The run also stops when the maximum charge you set on the run is reached. If the platform restarts a run
(for example when it moves the run to another server), the jobs it already returned are not returned or charged
again, and they still count towards `maxItems`.

### Compliance and good manners

- It uses each platform's **official public job-board API**, the same data employers publish for job seekers
  and job sites. No logins, no CAPTCHA solving, no proxies.
- It checks **robots.txt** before the first request to every host, including a company's own website, and
  follows it. If a robots.txt cannot be read, that host is skipped for the run.
- It identifies itself with the user agent `WeioBot/1.0`.
- It sends **at most one request per second to any host** (or the Crawl-delay a host asks for, up to 10
  seconds). Different hosts are read in parallel. When a host answers 429 or 503 it pauses that host for as long
  as it is told and gives up on that address after three tries. If the host asks for more than 60 seconds it
  gives up at once and leaves the host alone for that time, and a host that keeps answering 429 is left alone
  for the rest of the run.
- Addresses you supply are checked before they are opened: only http and https on the standard ports, and never
  private, local or internal network addresses. The connection is made to the address that was checked, so a
  name that changes its answer in between cannot redirect it to an internal address.

You are responsible for how you use the data, including any rules that apply to contacting people about jobs or
to republishing job descriptions.

### Limitations

- Only the five platforms above. A company that uses another system, or a custom careers page without one of
  these platforms behind it, cannot be read, and is reported as unresolved (free).
- Lever and Ashby do not publish a company name. `company_name` is filled from the name you typed, or from the
  website's own site name when that website links to the board, otherwise it is `null`.
- Fields a platform does not publish are `null`: for example Greenhouse has no structured country, employment
  type or salary field.
- `locations` matches the text the company wrote, so `United States` will not match a Lever job whose country is
  just `US`. Use the spellings you see in the output.
- Name matching tries a few likely board names; it cannot find a company whose board name has nothing to do with
  its name. Names that do not match are slower to rule out, because each guess costs requests that are limited to
  one per second per host. Board URLs are fastest.
- Search mode only contains companies that are in the index. In search mode, descriptions are fetched live from
  at most 200 company boards per run, and the daily index does not contain descriptions itself, so
  `searchKeywordsInDescription` is not available there.
- A single board response larger than 20 MB is refused. A Greenhouse or Workable board that is only that large
  with descriptions is read without them; its rows then have no `description_text` and `SUMMARY` says so.

### FAQ

**Do I pay for companies that cannot be found?** No. Only job rows you receive are charged.

**Why is `salary` empty?** The employer did not publish pay on that platform. Greenhouse, for instance, has no
salary field (pay is sometimes written inside the description text).

**How current is the data?** In companies mode every run reads the boards live. In search mode each row carries
`index_generated_at`, and turning on `includeDescription` re-checks every returned job against its live board.

**Can I watch for new jobs?** Schedule the actor and set `postedWithinDays` (for example `1` for a daily run).
Duplicates inside one run are removed, but a scheduled run does not know what earlier runs returned, so compare
on `job_id` and `company_slug` if you need only jobs you have not seen.

**What can I put in `companies`?** Careers or job-board URLs, company websites, `platform:name` prefixes such as
`lever:palantir`, or plain company names. You can mix them.

**Which job-board address is the right one?** Whatever you see when you open the company's careers page and
click through to its jobs: for example `boards.greenhouse.io/<company>`, `jobs.lever.co/<company>`,
`jobs.ashbyhq.com/<company>`, `apply.workable.com/<company>` or `<company>.recruitee.com`.

**Does it scrape LinkedIn or Indeed?** No. It only reads the five platforms listed above, through their public
job-board APIs.

**A company I know uses Greenhouse was not found. Why?** Try pasting its board URL. Some companies use a board
name that differs from their name.

Built and maintained by Weio, an AI-operated software company.

# Actor input Schema

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

Companies: look up the career sites of the companies you list and read their open jobs live. Search: search a daily index of career-site jobs across many companies, with the filters below, without naming companies.

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

Companies mode only. One entry per line: a careers page or job-board URL (for example https://boards.greenhouse.io/stripe), a company website (stripe.com or https://stripe.com/jobs), a prefix like greenhouse:stripe, or just a company name (Hugging Face). Job-board URLs are the most reliable; names are matched by trying the likely board names.

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

Keep a job when its title contains any of these words or phrases (not case sensitive; 'engineer' also matches 'Engineering Manager'). Leave empty for all jobs.

## `searchKeywordsInDescription` (type: `boolean`):

Companies mode only. Also keep a job when its description contains one of the keywords (not only its title). The search index has no descriptions, so search mode ignores this and matches titles.

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

Drop a job when its title contains any of these words (not case sensitive), for example intern or director.

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

Keep a job when its location or country contains any of these texts (not case sensitive), for example Berlin, Remote or United States. The text is matched as written by the company: some boards give a country code such as US instead of United States.

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

Keep only jobs the company marks as remote. Jobs where remote status is not stated are left out.

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

Keep only jobs posted in the last N days. 0 means no limit. Jobs without a posting date are left out when this is set.

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

Keep a job when its department or team contains any of these texts (not case sensitive), for example Engineering or Sales.

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

Add the plain-text job description to every row. Leave unset to use the mode's default: on in companies mode, off in search mode. In search mode, turning it on makes the run read each company's live job board (up to 200 boards per run) to fetch descriptions, and jobs that are no longer posted are dropped and not charged.

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

Return at most this many jobs from each company, newest first. 0 means no limit. The form suggests 20 so a first run shows jobs from every company you list.

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

The run stops after this many job rows. This is also the most you can be charged for jobs in one run.

## Actor input object example

```json
{
  "mode": "companies",
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "https://jobs.lever.co/palantir",
    "https://jobs.ashbyhq.com/openai"
  ],
  "searchKeywordsInDescription": false,
  "remoteOnly": false,
  "postedWithinDays": 0,
  "maxJobsPerCompany": 20,
  "maxItems": 50
}
```

# Actor output Schema

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

The default dataset, overview view.

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

Boards read, jobs found and output, companies that could not be resolved (with reasons), jobs charged and stop reason.

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

// Run the Actor and wait for it to finish
const run = await client.actor("weio/career-site-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 = {
    "mode": "companies",
    "companies": [
        "https://boards.greenhouse.io/stripe",
        "https://jobs.lever.co/palantir",
        "https://jobs.ashbyhq.com/openai",
    ],
    "maxJobsPerCompany": 20,
}

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

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,weio/career-site-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/6PuNHgwiQVDJmmdH9/builds/RNShEz9qSVJpeonYW/openapi.json
