# ATS Hiring Signals: Greenhouse, Lever & Ashby Jobs API (`dotcom/ats-hiring-signals`) Actor

Turn company domains or names into open jobs from the official public Greenhouse, Lever and Ashby job-board APIs. Normalized jobs (annual USD salary, seniority, function), a per-company hiring summary with a transparent hiring-intent score, and new/closed job tracking between runs.

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

## Pricing

from $1.00 / 1,000 job results

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

## ATS Hiring Signals: Greenhouse, Lever & Ashby Jobs API

Turn a list of **company domains or names** into **open jobs and hiring signals**. The Actor finds out whether each company uses **Greenhouse, Lever or Ashby**, then reads that ATS's **official public job-board API**. You get back:

- **Normalized jobs**: title, department, location, remote/hybrid/on-site, employment type, seniority, job function and published salary (also converted to annual USD).
- **A per-company summary**: open jobs by department, function and seniority, hiring velocity, salary transparency, and a **transparent 0–100 hiring-intent score**.
- **Change tracking**: jobs that are new or closed since your last run. Schedule it daily to monitor hiring.

There is no login, no scraping of job aggregators, no Workday and no LinkedIn. The Actor only reads the documented public endpoints that employers publish so others can build careers pages.

### Use cases

- **Sales prospecting (hiring intent):** find accounts that are hiring sales, data or security people right now. Rank them by `hiringIntentScore`.
- **Recruiting & talent intelligence:** track competitors' openings, salary bands and new roles every day.
- **Job boards & newsletters:** a clean, deduplicated feed of direct employer postings with apply links.
- **AI agents (MCP):** inputs are simple strings, every output field is described, and summaries are compact enough to fit in a prompt.

### Input

| Field | What it does |
|---|---|
| `companies` | Domains (`linear.app`), URLs, names (`Ramp`), board URLs (`https://jobs.lever.co/spotify`) or tokens (`greenhouse:airbnb`, `lever:palantir`, `ashby:ramp`). |
| `boards` | Optional known boards; these skip detection. |
| `titleKeywords`, `excludeTitleKeywords`, `locations`, `departments`, `workplace`, `seniority`, `postedWithinDays` | Filters. |
| `includeDescriptions` / `descriptionFormat` | Adds the full description as plain text or HTML. |
| `changeTracking`, `onlyNewJobs`, `emitChangeEvents`, `stateStoreName` | Monitoring between runs. |

Example:

```json
{
  "companies": ["linear.app", "Ramp", "greenhouse:discord"],
  "titleKeywords": ["engineer"],
  "workplace": "remote_or_hybrid",
  "changeTracking": true
}
```

### Output

Results go to three datasets (see the Output tab).

- **Jobs** (default dataset): one record per matching job. Fields include `company`, `ats`, `jobId`, `title`, `department`, `location(s)`, `workplaceType`, `employmentType`, `seniority`, `jobFunction`, `salaryMin/Max/Currency/Interval`, `salaryAnnualMinUSD/MaxUSD`, `salarySource`, `postedAt`, `url`, `isNew`, `firstSeenAt` and optionally `description`.
- **Companies**: one record per input. It has the detection result (`detectionMethod`, `detectionConfidence`, `detectionEvidence`) and these signals:
  - `boardOpenJobs`, `byDepartment`, `byFunction`, `bySeniority`, `postedLast7/30/90Days`
  - `salaryTransparencyRate`, `medianSalaryAnnualUSD`
  - `newSinceLastRun`, `closedSinceLastRun`
  - `hiringIntentScore`, plus its components and the exact formula
- **Changes**: `job_added` / `job_removed` events since the previous tracked run.

#### Hiring-intent score (0–100)

The score is computed on the jobs that match your filters:

- `volume = 35 × min(1, log10(1+openJobs)/log10(201))`
- `freshness = 30 × postedLast30Days/openJobs`
- `momentum = 25 × clamp(0.5 + 2.5 × (new − closed)/previousOpen, 0, 1)`. This is neutral (12.5) on the first run.
- `leadership = 10 × min(1, director/VP/C-level openings / 5)`

Every record includes the components, so you can re-weight them yourself.

#### How detection works

1. **Direct input**: a board URL or `ats:token` is used as given (confidence *high*).
2. **Website links**: the Actor reads the homepage and up to 4 careers pages (`robots.txt` respected) and looks for Greenhouse, Lever or Ashby board links or embeds (confidence *high*).
3. **Slug probing**: it tries likely tokens (from the domain or name) against the three APIs. Confidence is *medium* if the company name appears in the board data, *low* otherwise. Check low-confidence matches before relying on them. If plain tokens fail, common Greenhouse variants are tried too (`scaleai`, `doordashusa`, `andurilindustries`, `gleanwork`). A variant is only accepted when the company name is confirmed in the board data.

Detections are cached in the state store for 14 days.

### Pricing (pay per event)

| Event | Price |
|---|---|
| Company resolved (a board was found and read) | $0.002 |
| Job result (one job record) | $0.001 |
| Change event (job added/removed since last run) | $0.0005 |

Companies that aren't found, duplicates and summary records are free. Use **Max jobs per company**, **Output only new jobs** or the platform's *maximum cost per run* to cap spend. The Actor stops cleanly when the limit is reached and does not advance the change-tracking snapshot for companies it could not finish.

### Limitations

- Only **Greenhouse, Lever and Ashby** are supported. Companies on Workday, SmartRecruiters, iCIMS, etc. come back as `not_found`.
- **Name-only inputs** rely on slug probing, so give domains when you can.
- Salaries appear only when the employer publishes them. Description-parsed salaries are marked `description_text`.
- Seniority and job function are rule-based classifications of the title. They are not taken from the ATS.
- Greenhouse `first_published` can reset when a job is reposted. Lever provides no "updated" date.
- Very large Greenhouse boards (descriptions over about 20 MB, e.g. 2,000+ jobs) are fetched without descriptions. A note on the company record says so.
- Large boards produce many `job-result` events. Use `maxJobsPerCompany` or filters to cap the cost.
- Lever EU boards need the `lever:eu:<token>` form or a `jobs.eu.lever.co` URL; probing checks the global instance only.
- USD conversion uses European Central Bank reference rates of the day. If those rates can't be fetched, only USD salaries get USD values.

### Data sources & attribution

- Greenhouse Job Board API: https://developers.greenhouse.io/job-board.html. It states that "Job Board data is publicly available, so authentication is not required for any GET endpoints."
- Lever Postings API: https://github.com/lever/postings-api. Requests to `api.lever.co` are limited to 1 per second, following its robots.txt `Crawl-delay: 1`.
- Ashby Job Posting API: https://developers.ashbyhq.com/docs/public-job-posting-api
- Exchange rates: European Central Bank euro reference rates (source: ECB).

Job content belongs to the employers who publish it. Use it in line with each employer's terms and applicable law. The Actor identifies itself with the User-Agent `ATSHiringSignals/1.0`.

***

### Resumen en español

**ATS Hiring Signals** convierte una lista de dominios o nombres de empresa en **ofertas de empleo abiertas y señales de contratación**. Detecta si la empresa usa **Greenhouse, Lever o Ashby** y lee sus **APIs públicas oficiales**, sin login ni scraping de LinkedIn.

Devuelve:

- ofertas normalizadas (salario anualizado en USD, nivel de seniority, área funcional, remoto/híbrido/presencial);
- un resumen por empresa con una **puntuación de intención de contratación (0–100)** con fórmula transparente;
- **las ofertas nuevas y cerradas** desde la última ejecución.

Ideal para prospección B2B, reclutamiento y agentes de IA.

**Precio:** $0.002 por empresa encontrada, $0.001 por oferta y $0.0005 por cambio detectado.

# Actor input Schema

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

One entry per company. Accepts a domain ("linear.app"), a website URL ("https://www.stripe.com/jobs"), a company name ("Ramp"), a job-board URL ("https://jobs.lever.co/spotify") or an explicit board token ("greenhouse:airbnb", "lever:palantir", "ashby:ramp"). Domains give the most reliable detection; names rely on slug probing.

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

Optional extra boards when you already know the ATS. Format "greenhouse:<token>", "lever:<token>" (EU instance: "lever:eu:<token>"), "ashby:<token>", or a board URL. These skip detection entirely.

## `titleKeywords` (type: `array`):

Keep jobs whose title contains ANY of these (case-insensitive substring). Example: \["engineer", "developer"].

## `excludeTitleKeywords` (type: `array`):

Drop jobs whose title contains ANY of these. Example: \["intern"].

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

Keep jobs whose location(s) or country contain ANY of these substrings, e.g. \["New York", "London", "US"]. The value "remote" also matches jobs classified as remote.

## `workplace` (type: `string`):

Filter by normalized workplace type. "unknown" jobs (no signal in the ATS data) only pass with "any".

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

Keep jobs whose department or team contains ANY of these substrings, e.g. \["Sales", "Engineering"].

## `seniority` (type: `array`):

Keep only these rule-based seniority levels (classified from the job title). Leave empty for all.

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

Keep only jobs whose posting date is within this many days. Jobs without a posting date are dropped when this is set.

## `includeDescriptions` (type: `boolean`):

Add the full job description to each job record. Makes output much larger.

## `descriptionFormat` (type: `string`):

Plain text (good for LLMs) or the original HTML.

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

Cap the number of job records output per company (0 = no limit). The summary still counts all matching jobs.

## `changeTracking` (type: `boolean`):

Store a snapshot of each board in a named key-value store and report new/closed jobs, isNew flags and first-seen dates on later runs.

## `onlyNewJobs` (type: `boolean`):

Output only jobs that were not on the board at the previous tracked run (the first run only saves a baseline). Summaries are still produced.

## `emitChangeEvents` (type: `boolean`):

Write job_added / job_removed records to the "changes" dataset (after the first tracked run).

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

Named key-value store that keeps snapshots and the detection cache. Use different names for independent monitors. Only a-z, 0-9 and inner hyphens are valid; other characters are converted ("My Store" becomes "my-store").

## `detectFromWebsite` (type: `boolean`):

For domain inputs, read the homepage and up to 4 careers pages (robots.txt respected) and look for Greenhouse, Lever or Ashby board links.

## `probeSlugs` (type: `boolean`):

If no link is found, try likely board tokens (from the domain or name) against the three official APIs. Results include a confidence level.

## `cacheDetection` (type: `boolean`):

Remember which board a company uses (in the state store) to skip detection on later runs.

## `detectionCacheDays` (type: `integer`):

Re-detect after this many days.

## `parseSalaryFromDescription` (type: `boolean`):

When the ATS has no structured salary, extract a pay range from the description (marked salarySource = description_text).

## `convertSalaryToUSD` (type: `boolean`):

Add salaryAnnualMinUSD / salaryAnnualMaxUSD using European Central Bank reference rates of the day.

## `fetchGreenhouseSalaries` (type: `boolean`):

For Greenhouse jobs without a salary, call the per-job endpoint with pay_transparency=true (1 extra request per job, slower).

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

Process at most this many inputs (0 = all).

## `maxConcurrency` (type: `integer`):

Companies processed in parallel. Per-host rate limits stay in force (Lever: 1 request/second, as its robots.txt asks).

## Actor input object example

```json
{
  "companies": [
    "linear.app",
    "notion.so",
    "Ramp",
    "greenhouse:airbnb",
    "https://jobs.lever.co/palantir"
  ],
  "boards": [
    "greenhouse:stripe",
    "ashby:openai",
    "lever:zoox"
  ],
  "titleKeywords": [
    "engineer",
    "account executive"
  ],
  "excludeTitleKeywords": [
    "intern",
    "contract"
  ],
  "locations": [
    "New York",
    "London",
    "remote"
  ],
  "workplace": "any",
  "departments": [
    "Sales",
    "Engineering"
  ],
  "includeDescriptions": false,
  "descriptionFormat": "text",
  "maxJobsPerCompany": 0,
  "changeTracking": true,
  "onlyNewJobs": false,
  "emitChangeEvents": true,
  "stateStoreName": "ats-hiring-signals-state",
  "detectFromWebsite": true,
  "probeSlugs": true,
  "cacheDetection": true,
  "detectionCacheDays": 14,
  "parseSalaryFromDescription": true,
  "convertSalaryToUSD": true,
  "fetchGreenhouseSalaries": false,
  "maxCompanies": 0,
  "maxConcurrency": 5
}
```

# Actor output Schema

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

Normalized open jobs (one record per job).

## `companies` (type: `string`):

Per-company detection result, counts by department/function/seniority, hiring velocity, new/closed since last run and hiring-intent score.

## `changes` (type: `string`):

job_added / job_removed events since the previous tracked run.

## `runStats` (type: `string`):

Counts, HTTP requests per host and FX date for this run.

# 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": [
        "linear.app",
        "Ramp",
        "greenhouse:discord",
        "https://jobs.lever.co/spotify"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dotcom/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": [
        "linear.app",
        "Ramp",
        "greenhouse:discord",
        "https://jobs.lever.co/spotify",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("dotcom/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": [
    "linear.app",
    "Ramp",
    "greenhouse:discord",
    "https://jobs.lever.co/spotify"
  ]
}' |
apify call dotcom/ats-hiring-signals --silent --output-dataset

```

## MCP server setup

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