# Career Site Job Scraper – Greenhouse, Lever, Ashby, Workable (`locomint/career-site-jobs-scraper`) Actor

Career site jobs scraper that reads the job feed behind an employer's own careers page: Greenhouse, Lever, Ashby, Workable and more. Paste careers pages, board URLs or provider:board pairs and get one row per vacancy - title, department, location, remote, apply link, posting date and pay range.

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

## Pricing

from $4.80 / 1,000 result delivereds

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

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

## What's an Apify Actor?

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

## How to integrate an Actor?

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

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

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

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

# README

## Career Site Job Scraper – Greenhouse, Lever, Ashby, Workable

This career site jobs scraper reads the job feed behind an employer's own careers page and hands
you one row per open role: title, company, department, location, remote flag, employment type,
the full description, the apply link, the posting date and the pay range where the employer
publishes one. Six applicant tracking systems are supported - Greenhouse, Lever, Ashby, Workable,
Recruitee and Personio - and no job board sits in between, so the jobs are the employer's own
words on the day you run it.

Give it whatever you have: a company careers page, a board URL, or a `provider:board` pair.

### What it does

Every line of input is turned into one board to read:

- **A careers page** such as `https://monzo.com/careers` is read once and searched for the
  applicant tracking system behind it. Verified live: it resolves to `greenhouse:monzo`.
- **A board URL** such as `https://jobs.lever.co/matchgroup`, `https://jobs.ashbyhq.com/posthog`,
  `https://job-boards.greenhouse.io/duolingo` or `https://apply.workable.com/blueground/` is
  recognised from the URL itself, with no request at all.
- **A `provider:board` pair** such as `greenhouse:duolingo` is used as given.

Duplicates are removed. An entry that repeats another exactly is read once, and where two
different-looking entries turn out to be the same board - `ashby:linear` and a careers page
that resolves to `ashby:Linear` - every vacancy is still delivered, and charged for, exactly
once. Anything that is neither a URL nor a known pair is named in the log and skipped.

The board's feed is then read once over a direct datacenter connection - no browser, no proxy,
no login - and parsed into one row per job.

| Field | What goes in it |
|---|---|
| `job_id` | The provider's own id for the opening, as a string. Unique within a board. |
| `title` | The job title as posted. |
| `company` | The employer as the feed names them. Some feeds carry no company name, and those fall back to the board token. |
| `company_domain` | The employer's own website, when the row can show it: the careers page you pasted, or an apply link that is not on the platform. `null` otherwise. |
| `department` | The team or department the feed names. |
| `location` | The primary location, as the employer wrote it. |
| `locations` | Every location this one opening is posted for, primary first. One Greenhouse posting listing three cities comes back as three entries. |
| `remote` | `true` when the feed says remote, `false` when it says on-site or hybrid, `null` when it does not say. Null is not "no". |
| `employment_type` | Full-time, Part-time, Contract, Internship, and so on. |
| `description` | The posting body as readable Markdown. `null` when you turn descriptions off. |
| `apply_url` | Where a candidate applies - the employer's own link. |
| `posted_at`, `updated_at` | ISO 8601. `updated_at` exists on Greenhouse and Recruitee; it is `null` elsewhere. |
| `salary_min`, `salary_max`, `salary_currency`, `salary_interval` | The published pay range, split into columns. Only ever what the employer stated, never estimated. |
| `provider`, `board` | Which applicant tracking system, and which board token. |
| `source_url` | The public feed the row was read from. Fetch it yourself and you get the same bytes. |
| `fetched_at` | ISO 8601 UTC, when the feed was read. |
| `input` | The line you typed, so a row can be matched back to your sheet. |

#### Filters are free

Filters are applied before a job is handed back, so a job you filtered out never appears in your
dataset and is never charged for. A run that reads a 795-job board and keeps four nurses in
Lisbon costs four jobs.

| Filter | What it keeps |
|---|---|
| `titleIncludes` | Jobs whose title contains one of these words. |
| `titleExcludes` | Drops jobs whose title contains one of these words. |
| `locationIncludes` | Jobs matching one of these in any location the job lists. |
| `departmentIncludes` | Jobs in a matching department. |
| `remoteOnly` | Jobs the employer marks remote, or whose location says remote. |
| `postedWithinDays` | Jobs posted inside that many days. |

Two of them leave out jobs where the feed is silent, and deliberately: a job with no posting date
cannot be shown to be from the last seven days, and calling it recent because the field is empty
would be inventing the answer. The same goes for a department filter against a job with no
department.

#### A board that gives nothing costs nothing

Only jobs are charged for. A board that no longer exists, one that answered with nothing readable,
one that has no vacancies today, a careers page with no feed behind it and a line that turns out to
be a board you already named all produce no row at all - each is reported in the run log, with the
reason, and costs you nothing.

### Which platforms, and what each feed carries

Measured on a real board per provider on 12 September 2026. What a feed omits, the row leaves
`null` rather than guessing.

| Provider | Board token looks like | Department | Type | Pay range | Description |
|---|---|---|---|---|---|
| `greenhouse` | `job-boards.greenhouse.io/monzo` → `monzo` | yes | no | not published | yes |
| `lever` | `jobs.lever.co/matchgroup` → `matchgroup` | yes | yes | about half of jobs | yes |
| `ashby` | `jobs.ashbyhq.com/vanta` → `vanta` | yes | yes | about half of jobs | yes |
| `workable` | `apply.workable.com/blueground` → `blueground` | yes | mostly | rarely | yes |
| `recruitee` | `acme.recruitee.com` → `acme` | yes | yes | sometimes | yes |
| `personio` | `acme.jobs.personio.de` → `acme` | mostly | yes | rarely | usually |

Greenhouse publishes the department only in the payload that carries descriptions, so turning
descriptions off on a Greenhouse board leaves `department` empty as well - unless you set
`departmentIncludes`, which asks for that payload anyway so the filter has something to read, and
then drops the body you said you did not want.

### Who it is for

- **Job boards and aggregators** that want listings from the source rather than from another
  board, with the employer's own apply link so candidates land in the employer's pipeline.
- **Recruiters and agencies** watching a list of target employers and seeing what opened this
  week, filtered to the roles they place.
- **Sales and market research teams** using hiring as a signal: who is opening five sales roles,
  which companies publish pay, which are hiring in a market you are entering.
- **Compensation research**, using only the ranges employers publish themselves.
- **AI and data teams** who want a clean, typed job dataset with a `source_url` per row that
  anybody can check.

### How to use it

In the Apify Console:

1. Paste your boards into **Careers pages, board URLs or provider:board pairs**, one per line,
   up to 200 per run.
2. Set **Max jobs per board**. You are charged per job delivered.
3. Add filters if you only want some of the jobs. They are free.
4. Turn **Include the job description** off for a much smaller dataset when you only need
   titles, locations and apply links.
5. Start the run and export the dataset as CSV, Excel or JSON.

From the API, this call starts a run, waits for it and returns the rows:

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/locomint~career-site-jobs-scraper/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"boards": ["greenhouse:monzo", "https://jobs.ashbyhq.com/vanta"], "maxJobsPerBoard": 50}'
```

The synchronous endpoint waits up to 300 seconds. For a long list of boards, start the run with
`POST https://api.apify.com/v2/acts/locomint~career-site-jobs-scraper/runs?token=YOUR_APIFY_TOKEN`
and read its dataset when the run finishes. With the Python client (`pip install apify-client`):

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("locomint/career-site-jobs-scraper").call(
    run_input={
        "boards": ["https://monzo.com/careers", "ashby:vanta", "lever:matchgroup"],
        "titleIncludes": ["engineer"],
        "remoteOnly": True,
        "maxJobsPerBoard": 100,
    }
)
for row in client.dataset(run.default_dataset_id).iterate_items():
    print(row["title"], row["location"], row["apply_url"])
```

### Input example

```json
{
  "boards": [
    "https://monzo.com/careers",
    "https://jobs.ashbyhq.com/vanta",
    "lever:matchgroup",
    "workable:blueground"
  ],
  "descriptions": true,
  "maxJobsPerBoard": 200,
  "titleIncludes": ["engineer", "developer"],
  "titleExcludes": ["intern"],
  "locationIncludes": [],
  "departmentIncludes": [],
  "remoteOnly": false,
  "postedWithinDays": 0,
  "concurrency": 4,
  "boardTimeout": 60
}
```

| Field | Default | Allowed | What it does |
|---|---|---|---|
| `boards` | required | 1 to 200 | Careers pages, board URLs or `provider:board` pairs. Duplicates are removed. |
| `descriptions` | `true` | true / false | Keep the posting body as Markdown. Off is a much smaller dataset for the same price. |
| `maxJobsPerBoard` | 500 | 1 to 10,000 | Most jobs delivered from each board. |
| `titleIncludes`, `titleExcludes` | empty | any words | Title filters, applied before a job is charged. |
| `locationIncludes` | empty | any words | Matched against every location a job lists. |
| `departmentIncludes` | empty | any words | Jobs with no department are left out by this filter. |
| `remoteOnly` | `false` | true / false | Remote jobs only. |
| `postedWithinDays` | 0 (any) | 0 to 3,650 | Jobs with no posting date are left out by this filter. |
| `concurrency` | 4 | 1 to 10 | Boards read at the same time. |
| `boardTimeout` | 60 | 10 to 180 seconds | A board with no answer by then is left out, and not charged. |

### Output example

A real row from a run on 12 September 2026. The description is 4,966 characters of Markdown and is
cut here.

```json
{
  "job_id": "dbd4e02c-0c6c-4031-a658-f57e88ec9943",
  "title": "Senior Software Engineer, Developer Experience",
  "company": "vanta",
  "company_domain": null,
  "department": "Software Engineering",
  "location": "Remote U.S.",
  "locations": ["Remote U.S."],
  "remote": true,
  "employment_type": "Full-time",
  "description": "At Vanta, our mission is to help businesses earn and prove trust ...",
  "apply_url": "https://jobs.ashbyhq.com/vanta/dbd4e02c-0c6c-4031-a658-f57e88ec9943/application",
  "posted_at": "2026-03-17T18:17:38.753000+00:00",
  "updated_at": null,
  "salary_min": 224000.0,
  "salary_max": 263000.0,
  "salary_currency": "USD",
  "salary_interval": "year",
  "provider": "ashby",
  "board": "vanta",
  "source_url": "https://api.ashbyhq.com/posting-api/job-board/vanta?includeCompensation=true",
  "fetched_at": "2026-09-12T12:57:29+00:00",
  "input": "ashby:vanta"
}
```

A row from a careers page rather than a board token carries the employer's own site:

```json
{
  "title": "Senior / Staff Fullstack Engineer",
  "company": "Linear",
  "company_domain": "linear.app",
  "location": "Europe",
  "remote": true,
  "provider": "ashby",
  "board": "Linear",
  "input": "https://linear.app/careers"
}
```

### Pricing

| Event | Price |
|---|---|
| Job delivered (one dataset row) | $0.006, which is $6.00 per 1,000 jobs |
| Actor start | $0.00005 per GB of run memory, charged once per run |

Worked example: 50 employers with about 40 open roles each is 2,000 jobs, so 2,000 x $0.006 =
$12.00 plus one start event. Users on paid Apify plans pay less per job.

You pay only these event prices; Apify compute is not billed to you separately. Jobs removed by a
filter are not charged. Boards that could not be read, boards with no open jobs and careers pages
with no feed behind them produce no rows and cost nothing. If you set a maximum cost per run, no
row past that cost is ever delivered or charged for: the run stops there, the log says how many
jobs it had found but did not hand over, and the run's status message says the same.

### FAQ

#### Where does the data come from?

From the feed the employer's own careers page uses. Greenhouse, Lever, Ashby, Workable, Recruitee
and Personio each publish one for every board, served to anyone with no key and no login -
Greenhouse documents that no authentication is required. `source_url` on every row is the exact
address it was read from, so you can check any row yourself.

#### Why are there no job boards in the list?

Because the big boards block automated requests, and this actor does not work around a block. A
site that answers with a challenge or a 403 is backed off from and is never retried through
another address. Employers' own feeds are published to be read, which is why they are the whole
product here.

#### My careers page was not recognised. Now what?

Open the page and click through to where the jobs are actually listed, then paste that URL. Pages
that build their job list in the browser with JavaScript have nothing for a plain fetch to find,
and some company sites refuse datacenter connections outright. The board URL always works, and so
does the `provider:board` pair.

#### Do I get every job on the board?

Yes, up to `maxJobsPerBoard` and after your filters. There is no paging to run out of: one board
is one feed, read in one request, and the run log says how many open roles that board actually has
beside how many it kept for you.

#### Why is the pay range empty?

Because that employer did not publish one. Greenhouse's board feed carries no pay range at all,
so every Greenhouse row is null there; on Lever and Ashby about half the jobs carry one. Nothing
is estimated or inferred - an empty range means the employer stated none.

#### Is there any contact information in the rows?

No. There is no recruiter, hiring manager or contact name in any field, and personal contact
points are removed from descriptions before a row is written. The apply link is the employer's
own application page.

#### How do I find new jobs since my last run?

Run on a schedule and compare `job_id` within a board, which is stable while a posting is open.
`postedWithinDays` narrows a run to recent postings, but only where the feed publishes a date.

### Limits

- One request per board. `maxJobsPerBoard` caps the rows, not the fetch: every one of these feeds
  publishes the whole board as a single document, so it is read in full and then cut. No provider
  is asked to page - Lever accepts a page size and then caps it at 100 whatever you ask for, which
  would silently cut a 311-role board down to 100.
- 200 boards per run, 10,000 jobs per board, 60 seconds per board by default (10 to 180).
- Detection reads one page, and the `/careers` path when you pass a bare domain. A JavaScript-built
  careers page, or one that refuses datacenter connections, comes back as no feed found.
- Feeds differ. Employment type is missing on Greenhouse, `updated_at` on everything but
  Greenhouse and Recruitee, and some Personio boards publish no description at all.
- Board tokens can carry capitals (`ashby:Linear`), and the token is used as given.
- Closed postings are not in the feed, so a run shows what is open at that moment. Nothing is
  cached between runs.
- Datacenter connections only. A provider that answers with a challenge or a 403 is not retried
  from another address; that board is reported in the log and costs nothing.

### Compliance

Job postings only, from feeds employers publish for their own careers pages. No field holds a
person: there is no recruiter, hiring manager or contact name, and personal contact points are
removed from descriptions. You are responsible for using the data lawfully.

Questions, bug reports and feature requests go on this actor's Issues tab. Business owners can send data-removal requests to info@locomint.io; that address is for removal requests only. This actor keeps no copy of what it reads between runs, so each run returns what the public feeds show at that moment.

### Other Locomint actors

- [Google Maps Scraper & Email Extractor – Business Leads](https://apify.com/locomint/google-maps-scraper): Search terms and a city in, business records with website contacts out.
- [Google Maps Scraper – Multi-City Lead Lists with Emails](https://apify.com/locomint/google-maps-multi-city-scraper): Many categories across many cities in one deduplicated run.
- [Google Maps Place Details Scraper – Bulk Place ID Lookup](https://apify.com/locomint/google-maps-place-details-scraper): Place IDs or place-page links in, full records out.
- [Website Email Scraper – Contact Details, Socials & WhatsApp](https://apify.com/locomint/website-content-contact-extractor): Contact points from website URLs you supply.
- [Website Content Crawler – Markdown for AI, Emails & Contacts](https://apify.com/locomint/website-crawler-content-contacts): A whole site as Markdown, with its contact points.
- [Bulk Email Verifier & Validator](https://apify.com/locomint/bulk-email-verifier): Checks whether addresses can receive mail.
- [Company Enrichment API – Domain to Emails, Socials & Tech](https://apify.com/locomint/company-enrichment): A domain in, its contacts and technologies out.
- [AI Crawler Checker – robots.txt Rules for GPTBot & ClaudeBot](https://apify.com/locomint/ai-crawler-checker): Which AI crawlers a site's robots.txt allows.
- [Schema Markup Validator & Generator – JSON-LD Checker](https://apify.com/locomint/schema-markup-validator): Checks and generates schema.org markup.

# Actor input Schema

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

One per line, in whichever form you have. A company careers page (https://www.example.com/careers), a job board URL (https://job-boards.greenhouse.io/duolingo, https://jobs.lever.co/matchgroup, https://jobs.ashbyhq.com/linear, https://apply.workable.com/blueground), or a pair such as greenhouse:duolingo. Duplicates are removed. Up to 200 boards per run.

## `descriptions` (type: `boolean`):

Keep the full description the employer publishes for each job. Turn it off for a much smaller dataset when you only need titles, locations and apply links - the price per job is the same either way. On Greenhouse the department travels with the description, so turning this off leaves that column empty unless you are filtering on it.

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

Upper limit of jobs to deliver from each board. You are charged per job delivered.

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

How many feeds to read at the same time.

## `boardTimeout` (type: `integer`):

A feed that has not answered by then is reported in the log and left out. It produces no rows, so it costs nothing.

## `titleIncludes` (type: `array`):

Keep only jobs whose title contains one of these words, e.g. engineer, sales, nurse. Free - filtered jobs are not charged.

## `titleExcludes` (type: `array`):

Drop jobs whose title contains one of these words, e.g. intern, contract.

## `locationIncludes` (type: `array`):

Keep only jobs whose location matches one of these, e.g. London, Germany, New York. Matched against every location the job lists.

## `departmentIncludes` (type: `array`):

Keep only jobs in a department matching one of these, e.g. engineering, marketing. Jobs whose feed names no department are left out by this filter.

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

Keep only jobs the employer marks as remote, or whose location says remote.

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

Keep only jobs posted in this many days. 0 means any date. Jobs whose feed publishes no posting date are left out by this filter, because there is no date to judge.

## Actor input object example

```json
{
  "boards": [
    "greenhouse:doximity",
    "lever:matchgroup",
    "ashby:posthog",
    "workable:blueground"
  ],
  "descriptions": true,
  "maxJobsPerBoard": 5,
  "concurrency": 4,
  "boardTimeout": 60,
  "remoteOnly": false,
  "postedWithinDays": 0
}
```

# Actor output Schema

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

One row per open job: title, company, department, location, remote flag, employment type, description, apply link, posting date and the pay range where the employer publishes one.

# 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": [
        "greenhouse:doximity",
        "lever:matchgroup",
        "ashby:posthog",
        "workable:blueground"
    ],
    "maxJobsPerBoard": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("locomint/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 = {
    "boards": [
        "greenhouse:doximity",
        "lever:matchgroup",
        "ashby:posthog",
        "workable:blueground",
    ],
    "maxJobsPerBoard": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("locomint/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 '{
  "boards": [
    "greenhouse:doximity",
    "lever:matchgroup",
    "ashby:posthog",
    "workable:blueground"
  ],
  "maxJobsPerBoard": 5
}' |
apify call locomint/career-site-jobs-scraper --silent --output-dataset

```

## MCP server setup

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