# Company Hiring Signals - Greenhouse, Lever, Ashby (`lukehunter/company-hiring-signals`) Actor

Which of your target accounts are hiring? One row per company from public Greenhouse, Lever and Ashby boards: open roles, departments, locations and new roles since last run, for $0.01/company.

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

## Pricing

$10.00 / 1,000 company hiring snapshots

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

## Company Hiring Signals - Greenhouse, Lever, Ashby

**For sales teams, recruiters selling to employers, RevOps, investors and market researchers who want to know which target accounts are hiring.** Paste company domains and get **one row per company**: how many roles are open, in which **departments**, in which **locations**, how many are remote, and **what is new or removed since your last run**. Built on the public job-board APIs of **Greenhouse, Lever and Ashby**.

**$0.01 per company whose board was found and read. 100 companies = $1. Companies with no board, invalid input and failed requests are free.**

Most hiring scrapers return one row per *job*, so you have to aggregate thousands of rows yourself to learn that "Acme opened six engineering roles this week". This Actor does that aggregation and the week-over-week comparison for you. A company that suddenly opens a run of sales or engineering roles is a buying signal for recruiting agencies, dev tools, HR software, security, relocation and payroll vendors.

No login, no proxy, no API key, no scraping of rendered pages: only the documented public JSON endpoints that companies' own career pages use. It never outputs recruiter names, emails or any other personal data: titles, departments, locations and counts only.

### Quick start

```json
{
  "companies": ["stripe.com", "ashby:openai", "https://jobs.lever.co/palantir"],
  "maxCompanies": 3
}
```

### What goes in `companies`

| You give | Example | What happens |
|---|---|---|
| A company domain | `stripe.com`, `https://www.stripe.com/jobs` | The Actor guesses the board slug from the domain (`stripe`, and the name without hyphens, e.g. `my-co` then `myco`) and tries Greenhouse, then Lever, then Ashby for each guess. The first board that exists is used and reported in `ats` and `boardUrl`. If none exists the row is `not_found` (free). |
| A board URL | `https://boards.greenhouse.io/stripe`, `https://jobs.lever.co/palantir`, `https://jobs.ashbyhq.com/openai` | Used directly, no guessing. |
| `ats:slug` | `greenhouse:stripe`, `lever:palantir`, `ashby:openai` | Used directly, no guessing. |

Boards on the EU hosts (`boards.eu.greenhouse.io`, `jobs.eu.lever.co`) are read from the matching EU API when you pass their URL. Domain discovery is a heuristic: a company whose board slug is not derived from its domain (or that uses another ATS such as Workday) comes back as `not_found`. Pass the board URL or `ats:slug` for those. Check `boardUrl` on a discovered row if the slug could belong to a different company. Duplicates, and different inputs that resolve to the same board, are reported once per run.

### Recipes

**Which of my target accounts are hiring engineers?**

```json
{
  "companies": ["stripe.com", "airbnb.com", "databricks.com"],
  "departmentsFilter": ["engineering"],
  "trackChanges": false
}
```

`openRoles`, `departments`, `locations` and `remoteRoles` then describe engineering roles only.

**Weekly "what's new" for an account list**

```json
{
  "companies": ["stripe.com", "airbnb.com", "databricks.com", "ashby:openai"],
  "trackChanges": true,
  "stateStoreName": "my-target-accounts"
}
```

Save it as a Task and add a weekly Schedule. The first run is the baseline: `baseline: true` and `newRolesSinceLastRun: 0` for every company. Later runs report `newRolesSinceLastRun`, `removedRolesSinceLastRun` and up to five `sampleNewTitles`. Sort by `newRolesSinceLastRun` to see who is ramping up. Each company (and each filter set) keeps its own history in the named key-value store, so different account lists can use different `stateStoreName` values.

**Only roles in a region**

```json
{
  "companies": ["greenhouse:stripe"],
  "locationsFilter": ["london", "dublin", "remote"]
}
```

### Input

| Field | Default | What it does |
|---|---:|---|
| `companies` | required | Domains, board URLs or `ats:slug` values. |
| `departmentsFilter` | `[]` | Count only roles whose department contains one of these words (case-insensitive). |
| `locationsFilter` | `[]` | Count only roles whose location contains one of these words (case-insensitive). |
| `trackChanges` | `true` | Remember role IDs between runs and report new and removed roles. |
| `stateStoreName` | `company-hiring-signals-state` | Named store holding that memory. Use a different name per account list. |
| `maxCompanies` | `100` | Hard cap on companies checked and charged, 1 to 5000. |

### Example output

One row per company:

```json
{
  "company": "acmeexample.com",
  "inputValue": "acmeexample.com",
  "ats": "greenhouse",
  "boardSlug": "acmeexample",
  "boardUrl": "https://boards.greenhouse.io/acmeexample",
  "boardCompanyName": "Acme Example, Inc.",
  "matchConfidence": "high",
  "openRoles": 42,
  "departments": { "Engineering": 18, "Sales": 9, "Marketing": 5, "Customer Success": 4, "Finance": 2 },
  "locations": { "San Francisco, CA": 15, "Remote - US": 11, "New York, NY": 8, "London, UK": 6 },
  "remoteRoles": 12,
  "newRolesSinceLastRun": 6,
  "removedRolesSinceLastRun": 2,
  "baseline": false,
  "newestPostedAt": "2026-10-12T14:03:00.000Z",
  "oldestPostedAt": "2026-06-02T09:10:00.000Z",
  "sampleNewTitles": ["Senior Backend Engineer", "Account Executive, Enterprise", "Data Scientist"],
  "warnings": [],
  "status": "ok",
  "error": null,
  "checkedAt": "2026-10-13T08:00:00.000Z"
}
```

A company with no board:

```json
{
  "company": "no-board-example.com",
  "inputValue": "no-board-example.com",
  "ats": null,
  "openRoles": null,
  "status": "not_found",
  "error": "No Greenhouse, Lever or Ashby board found for slug guesses: greenhouse:no-board-example, lever:no-board-example, ashby:no-board-example, greenhouse:noboardexample, lever:noboardexample, ashby:noboardexample. Pass the board URL or ats:slug instead.",
  "checkedAt": "2026-10-13T08:00:00.000Z"
}
```

#### Fields

| Field | Meaning |
|---|---|
| `company`, `inputValue` | The domain (or board slug) and the entry you gave. |
| `ats`, `boardSlug`, `boardUrl` | Which platform matched, its slug and the public board URL so you can verify it. |
| `boardCompanyName`, `matchConfidence` | Greenhouse boards show their company name. `matchConfidence` is `high` when your domain's name (or the slug) appears in it and `low` when it does not, which is your cue to open `boardUrl` and check that the guessed slug is the right company. Both are `null` for Lever and Ashby, which do not expose a name. |
| `openRoles` | Open roles after your filters. |
| `departments`, `locations` | Top 10 name to count, biggest first. Locations are the primary location as the board lists it. |
| `remoteRoles` | Roles the ATS marks remote, or whose location says "remote". Titles are not used, so "Remote Sensing Engineer" is not counted. Hybrid is not counted. |
| `newRolesSinceLastRun`, `removedRolesSinceLastRun`, `baseline`, `sampleNewTitles` | Change tracking. `null` when `trackChanges` is off. |
| `newestPostedAt`, `oldestPostedAt` | Range of posting dates when the board exposes them. Long-open roles can mean hard-to-fill positions. |
| `warnings` | Notes on an `ok` row, for example that the Greenhouse departments list was unavailable, or that change-tracking state could not be saved. Empty when clean. |
| `status`, `error` | `ok`, `not_found`, `invalid_input`, `fetch_failed`, `parse_error`, `duplicate`, `budget_reached` or `time_limit`. Only `ok` is charged. |
| `checkedAt` | When the board was read. |

### Pricing

**$0.01 per company** (`company` event), charged only when a board was found and read. `not_found`, `invalid_input`, `fetch_failed`, `parse_error`, `duplicate`, `budget_reached` and `time_limit` rows are free. A board that was found but has zero matching open roles (or none after your filters) is still a charged `ok` row with `openRoles: 0`: a company that is not hiring is a signal too. A company is delivered and charged at most once per run. The Actor honours your run's maximum cost: when the limit is reached it stops, marks the unchecked companies `budget_reached`, and never bills past it.

### Limitations

- Greenhouse, Lever and Ashby only. Workday, SmartRecruiters, Workable, BambooHR and others are not covered here; [Greenhouse Jobs Scraper & Job Alerts](https://apify.com/lukehunter/greenhouse-job-alerts) covers more platforms, with one row per job.
- Domain discovery guesses board slugs; it cannot find a board whose slug differs from the domain name.
- Greenhouse department names come from the board's public departments list. If that call fails after retries, the row is a free `fetch_failed` whenever you use `departmentsFilter` or change tracking (so you are never billed for incomplete data); otherwise it is delivered with empty `departments` and a `warnings` note.
- Domain discovery never skips over a guess it could not check: if an earlier candidate (say Lever) failed with a server error while a later one (Ashby) exists, the row is a free `fetch_failed` rather than the lower-priority board.
- Counts are what the public board shows right now. Unlisted or internal roles are not visible and are not counted.
- At most 5,000 companies per run, 5 companies in parallel, 15 s per request, 3 attempts on 429 and 5xx errors with backoff, 30 MB per response, 40 minutes per run, enforced before every request (companies not reached are free `time_limit` rows you can re-run).
- Change tracking compares to the previous run that used the same `stateStoreName`, board and filters.

### Pair it with

- **[Greenhouse Jobs Scraper & Job Alerts](https://apify.com/lukehunter/greenhouse-job-alerts)**: the job-level view. When a company shows a spike here, pull the individual postings (titles, locations, salary where listed) with a new-jobs-only alert mode.
- **[Website Contact Scraper](https://apify.com/lukehunter/company-contact-scraper)**: once you know which accounts are hiring, collect public business contact details for the same domains.

### Use it from your code, Make, Zapier or n8n

Built for automated pipelines: small runs, stable field names, one row per item, and you pay only for rows actually delivered.

**One API call that runs the Actor and returns the data** (replace `$APIFY_TOKEN` with your token from Apify Console → Settings → Integrations):

```bash
curl -s -X POST "https://api.apify.com/v2/acts/lukehunter~company-hiring-signals/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"companies": ["stripe.com", "ashby:openai", "https://jobs.lever.co/palantir"], "maxCompanies": 3}'
```

**JavaScript** (`npm i apify-client`):

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('lukehunter~company-hiring-signals').call({
  "companies": [
    "stripe.com",
    "ashby:openai",
    "https://jobs.lever.co/palantir"
  ],
  "maxCompanies": 3
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python** (`pip install apify-client`):

```python
import os
from apify_client import ApifyClient
client = ApifyClient(os.environ['APIFY_TOKEN'])
run = client.actor('lukehunter~company-hiring-signals').call(run_input={"companies": ["stripe.com", "ashby:openai", "https://jobs.lever.co/palantir"], "maxCompanies": 3})
items = client.dataset(run['defaultDatasetId']).list_items().items
```

**Schedules, webhooks and no-code tools:** save your input as a Task, add a Schedule (e.g. hourly or daily), and attach a webhook or one of the Integrations tab's connectors (Make, Zapier, n8n, Google Sheets, Slack) to push each run's results into your own system.

**Feedback:** an honest rating on the Store page, good or bad, helps others decide if this Actor fits their needs. Missing a field or found a problem? Open an issue on the Actor page.

### FAQ

**Is this allowed?** It reads the same public, unauthenticated job-board JSON endpoints that companies publish for their own career pages, with a descriptive User-Agent, no login and no evasion.

**Does it collect personal data?** No. Rows contain company-level counts and job titles only. Recruiter and hiring-manager fields, where an ATS exposes them, are never read.

**Why is my company `not_found`?** It is probably on another ATS, or its board slug differs from its domain name. Find its careers page, then pass the board URL or `ats:slug`.

**Why is `newRolesSinceLastRun` 0 on every row?** That is the first run: it records the baseline. Run it again on a schedule.

**Can I use a company's domain list from another Actor?** Yes: pass the domains as `companies`, for example from a Google Maps or contact-scraper run.

### Related Actors

Other data tools from the same developer, built to the same standard: official or public sources, hard cost caps, and honest documentation of limits.

- **[Website Contact Scraper](https://apify.com/lukehunter/company-contact-scraper)**: emails, phone numbers and social profile links from any list of company domains, with the page each came from and a changed-contacts-only mode for scheduled runs.
- **[Domain Email Provider Lookup](https://apify.com/lukehunter/email-provider-mx-lookup)**: the email provider (Google Workspace, Microsoft 365, Zoho and more), MX, SPF and DMARC setup of any domain list, with provider-change alerts.
- **[Bulk WHOIS & Domain Age Lookup (RDAP)](https://apify.com/lukehunter/domain-whois-rdap-lookup)**: domain age, expiry date, registrar, nameservers and availability for any domain list over the official RDAP protocol, without personal contact data.
- **[French Company Leads](https://apify.com/lukehunter/french-companies-sirene)**: French companies (entreprises) from the official Sirene-based API, filtered by NAF code, department, headcount and creation date, with SIREN/SIRET, officers and new-company alerts.
- **[Poland KRS Company Lookup](https://apify.com/lukehunter/poland-krs-company-lookup)**: Polish companies from the official KRS API by KRS number: NIP, REGON, address, share capital, PKD and the board as published, with optional VAT status.
- **[EU VAT Number Validator](https://apify.com/lukehunter/eu-vat-number-validator)**: bulk EU VAT number checks against the official VIES service: valid or invalid, registered name and address, with optional consultation numbers.
- **[Phone Number Validator](https://apify.com/lukehunter/phone-number-validator)**: bulk phone number validation and E.164 formatting, offline: valid or invalid, country, and mobile or landline type for every number in a list or scraped contact dataset.
- **[Impressum Scraper](https://apify.com/lukehunter/impressum-scraper)**: legal entity data from the imprint of any company website in Germany, Austria and Switzerland: legal name and form, managing directors, HRB/HRA or FN register, VAT id and address, with a changed-data-only mode for scheduled runs.

# Actor input Schema

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

Target accounts. A company domain such as stripe.com (the Actor guesses its Greenhouse, Lever or Ashby board), a board URL such as https://jobs.lever.co/palantir, or ats:slug such as greenhouse:stripe. Duplicates and entries pointing at the same board are reported once.

## `departmentsFilter` (type: `array`):

Count only roles whose department contains one of these words, case-insensitive, e.g. \["engineering", "sales"]. Leave empty to count every department. Each filter set keeps its own change history.

## `locationsFilter` (type: `array`):

Count only roles whose location contains one of these words, case-insensitive, e.g. \["london", "remote"]. Leave empty to count every location. Each filter set keeps its own change history.

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

When true, remember each company's role IDs between runs and report newRolesSinceLastRun, removedRolesSinceLastRun and sampleNewTitles, e.g. on a weekly Schedule. The first run is the baseline (new = 0, baseline = true). Set false for a one-off snapshot.

## `stateStoreName` (type: `string`):

Named key-value store that remembers each company's role IDs between runs, used only when change tracking is on. Example: my-target-accounts. Use a different name to keep separate account lists. Letters, numbers and hyphens only.

## `maxCompanies` (type: `integer`):

Hard cap on companies checked and charged, 1 to 5000, e.g. 100. At $0.01 per company with a board, 100 companies cost at most $1.00. Entries beyond the cap are not checked. Not-found and failed boards are never charged.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "ashby:openai",
    "https://jobs.lever.co/palantir"
  ],
  "trackChanges": true,
  "stateStoreName": "company-hiring-signals-state",
  "maxCompanies": 3
}
```

# Actor output Schema

## `companies` (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 = {
    "companies": [
        "stripe.com",
        "ashby:openai",
        "https://jobs.lever.co/palantir"
    ],
    "maxCompanies": 3
};

// Run the Actor and wait for it to finish
const run = await client.actor("lukehunter/company-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": [
        "stripe.com",
        "ashby:openai",
        "https://jobs.lever.co/palantir",
    ],
    "maxCompanies": 3,
}

# Run the Actor and wait for it to finish
run = client.actor("lukehunter/company-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": [
    "stripe.com",
    "ashby:openai",
    "https://jobs.lever.co/palantir"
  ],
  "maxCompanies": 3
}' |
apify call lukehunter/company-hiring-signals --silent --output-dataset

```

## MCP server setup

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