# Multi-ATS Jobs & Hiring Signals API (7 platforms) (`reedy_flagon/multi-ats-hiring-signals-api`) Actor

Normalized jobs and hiring signals (tools, seniority, salary, visa) from 7 ATS platforms, incl. Greenhouse, Lever, Ashby and Workday. Pay per delivered job.

- **URL**: https://apify.com/reedy_flagon/multi-ats-hiring-signals-api.md
- **Developed by:** [Asnim Ariff](https://apify.com/reedy_flagon) (community)
- **Categories:** Jobs, Lead generation, Developer tools
- **Features:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.20 / 1,000 job delivereds

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

## Multi-ATS Career Site Jobs & Hiring Signals API

**Name a company. Get its open jobs — normalized, with the tools each role mentions, seniority, salary and visa signals.**

One input line (`["stripe","nvidia","palantir"]`) returns live open roles from **seven ATS platforms**, fetched over HTTP at run time. No logins, no proxies, no browser.

Built for **lead generation** (find companies hiring for a tool you sell), **recruiting intelligence** (track competitors' openings and stated salaries), **job boards** and **AI agents** that need structured hiring data.

### Why this Actor

✅ **Filter by tools** — `toolsFilter: ["snowflake","dbt"]` finds jobs mentioning either tool; only matching open jobs are charged.
✅ **Live company boards** — data comes from the public ATS at run time, not a stale cache.
✅ **Seven ATS platforms** — Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee and Workday.
✅ **Signals on each row** — tools, title-based seniority, stated yearly USD salary and a negation-aware visa-sponsorship flag.
✅ **Monitor changes** — delta mode reports `NEW_ROLE`, `SALARY_CHANGED` and `ROLE_CLOSED` events; see limitations below.
✅ **Pay for delivered jobs** — filtered, closed, unknown and error rows carry no job-delivered charge; an Actor start charge applies.
✅ **Agent-friendly input** — accept company names, domains, URLs and aliases; bad entries return free error items.

### Quick start

**1. Apify Console** — open the Actor, paste an input, press Start:

```json
{ "companies": ["stripe"], "maxJobsPerCompany": 5 }
```

**2. API (any language, or the `apify-client` SDK)** — replace `<YOUR_TOKEN>`:

```bash
curl -X POST "https://api.apify.com/v2/acts/reedy_flagon~multi-ats-hiring-signals-api/run-sync-get-dataset-items?token=<YOUR_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"companies":["stripe","nvidia"],"toolsFilter":["snowflake","dbt"],"maxJobsPerCompany":100}'
```

The response is the dataset itself — one JSON object per delivered job.

**3. Agents / MCP** — the Actor also runs from an LLM agent through the Apify MCP server; the same input object is all the agent needs.

### Sample data (free, no signup)

Browse **115 real jobs delivered by this Actor** — Stripe, Nvidia, Palantir, Linear and Hugging Face,
filtered to roles mentioning `kubernetes`, `dbt`, `snowflake` or `airflow`:

```
https://api.apify.com/v2/datasets/ebfwuRYPrNuXXw3mX/items?format=json&clean=1
```

The dataset is public and reads without a token. Raw endpoint, no UI: pipe it into `jq` or open it in
the Apify Console dataset viewer if you prefer a table.

### What you get back

| Field | Meaning |
|---|---|
| `globalId` | Stable cross-run key (`greenhouse:stripe:8172510`) — use it to deduplicate and to detect changes. |
| `company`, `companyDomain`, `ats` | Who is hiring, their domain, and which ATS the board runs on. |
| `title`, `department`, `location`, `workplaceType` | Role details; `workplaceType` can be null. |
| `seniority` | `intern` → `vp`, inferred from the title. |
| `salaryMinUsd`, `salaryMaxUsd`, `salaryRaw`, `currency` | Only stated yearly USD ranges are parsed — no conversion, no guessing. |
| `toolsMentioned` | Tools detected in the posting text, matched against a fixed lexicon. |
| `visaSponsorship` | Negation-aware text signal (not legal advice). |
| `status`, `changeEvents`, `firstSeen`, `lastSeen` | `OPEN` plus delta events when `onlyNewOrChanged` is on. |
| `jobUrl`, `publishedAt`, `inputIndex`, `jobsFound` | Apply link, ATS publication date (when given), and which input entry produced the row. |

### Coverage

| ATS | Discovery | Notes |
|---|---|---|
| Greenhouse | name / domain / URL | Public board API. |
| Lever | name / domain / URL | Public postings API. |
| Ashby | name / domain / URL | Public job board API. |
| Workable | name / domain / URL | Public widget API. |
| SmartRecruiters | name / domain / URL | May need extra detail requests for tool filtering. |
| Recruitee | name / domain / URL | Public offers API. |
| Workday | **career URL recommended** | `publishedAt` is always null; salary changes are not detected in delta mode. |

### Use cases

| Use case | How to use the data |
|---|---|
| Sales and lead generation | Find companies hiring for roles that mention a tool. |
| Recruiting intelligence | Track new roles and stated salary changes. |
| Job boards | Collect normalized open roles across supported ATS platforms. |
| AI agents | Feed structured jobs and hiring signals into an agent workflow. |
| Hiring monitoring | Schedule delta runs to spot new and closed roles. |

### Pricing (pay per event)

| Event | Price | When it is charged |
|---|---|---|
| Actor start | $0.00005 | once per run (automatic) |
| `job-delivered` | **$0.0012** | for each matched OPEN job saved to the dataset |

| Jobs delivered | Cost |
|---|---|
| 100 | $0.12 |
| 1,000 | $1.20 |
| 10,000 | $12.00 |

**Free ($0):** closed roles (ROLE_CLOSED records), roles removed by your seniority filter, roles beyond `maxJobsPerCompany`, unknown companies (`ATS_NOT_FOUND`), empty boards, invalid inputs and error items. You are charged only after the job has been saved. The Actor respects your maximum total charge per run and stops gracefully (partial results are kept) when the limit is reached.

In delta mode, unchanged roles are not delivered or charged. The first run treats every role as new, and each run has an Actor start charge.

### Input

| Field | Type | Description |
|---|---|---|
| `companies` | array of strings | Names (`stripe`), domains (`stripe.com`) or career URLs (`https://jobs.lever.co/palantir`). |
| `maxJobsPerCompany` | integer, default 50 | Maximum open jobs delivered per company. |
| `onlyNewOrChanged` | boolean, default false | Delta mode (see below). |
| `minSeniority` | string, optional | `intern`, `junior`, `mid`, `senior`, `staff`, `principal`, `director`, `vp`. |
| `titleKeyword` | string, optional | Deliver only jobs whose title contains this text (case-insensitive substring). |
| `toolsFilter` | array of strings, optional | Deliver only jobs whose `toolsMentioned` contains ANY listed tool (case-insensitive, lexicon spelling, e.g. `["snowflake","dbt","kubernetes"]`). Empty or unknown entries are ignored. |
| `location` | string, optional | Deliver only jobs whose `location` contains this text (case-insensitive). |
| `remoteOnly` | boolean, default false | Deliver only jobs with workplace type `remote` or remote / home-office wording in the location. Jobs with no remote signal are dropped (free). |
| `postedAfter` | string `YYYY-MM-DD`, optional | Deliver only jobs with `publishedAt` on or after this date. Jobs for which the ATS gives no date are dropped (free). |
| `snapshotStoreName` | string, optional | Named key-value store used for delta snapshots. |

Filters combine with AND; `toolsFilter` matches ANY listed tool. Filters run before delivery, so filtered jobs are free. Invalid filter values are ignored and produce one free `FILTER_INVALID` warning per company. If every tool is invalid, no tools filter is applied; check the warning before using the results.

Aliases merged into `companies`: `urls`, `startUrls` (strings or `{ "url": ... }`), `domains`, `websites`, `url`, `company`. Comma- or newline-separated strings also work. Invalid entries produce free error items.

#### Input examples

```json
{ "companies": ["stripe"], "maxJobsPerCompany": 5 }
```

```json
{
  "companies": ["palantir", "linear.app", "https://apply.workable.com/huggingface"],
  "maxJobsPerCompany": 100,
  "minSeniority": "senior"
}
```

```json
{ "urls": ["https://stripe.com"] }
```

#### Recipes

Tools-filtered lead generation:

```json
{"companies":["stripe","nvidia"],"toolsFilter":["snowflake","dbt","cuda"],"titleKeyword":"engineer","maxJobsPerCompany":100}
```

New-roles-only monitor (also emits salary changes and free closed-role events):

```json
{"companies":["stripe"],"onlyNewOrChanged":true}
```

Remote roles at senior level or above:

```json
{"companies":["palantir"],"minSeniority":"senior","remoteOnly":true}
```

### Output example

```json
{
  "jobId": "8172510",
  "globalId": "greenhouse:stripe:8172510",
  "company": "Stripe",
  "companyDomain": "stripe.com",
  "ats": "greenhouse",
  "title": "Abuse Investigator",
  "department": "8611 Security Analytics",
  "location": "Seattle, San Francisco, New York City",
  "workplaceType": null,
  "seniority": "mid",
  "salaryMinUsd": null,
  "salaryMaxUsd": null,
  "salaryRaw": null,
  "currency": null,
  "visaSponsorship": false,
  "toolsMentioned": ["python", "databricks", "trino", "pyspark", "pandas"],
  "jobUrl": "https://stripe.com/jobs/search?gh_jid=8172510",
  "publishedAt": "2026-09-09T10:50:29-04:00",
  "inputIndex": 0,
  "jobsFound": 6,
  "firstSeen": "2026-11-07T14:26:01+00:00",
  "lastSeen": "2026-11-07T14:26:01+00:00",
  "status": "OPEN",
  "changeEvents": []
}
```

`globalId` is a cross-run job key; `inputIndex` identifies the input entry; `jobsFound` is the board count available to the Actor. `publishedAt` is the ATS publication date when available. `companyDomain` can be null when it cannot be determined.

An unknown company produces an `ATS_NOT_FOUND` error item at no job-delivered charge. Other errors include `INVALID_INPUT`, `EMPTY_BOARD`, `FETCH_ERROR` and `PROCESSING_ERROR`.

### Delta mode

Set `onlyNewOrChanged: true` and schedule runs to compare boards against saved snapshots. The default key-value store is `ats-hiring-signal-snapshots`; use `snapshotStoreName` to change it.

- `NEW_ROLE`: absent from the previous snapshot. On the first run, every role is new.
- `SALARY_CHANGED`: the parsed salary differs from the snapshot, subject to the ATS limitations below.
- `ROLE_CLOSED`: a previously seen role is no longer on the board. Closed records are free.

Unchanged roles are not emitted or charged. Snapshots cover the whole board, not just the first `maxJobsPerCompany` roles.

### Limitations

- **Coverage:** only the seven listed ATS platforms are supported. Unsupported or undiscovered boards can return `ATS_NOT_FOUND`. Public boards may hide jobs.
- **Detection:** names and domains are used to guess board slugs; if the slug differs, pass the supported career URL. Workday discovery can require its specific career URL.
- **Salary:** only stated yearly USD ranges fill `salaryMinUsd` and `salaryMaxUsd`; other salary text stays raw. No conversion or guessing. When several regional ranges are listed, the first found is used.
- **Seniority:** inferred from titles; unrecognized titles default to `mid`, and ambiguous titles can be misclassified.
- **Tools:** matched against a fixed lexicon, not every possible tool. Unknown `toolsFilter` entries are ignored with a free warning. If none are valid, the filter is inactive. Mentions can have false positives.
- **Visa sponsorship:** a negation-aware text signal, not legal advice or a guarantee of eligibility.
- **Dates:** `postedAfter` drops undated jobs. Workday `publishedAt` is always null, so that filter returns no Workday jobs.
- **Remote:** `remoteOnly` drops jobs without a remote signal; workplace type can be null.
- **Details and delta:** SmartRecruiters and Workday may require extra detail requests for tool filtering. Workday salary changes are not detected in delta mode. SmartRecruiters salary changes may be missed for roles whose details were not delivered.
- **Domains:** `companyDomain` comes from the input or a company-hosted job URL; it is not guessed from an ATS-hosted URL.

### FAQ

**Is this a scraper?** It calls public career-site endpoints over HTTP. No logins, proxies or browser are needed.

**What if a company isn't found?** You get a free `ATS_NOT_FOUND` item. Try its supported ATS career URL instead.

**Do you charge for closed roles?** No. `ROLE_CLOSED` records have no job-delivered charge. The Actor start charge still applies.

**Does delta mode find every salary change?** No. Workday salary changes are not detected, and SmartRecruiters has the detail-fetch limitation described above.

**How do I find companies hiring for a specific tool?** Pass the tool to `toolsFilter` along with the companies you care about: `{"companies":["stripe","nvidia"],"toolsFilter":["snowflake"]}`. Only jobs that mention it are delivered and charged.

**Can I monitor new job postings automatically?** Yes — run with `"onlyNewOrChanged": true` on a schedule (for example daily). New roles, closed roles and salary changes come back as events; unchanged roles cost nothing.

### Changelog and support

See `CHANGELOG.md` for version history. Questions, a missing ATS platform, or a board that should be found? Open an issue on the Actor's page — platform requests are read.

# Changelog

This Actor's version history is a separate document: https://apify.com/reedy_flagon/multi-ats-hiring-signals-api/changelog.md

# Actor input Schema

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

Company names (`stripe`), domains (`stripe.com`) or career-page URLs (`https://jobs.lever.co/palantir`). The ATS is detected automatically.

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

Upper bound of OPEN jobs delivered (and charged) per company.

## `onlyNewOrChanged` (type: `boolean`):

Compare with the snapshot of the previous run and emit only roles with a NEW_ROLE or SALARY_CHANGED event, plus ROLE_CLOSED records (closed roles are free). The first run has no snapshot, so every role is NEW_ROLE.

## `minSeniority` (type: `string`):

Optional filter. Roles below this level are skipped and never charged.

## `titleKeyword` (type: `string`):

Optional filter: deliver only jobs whose title contains this text (case-insensitive). Other jobs are skipped and never charged.

## `toolsFilter` (type: `array`):

Optional filter: deliver only jobs whose toolsMentioned contains ANY of these tools (case-insensitive, lexicon spelling, e.g. snowflake, dbt, kubernetes). Unknown or empty entries are ignored. Other jobs are never charged.

## `location` (type: `string`):

Optional filter: deliver only jobs whose location contains this text (case-insensitive). Never charged for other jobs.

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

Deliver only jobs with workplace type remote or remote/home-office wording in the location. Jobs with no remote signal are dropped (free).

## `postedAfter` (type: `string`):

Deliver only jobs published on or after this date. Jobs for which the ATS gives no date (e.g. all Workday jobs) are dropped (free).

## `snapshotStoreName` (type: `string`):

Named key-value store that keeps per-company snapshots between runs (delta mode only).

## `urls` (type: `array`):

Alias of `companies`. Accepts strings or `{url}` objects.

## `startUrls` (type: `array`):

Alias of `companies`. Accepts strings or `{url}` objects.

## `domains` (type: `array`):

Alias of `companies`.

## `websites` (type: `array`):

Alias of `companies`.

## `url` (type: `string`):

Single company URL, domain or name. Alias of `companies`.

## `company` (type: `string`):

Single company name or domain. Alias of `companies`.

## Actor input object example

```json
{
  "companies": [
    "stripe"
  ],
  "maxJobsPerCompany": 5,
  "onlyNewOrChanged": false,
  "remoteOnly": false,
  "snapshotStoreName": "ats-hiring-signal-snapshots"
}
```

# Actor output Schema

## `jobs` (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"
    ],
    "maxJobsPerCompany": 5
};

// Run the Actor and wait for it to finish
const run = await client.actor("reedy_flagon/multi-ats-hiring-signals-api").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"],
    "maxJobsPerCompany": 5,
}

# Run the Actor and wait for it to finish
run = client.actor("reedy_flagon/multi-ats-hiring-signals-api").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"
  ],
  "maxJobsPerCompany": 5
}' |
apify call reedy_flagon/multi-ats-hiring-signals-api --silent --output-dataset

```

## MCP server setup

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

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/wfAyVUYDMfjITljhc/builds/kaTSmJdyK6zpOGz6X/openapi.json
