# Job Board Scraper — Greenhouse, Lever & Ashby (`danielmachad/ats-hiring-signals`) Actor

Scrape open roles from any company's Greenhouse, Lever or Ashby job board, and see what changed: roles opened, roles closed, and how long each has been open. Job postings only, no candidate data.

- **URL**: https://apify.com/danielmachad/ats-hiring-signals.md
- **Developed by:** [Daniel](https://apify.com/danielmachad) (community)
- **Categories:** Lead generation, Developer tools, Jobs
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.50 / 1,000 role-scrapeds

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/platform/actors/running/actors-in-store#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

## Job Board Scraper — Greenhouse, Lever & Ashby

Track open roles across any company's Greenhouse, Lever or Ashby board, and see what actually changed: which roles opened, which closed, and how long each has been sitting there.

### Why this instead of a job list

A list of open roles is a commodity — the boards publish it themselves. What no single fetch can tell you is **movement**, and movement is the signal:

- A company that opened 14 engineering roles this month is scaling, and is buying tools.
- A role that has been open for 120 days is one they cannot fill.
- A role that disappeared after 30 days was probably filled. One that disappeared after 4 was probably cancelled.

Getting that requires remembering the previous run, comparing against it, and reporting closures as first-class rows rather than silently dropping them. That is what this Actor does.

### What you get

**Per role** — company, ATS, title, department, team, location, country, employment type, workplace type, remote flag, publication date, direct link and apply link. Description text is optional.

**How long it has been open** — `daysOpen`, computed from the board's own publication date. The single most useful field here, and the one a plain job list never carries.

**What changed since your last run** — every row carries a `status`:

| Status | Meaning |
|---|---|
| `open` | listed last run and still listed |
| `new` | appeared since the last run |
| `closed` | was listed last run, gone now — with `daysOpenAtClose` |

**A hiring summary per company** — open roles, opened and closed since last run, net change, median days open, top departments and remote share. Written to the key-value store under `HIRING_SUMMARY`.

### Sample output

```json
{
  "jobId": "8077887",
  "company": "Stripe",
  "ats": "greenhouse",
  "title": "Account Executive, Bridge",
  "department": "Sales",
  "location": "SF, NYC, SEA, CHI",
  "isRemote": false,
  "status": "new",
  "publishedAt": "2026-07-22T17:15:53.000Z",
  "daysOpen": 30,
  "daysSincePreviousRun": 7,
  "url": "https://stripe.com/jobs/search?gh_jid=8077887"
}
```

### Who uses this

- **Sales and go-to-market teams** — hiring is a buying signal. A company staffing up a function is budgeting for it.
- **Recruiters and talent teams** — watch competitor pipelines, and see which roles stay open longest in your market.
- **Investors and analysts** — headcount demand as a growth proxy, tracked weekly without a data vendor.
- **Job aggregators** — a clean, normalised feed across three ATS platforms in one call.

### Input

| Field | Default | Notes |
|---|---|---|
| `companies` | 3 examples | `"ats:token"` entries, e.g. `greenhouse:stripe`. A bare token means Greenhouse. |
| `includeDescription` | `false` | Full text per role. Multiplies payload ~12x. |
| `maxJobsPerCompany` | `100` | Caps results and cost per board. Raise it for a company's full pipeline — large employers list well over a thousand roles. |
| `trackChanges` | `true` | Stores a snapshot so the next run reports what moved. |
| `reportClosedRoles` | `true` | Emit closed roles as rows. They are billed like any other row. |

**Finding a company's token:** it is the last part of its careers URL — `boards.greenhouse.io/`**`stripe`**, `jobs.lever.co/`**`palantir`**, `jobs.ashbyhq.com/`**`ramp`**.

The defaults run as-is and return about 300 live roles across three well-known boards, so you can see the shape of the output before deciding what to track.

### Pricing

Pay per event, **$1.50 per 1,000 roles**.

- A default run — three boards, 100 roles each — costs **$0.45**.
- Tracking those same three companies weekly costs about **$1.95 a month**.
- Pulling a large employer's full pipeline (raise `maxJobsPerCompany`) costs proportionally more: Stripe alone lists over 500 open roles.

Days-open, change tracking and the hiring summary are included. You pay per role and for nothing else.

### Change tracking

Snapshots are keyed by the **exact set of companies** you asked for. Add a company and the comparison restarts, because every one of its roles would otherwise look like this week's hiring surge.

Run it weekly against a fixed watchlist and you build a hiring history for those companies instead of a series of disconnected exports.

### Limits, stated plainly

- **Greenhouse omits departments unless descriptions are included.** Its lightweight endpoint returns location but not department; set `includeDescription` if you need it, and accept the larger payload.
- **A closed role is not proof of a hire.** It means the posting is gone. It may have been filled, cancelled, or merged into another requisition. Treat it as a signal, not a record.
- **A board returning zero roles is treated as unverified.** A mistyped token and a company with nothing open look identical from the outside, so no closures are inferred for it and it is listed in `companiesFailed`. This is deliberate: reporting an entire pipeline as closed because of a typo would be worse than reporting nothing.
- **Three platforms, not all of them.** Greenhouse, Lever and Ashby are supported because all three were verified against live boards. Workable, Recruitee and SmartRecruiters expose similar endpoints and are not included until they are tested the same way.
- **Only public boards.** If a company does not publish its roles openly, it does not appear here.

### Data and privacy

This Actor returns **job postings, which are company data**. No candidate, applicant or recruiter personal information is fetched, derived or stored.

It reads the public job board APIs the platforms provide for exactly this purpose — the same endpoints that power each company's own careers page. No key, no login, no bypassed restriction.

### FAQ

**How many companies can I track at once?**
As many as you like. They are fetched concurrently and compared as one set.

**Why is `status` never `new` on my first run?**
There is no previous run to compare against, so calling everything new would overstate hiring. Change fields fill in from the second run onwards.

**Can I run it on a schedule?**
Yes, and it is built for it. Weekly against a fixed company list gives the cleanest signal.

**What happens if one company's board breaks?**
That company is reported in `companiesFailed` and excluded from the run. The other companies are unaffected, and none of the failed company's roles are marked closed.

# Actor input Schema

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

One entry per company, as "ats:token" — for example "greenhouse:stripe", "lever:palantir", "ashby:ramp". The token is the company name in its careers URL (boards.greenhouse.io/TOKEN, jobs.lever.co/TOKEN, jobs.ashbyhq.com/TOKEN). A bare token with no prefix is treated as Greenhouse. Add as many companies as you want to track together; they are compared as one set from run to run.

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

Fetch the full description text for every role. Useful for keyword analysis, but it multiplies the payload roughly twelvefold and slows the run. Leave off unless you need the text.

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

Upper bound per board, and therefore on cost — billing is per role returned. Large employers list well over a thousand roles, so raise this when you want a company's full pipeline rather than a sample.

## `trackChanges` (type: `boolean`):

Stores a snapshot of this exact set of boards so a later run can report which roles opened and which closed. Every change field is empty on the first run — there is no history to compare against. Changing the company list starts a new comparison, because the question being asked has changed.

## `reportClosedRoles` (type: `boolean`):

Emit a row for each role that was present last run and is gone now, with how long it stayed open. These rows are billed like any other. Turn off if you only want the currently open pipeline.

## `proxyConfiguration` (type: `object`):

Optional. These are public job board APIs that do not block, so the automatic setting is fine and residential proxy is deliberately ignored.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:stripe",
    "lever:palantir",
    "ashby:ramp"
  ],
  "includeDescription": false,
  "maxJobsPerCompany": 100,
  "trackChanges": true,
  "reportClosedRoles": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# 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 = {
    "companies": [
        "greenhouse:stripe",
        "lever:palantir",
        "ashby:ramp"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("danielmachad/ats-hiring-signals").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 = { "companies": [
        "greenhouse:stripe",
        "lever:palantir",
        "ashby:ramp",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("danielmachad/ats-hiring-signals").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 '{
  "companies": [
    "greenhouse:stripe",
    "lever:palantir",
    "ashby:ramp"
  ]
}' |
apify call danielmachad/ats-hiring-signals --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,danielmachad/ats-hiring-signals"
        }
    }
}

```

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/xLp6KcMMsUWto7VSC/builds/fpxb5GbZUoUI8WVrl/openapi.json
