# ATS Jobs Scraper for Greenhouse, Lever, Ashby, SmartRecruiters (`gubidonius/company-jobs-scraper`) Actor

Open roles straight from company career boards on Greenhouse, Lever, Ashby and SmartRecruiters, in one format. Start from a ready made company list, or name your own. Change detection returns only what opened and closed since your last run. No login, and you pay only for adverts you get.

- **URL**: https://apify.com/gubidonius/company-jobs-scraper.md
- **Developed by:** [Gregory Bolshakov](https://apify.com/gubidonius) (community)
- **Categories:** Jobs, MCP servers, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

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

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

## What's an Apify Actor?

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

## Company Jobs Scraper

Give it a list of companies. Get back their open roles in one clean table, whichever
hiring system each company happens to use.

### The problem it solves

Companies post jobs through an applicant tracking system, and there are dozens of them.
Greenhouse, Lever, Ashby and SmartRecruiters each hand back a completely different shape
for the same advert. One calls the role `title`, another calls it `text`, another calls it
`name`. Locations, departments and dates all differ too.

This Actor reads all of them and gives you one format.

### You do not need to know which system a company uses

Paste the career page link if you have it. If you only know the company, write the name on
its own and the Actor checks each system until it finds live adverts.

```
https://boards.greenhouse.io/stripe     a link works
lever:leverdemo                         force one system if you already know
ramp                                    just the name, it will find it
```

### You do not need a list of companies either

Pick a preset and press run. Four lists, 69 companies, and every one of them was fetched
live on 2026-08-29 and returned adverts.

```
ai-labs     15 companies    Anthropic, OpenAI, Cohere, ElevenLabs, Harvey ...
devtools    16              GitLab, Sentry, Vercel, Supabase, Datadog ...
fintech     19              Stripe, Ramp, Plaid, Wise, Monzo, Coinbase ...
top-tech    19              Figma, Notion, Databricks, Canva, Reddit ...
```

Mix a preset with your own companies. Duplicates are merged, so nothing is fetched or
billed twice. If a company moves system or shuts its board, it shows up in the run summary
as not found and the rest of the list still runs.

### Watching for changes

Turn on **Only new and closed since the last run** and each run returns only what moved.
New adverts arrive with `isNew: true`, and adverts that disappeared arrive with
`isClosed: true` and the title they had.

Two rules it follows, because getting these wrong is worse than returning nothing:

- The first run on a board is a baseline. Nothing is new the first time you look, so
  `isNew` is empty rather than true for every row.
- An advert is only closed if the board actually answered. If a board errors or the run
  stops early, the previous list is kept and no closures are reported for it. A false
  closure ends up in your history table and there is no way to tell later that it was our
  fault rather than the company's.
- Change detection reads the whole board and ignores the per company cap. Closure means
  "was here last run, is not here now", so a capped read would report everything past the
  cap as closed the first time you changed the cap.

### Input

| Field | What it does |
|---|---|
| Preset company lists | Ready made lists, so a first run needs nothing typed |
| Companies | Board links, or names on their own, or system:name |
| Only new and closed since the last run | Change detection, see above |
| Include the full advert text | Adds the whole description as plain text. Off by default because it makes the output much bigger |
| Remote roles only | Keeps only adverts the board marks remote |
| Max adverts per company | Defaults to 100 so a first run over a preset stays cheap. Raise it to take whole boards |

### Output

One row per advert: role title, company, which system it came from, location, remote flag,
department, employment type, pay, posting date and a direct apply link. Add the description
if you asked for it.

A SUMMARY record shows what happened to each company you asked for, including anything the
Actor could not read and why.

#### About the pay fields

`salaryMin`, `salaryMax`, `salaryCurrency` and `salaryInterval` carry the pay the company
published as numbers. `salaryRaw` keeps the board's own summary string, which often also
mentions equity or a bonus that the numeric fields leave out.

Three boards of the four publish pay, and on all three it is a setting each company
chooses rather than something the platform does. Coverage swings from 96% to 0% between
two boards on the same system, so sample before you chart it.

- Ashby sends it with the board. Ramp fills it on 133 of 138 adverts, Linear on none.
- Greenhouse sends it only if you ask for `pay_transparency`. Across 2,448 adverts it is
  on 47% of them. Anthropic 89%, Stripe 0%.
- SmartRecruiters keeps it on the per advert endpoint, so the Actor makes one extra
  request per row on that board. Wise declares it on 38 of 40 sampled adverts, Bosch on
  none. It is also the only board that states the period as a field instead of free text.
- Lever has `salaryRange`, and almost nobody fills it. 12 of 2,527 postings across eight
  boards, and 8 of those were Lever's own demo board.

An empty salary means the company did not publish one, never that it was missed.

##### One advert can declare several pay bands

On Greenhouse the pay field is a list, and 20% of the adverts that declare pay put more
than one band in it. A company that pays differently by state sends one band per state. A
company hiring at three levels at once sends one band per level. Reading only the first
band gives you one state's pay as though it were the whole answer, and on 111 of the
adverts we measured the bands that got dropped were the higher ones.

So `salaryMin` is the bottom of the lowest band and `salaryMax` is the top of the highest.
`salaryBands` holds the bands themselves, with the label the company wrote on each one, and
it is null when there is only one band.

Base pay and total on-target earnings are not added together. Sales adverts often carry
both, and mixing them gives you a base salary that ends at the commission number. The two
bounds come from base pay when both are there, and each band says which it is in `basis`.

##### The period can be wrong

`salaryInterval` is typed by the company on three of the four boards, and companies get it
wrong in both directions. One live advert says `Estimated Hourly Pay Range` over 200,000 to
260,000. Another says `Annual Base Salary` over 35 to 58. A Lever advert labels a 22.4 to 26
hourly rate `bi-week-salary`.

The Actor keeps the label only when the numbers could belong to that period, and leaves
`salaryInterval` null when they could not. It never rescales the amount to fit the label.
A null interval means the period was not stated or was not believable. The bounds are the
company's own numbers either way.

Currencies that are not dollar-scale skip this check, because 6,000,000 yen a year is an
ordinary salary and the check would throw away a correct label.

A pay band the company switched on and left empty is treated as absent. Lever sends
`{min: 0, max: 0}` for it and Greenhouse sends 1 to 1. That is a company with the field
turned on, not a job that pays nothing.

`salarySource` is `declared` when the company published the number itself. Nothing here is
guessed from advert text, so there is no second value today. The field exists so that if
text extraction is ever added, you can tell the two apart in the same column instead of
finding out later that you cannot.

### How it behaves when things go wrong

A name that matches nothing is reported and skipped, and you are not charged for it. One
bad entry never stops the rest of the list. On very long runs the Actor watches its own
clock, stops before the platform cuts it off, saves everything gathered so far and tells
you which companies it did not reach.

The start fee applies only once a board actually returns adverts, so a run that finds
nothing costs you nothing.

### Where the data comes from

Public job board endpoints that each company publishes so their own careers page can
display the roles. Nothing is logged into and no account is needed.

### Pricing

Pay per event, so you are charged for adverts you actually get. A run that finds nothing
costs you nothing, because the start charge only happens once a board returns something.

The live price is on the Actor page. It changes on 13 September 2026: the per advert rate
goes up and the per run charge comes down, which is the same shape the rest of this category
uses and is much kinder to anyone running change detection on a schedule.

# Actor input Schema

## `presetLists` (type: `array`):

Ready made company lists, so a first run needs nothing typed. Every company in them was fetched live on 2026-08-29 and returned adverts. Combine with your own companies, duplicates are merged and billed once.

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

One entry per company. Paste the career board link, or write the board name on its own and we will work out which system it uses. You can also force one with greenhouse:name, lever:name, ashby:name or smartrecruiters:name.

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

Adds the whole job description as plain text. Makes the output much larger, so leave it off if you only need titles and links.

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

Keeps only adverts the board marks as remote.

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

Useful when a large company posts hundreds of roles and you only want a sample. Default 100, which keeps a first run over a preset list cheap. Raise it to take whole boards.

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

Returns only adverts that appeared since your last run, plus adverts that disappeared, marked isClosed. The first run on a board records a baseline and returns everything with isNew empty, because nothing is new the first time you look. A board that fails to answer is carried forward untouched, so a broken fetch never reports adverts as closed.

## Actor input object example

```json
{
  "presetLists": [
    "ai-labs"
  ],
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "lever:leverdemo",
    "ramp"
  ],
  "includeDescription": false,
  "onlyRemote": false,
  "maxJobsPerCompany": 100,
  "onlyNewSinceLastRun": false
}
```

# Actor output Schema

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

One row per advert, with the role, company, location, posting date and a direct apply link.

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

Which company boards were read, which system each one uses, and anything that could not be read.

# 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 = {
    "presetLists": [
        "ai-labs"
    ],
    "companies": [
        "https://boards.greenhouse.io/stripe",
        "lever:leverdemo",
        "ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("gubidonius/company-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 = {
    "presetLists": ["ai-labs"],
    "companies": [
        "https://boards.greenhouse.io/stripe",
        "lever:leverdemo",
        "ramp",
    ],
}

# Run the Actor and wait for it to finish
run = client.actor("gubidonius/company-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 '{
  "presetLists": [
    "ai-labs"
  ],
  "companies": [
    "https://boards.greenhouse.io/stripe",
    "lever:leverdemo",
    "ramp"
  ]
}' |
apify call gubidonius/company-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,gubidonius/company-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/F3b63oZcGzzdi6Ke2/builds/0CpAo033563KajhqO/openapi.json
