# Company Hiring Signals API: Open Roles by Department (`interconnected_seaport/ats-hiring-signals`) Actor

Turn a list of company career pages into hiring signals: open roles by department and function, seniority mix, locations, remote share, posting velocity, hiring surge flags and change since the last run. Official public job-board endpoints of 7 ATS, no proxies.

- **URL**: https://apify.com/interconnected_seaport/ats-hiring-signals.md
- **Developed by:** [Zenith Ops](https://apify.com/interconnected_seaport) (community)
- **Categories:** Jobs, Lead generation
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 company analyzeds

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 API: Open Roles by Department

Give the actor a list of companies and get **one row of hiring signals per company**: how many roles are open, in which departments and functions (engineering, sales, marketing, ...), at what seniority, where, how remote, how fresh the postings are, and whether the company is in a **hiring surge**. Schedule it weekly and every row also tells you **what changed since the last run** (new roles, closed roles, net change).

Data comes only from the **official public job-board endpoints** that employers expose for embedding and indexing on 7 applicant tracking systems (ATS): Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee, Teamtailor and Personio. No login, no HTML scraping, no proxies, no anti-bot bypass.

### Use cases

- **B2B sales and lead scoring:** a company hiring 6 sales people and a Head of Revenue is growing and has budget. Filter rows by `signals.sales_hiring` or `signals.hiringSurge`.
- **Investors and analysts:** track headcount growth proxies for a watchlist of startups: open roles, new vs. closed roles per week, share of senior roles, expansion into new countries.
- **Recruiting agencies and talent teams:** see which employers are hiring in your niche (`data_ai`, `engineering`, `security`) and where.
- **Competitive intelligence:** benchmark a competitor's hiring by function (are they building an AI team? opening a new country?).
- **Job-market research:** compare remote share, salary disclosure and posting age across companies.

### Input

| Field | Meaning |
|---|---|
| `companies` (required) | Company slugs (`figma`), slugs with ATS (`greenhouse:figma`) or career page URLs (`https://jobs.lever.co/spotify`, `https://boards.greenhouse.io/figma`, `https://acme.recruitee.com`). Without an ATS prefix the actor probes all 7 systems and uses the one with most open roles. Max 1,000 entries |
| `maxCompanies` | Cap on companies analyzed and charged per run (default 100, max 1,000) |
| `compareToLastRun` | Default on. Stores each company's open roles between runs (named key-value store `ats-hiring-signals-state`) and fills `changeSinceLastRun` |
| `includeTopTitles` | Default on. Adds the 10 most common job titles per company |
| `departmentKeywordGroups` | Optional object `{"group": ["keyword", ...]}` to override or extend the function classifier (see below) |
| `concurrency` | Companies fetched in parallel (default 5, max 10) |

You need the company's identifier (slug) or career page URL. The actor is not a database of all companies.

#### Example input

```json
{
  "companies": ["figma", "greenhouse:stripe", "https://jobs.lever.co/spotify"],
  "compareToLastRun": true,
  "includeTopTitles": true,
  "maxCompanies": 100
}
```

#### Custom function keywords

```json
{
  "companies": ["figma"],
  "departmentKeywordGroups": {
    "sales": ["account executive", "sdr$", "business development"],
    "climate": ["carbon", "sustainab"]
  }
}
```

An existing group name (for example `sales`) replaces that group's default keywords. A new name (`climate`) is added as an extra key in `rolesByFunction` and is checked first.

### Output

One dataset row per company (example values are illustrative):

```json
{
  "company": "Example Co",
  "ats": "greenhouse",
  "slug": "exampleco",
  "careersUrl": "https://boards.greenhouse.io/exampleco",
  "openRoles": 64,
  "rolesByDepartment": [{ "department": "Engineering", "count": 21 }, { "department": "Sales", "count": 12 }],
  "rolesByFunction": { "engineering": 24, "sales": 13, "marketing": 5, "product": 4, "design": 3, "data_ai": 6, "customer_success_support": 3, "operations": 2, "finance": 1, "legal": 1, "hr_recruiting": 1, "security": 1, "executive": 0, "other": 0 },
  "seniorityMix": { "intern_entry": 2, "junior": 3, "mid": 1, "senior": 22, "lead_manager": 7, "director_plus": 3, "unknown": 26 },
  "topLocations": [{ "location": "Stockholm", "count": 30 }],
  "countries": [{ "country": "SE", "count": 30 }, { "country": "DE", "count": 8 }],
  "remoteShare": 0.25,
  "hybridShare": 0.4,
  "postedLast7Days": 9,
  "postedLast30Days": 35,
  "newestPostingAt": "2026-10-16T08:12:00.000Z",
  "oldestPostingAt": "2026-03-02T09:00:00.000Z",
  "medianPostingAgeDays": 21.5,
  "salaryDisclosedShare": 0.1,
  "topTitles": [{ "title": "Account Executive", "count": 6 }],
  "signals": { "hiringSurge": true, "sales_hiring": true, "engineering_hiring": true, "ai_ml_hiring": true, "security_hiring": false, "international_expansion": true },
  "changeSinceLastRun": { "previousOpenRoles": 58, "openRolesDelta": 6, "newRoles": 11, "closedRoles": 5, "previousRunAt": "2026-10-10T06:00:00.000Z" },
  "checkedAt": "2026-10-17T06:00:03.120Z"
}
```

#### How the numbers are computed

- **openRoles:** open (listed) roles returned by the job board; at most 5,000 are read per company, and `openRoles` reports the board's own total when it is larger.
- **remoteShare / hybridShare:** share among roles that state a work arrangement (explicit remote flag or workplace type). Remote is never guessed from the title. `null` if no role states it.
- **salaryDisclosedShare:** share of roles that publish a salary. `postedLast7Days` / `postedLast30Days`, ages and `newestPostingAt` use the posting date from the ATS, so roles without a date are ignored there.
- **countries:** only explicit country fields (ISO-2); countries are not guessed from city names, so some ATS yield few or no countries.
- **hiringSurge:** `postedLast30Days >= max(5, 0.5 * openRoles)`.
- **sales_hiring / engineering_hiring:** at least 3 roles in that function. **security_hiring:** at least 1 security role. **ai_ml_hiring:** at least 1 title matching the AI keywords below.
- **international_expansion:** at least 3 countries among open roles and at least 1 role posted in the last 30 days.
- **changeSinceLastRun:** compares job IDs with the previous run (up to 3,000 stored per company). `newRoles`/`closedRoles` are `null` if the board has more roles than were stored or read; `openRolesDelta` is always exact. Companies whose slug changes or that disappear are simply not compared. The first run for a company returns `null`.

### Classifier keyword lists

The classifiers are deterministic keyword rules (no AI, no guessing), unit-tested, and applied to the **job title** first; the **department** is used only when the title gives no hit. The first group (in the table order) with a matching keyword wins; the order resolves ambiguous titles (`Machine Learning Engineer` is `data_ai`, `Sales Engineer` is `sales`, `Product Designer` is `design`, `Technical Recruiter` is `hr_recruiting`). Titles without any hit are `other`. English, Swedish and German keywords are included; other languages mostly end up in `other`.

Matching rules (case-insensitive):

- `engineer` matches at the **start of a word** (engineer, engineering).
- `sales$` (trailing `$`) matches the **whole word** only (sales, not Salesforce).
- `~entwickl` (leading `~`) matches **anywhere in a word** (Softwareentwickler).
- A space in a phrase also matches a hyphen (`full stack` matches "full-stack").

#### Function keywords

| Function (priority order) | Keywords |
|---|---|
| security | `security`, `infosec`, `cyber`, `appsec`, `pentest`, `penetration test`, `soc analyst`, `säkerhet`, `~sicherheit`, `ciso$` |
| data_ai | `data scien`, `data engineer`, `data analy`, `data architect`, `data & ai`, `data and ai`, `data platform`, `analytics`, `analytik`, `business intelligence`, `machine learning`, `maskininlärning`, `maschinelles lernen`, `deep learning`, `mlops`, `ml$`, `ai$`, `genai`, `generative`, `artificial intelligence`, `künstliche intelligenz`, `llm`, `nlp$`, `computer vision`, `research scientist`, `applied scientist`, `dataanaly` |
| hr_recruiting | `recruit`, `rekryter`, `talent$`, `talent acquisition`, `people$`, `human resources`, `hr$`, `personalleiter`, `~personalreferent`, `~personalsachbearbeit`, `personalwesen`, `personalabteilung`, `employer brand`, `hrbp$`, `compensation & benefits` |
| legal | `legal`, `counsel`, `lawyer`, `paralegal`, `attorney`, `jurist`, `~juridi`, `advokat`, `rechtsanw`, `syndikus`, `compliance`, `privacy`, `dpo$` |
| finance | `financ`, `finans`, `finanz`, `accountant`, `accounting`, `bokför`, `buchhalt`, `controller`, `ekonom`, `treasury`, `payroll`, `audit`, `tax$`, `~steuer`, `revisor`, `fp&a`, `lönespec`, `lönehand`, `bookkeep` |
| customer_success_support | `customer success`, `customer support`, `customer service`, `customer care`, `customer experience`, `client success`, `success manager`, `technical account`, `support$`, `helpdesk`, `help desk`, `service desk`, `kundtjänst`, `kundsupport`, `kundservice`, `~kundenservice`, `~kundenbetreu`, `~kundensupport`, `onboarding specialist`, `implementation` |
| sales | `sales$`, `salesperson`, `account executive`, `account manager`, `account director`, `key account`, `business development`, `bdr$`, `sdr$`, `inside sales`, `sales development`, `partnership`, `revenue`, `säljare`, `~sälj`, `försäljning`, `~vertrieb`, `~verkäuf`, `channel manager`, `pre-sales`, `presales` |
| design | `designer`, `ux`, `ui$`, `user experience`, `visual design`, `brand design`, `design$`, `~grafik`, `~formgiv`, `creative director`, `illustrator` |
| marketing | `marketing`, `growth`, `brand`, `content`, `seo$`, `sem$`, `communications`, `copywriter`, `social media`, `demand gen`, `kommunikation`, `~marknads`, `~marketing`, `public relations`, `pr$`, `community manager` |
| engineering | `engineer`, `developer`, `software`, `devops`, `sre$`, `backend`, `frontend`, `full stack`, `fullstack`, `programmer`, `architect`, `qa$`, `sdet`, `firmware`, `embedded`, `ios$`, `android`, `system administrator`, `sysadmin`, `tech lead`, `technical lead`, `site reliability`, `~ingenjör`, `~ingenieur`, `~utvecklare`, `~entwickl`, `~mjukvaru`, `~systemutveckl` |
| product | `product manager`, `product owner`, `product lead`, `product management`, `product$`, `produktchef`, `produktägare`, `produktmanager`, `produktmanagement`, `produktverantwortlich` |
| executive | `ceo$`, `chief`, `founder`, `co-founder`, `managing director`, `general manager`, `vd$`, `verkställande`, `~geschäftsführ`, `president$`, `executive director`, `managing partner` |
| operations | `operations`, `operational`, `logistic`, `supply chain`, `procurement`, `purchas`, `warehouse`, `facilit`, `office manager`, `office$`, `administrat`, `assistant$`, `coordinator`, `project manager`, `program manager`, `projektledare`, `projektmanager`, `~projektleit`, `inköp`, `~logistik`, `~einkauf`, `~lagerist`, `bürokraft`, `sekreterare`, `receptionist`, `chauffeur`, `driver$` |

AI/ML title keywords (for `signals.ai_ml_hiring`): `machine learning`, `maskininlärning`, `maschinelles lernen`, `deep learning`, `mlops`, `ml$`, `ai$`, `genai`, `generative`, `artificial intelligence`, `künstliche intelligenz`, `ki$`, `llm`, `nlp$`, `computer vision`, `research scientist`, `applied scientist`

#### Seniority keywords

Seniority is read from the title (priority order, first match wins). Titles without a marker are `unknown` - the actor does not guess a level for plain titles like "Software Engineer". If the title is silent, a seniority label from the ATS (Personio, SmartRecruiters) or an internship employment type is used.

| Seniority (priority order) | Keywords |
|---|---|
| intern_entry | `intern$`, `interns$`, `internship`, `praktik`, `~werkstudent`, `student$`, `studentisch`, `trainee$`, `graduate`, `apprentice`, `lärling`, `lia$`, `azubi`, `auszubildend`, `ausbildung`, `exjobb`, `examensarbete`, `masterarbete`, `thesis$`, `ferienjob`, `sommarjobb`, `new grad`, `working student` |
| director_plus | `head of`, `heads of`, `director$`, `directeur$`, `direktör`, `~direktor`, `vp$`, `svp$`, `evp$`, `vice president`, `president$`, `chief`, `c-level`, `ceo$`, `cto$`, `cfo$`, `coo$`, `cmo$`, `cio$`, `ciso$`, `cpo$`, `cro$`, `founder`, `managing director`, `~geschäftsführ`, `general manager`, `vd$`, `verkställande`, `bereichsleiter` |
| lead_manager | `lead$`, `leader$`, `team lead`, `teamlead`, `manager`, `~leiter`, `~leitung`, `principal`, `supervisor`, `staff engineer`, `staff software`, `staff data`, `staff designer`, `staff product`, `staff scientist`, `~chef`, `teamleader` |
| senior | `senior`, `sr$`, `~erfaren` |
| junior | `junior`, `jr$`, `entry level`, `nyexaminerad`, `berufseinsteiger`, `~einsteiger`, `early career` |
| mid | `mid-level`, `mid level`, `midlevel`, `intermediate`, `ii$`, `iii$` |

Individual-contributor "manager" titles do **not** count as `lead_manager`: these phrases are removed before the check: `account manager`, `product manager`, `project manager`, `program manager`, `customer success manager`, `success manager`, `partner manager`, `community manager`, `case manager`, `relationship manager`, `product marketing manager`, `channel manager`, `lead generation`, `lead gen`, `lead development`, `lead qualification`, `projektmanager`, `produktmanager`

Known limits: a keyword list cannot read intent. "Product Manager, Software Tools" is classified as `engineering`, "Art Director" as `director_plus`, and "Head Chef" is `other`. Use `departmentKeywordGroups` to adapt the function rules to your needs.

### Pricing

Pay per event, no subscription:

| Event | Price |
|---|---|
| Actor start | $0.00005 (Apify default) |
| `company-analyzed` (one company with a successful result) | $0.015 |

That is **$15 per 1,000 companies**. Companies that are not found or fail upstream are not charged; they are listed in the run's `SUMMARY` record. A company whose board exists but has no open roles is a valid result (`openRoles: 0`) and is charged. Set a maximum total charge in the Apify run options and the actor stops when it is reached.

### API examples

curl (replace `YOUR_API_TOKEN`):

```bash
curl -X POST "https://api.apify.com/v2/acts/interconnected_seaport~ats-hiring-signals/run-sync-get-dataset-items?token=YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"companies": ["figma", "greenhouse:stripe"]}'
```

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_API_TOKEN")
run = client.actor("interconnected_seaport/ats-hiring-signals").call(
    run_input={"companies": ["figma", "greenhouse:stripe", "https://jobs.lever.co/spotify"]}
)
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
    if row["signals"]["hiringSurge"]:
        print(row["company"], row["openRoles"], row["rolesByFunction"])
```

For weekly tracking create an Apify schedule with the same input; `changeSinceLastRun` is filled from the second run on.

### FAQ

**Which job boards are supported?** Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee, Teamtailor and Personio. Workable, Workday, LinkedIn, Indeed and company-specific career sites are not supported.

**How do I find a company's slug?** Open its careers page. If the URL looks like `boards.greenhouse.io/figma` or `jobs.lever.co/spotify`, paste the URL or the slug. You can also just try the company name - the actor probes all 7 systems.

**Why is my company "not found"?** It uses another ATS, the slug differs, or it has no public board (or no open roles when auto-detecting). The run summary lists every such entry.

**Are the function and seniority numbers exact?** They are rule-based and transparent (lists above), good for trends and comparisons but not a substitute for reading the roles.

**Are job descriptions fetched?** No. Only titles, departments, locations, dates, work arrangement and salary flags are used, which keeps runs fast and cheap.

**Does "newRoles" count reposted jobs?** A job is "new" when its ATS job ID was not present in the previous run. Reposts under a new ID count as new.

### Legal and ethics

- Only official public endpoints that employers expose for embedding or indexing their job boards are used. No login, no scraping of HTML pages, no proxies, no anti-bot bypass. A 404 means "no board" and HTTP 429/5xx responses are retried politely with backoff.
- Job postings and the career data belong to the employers. Use the data in line with the terms of the sources and applicable law.
- GDPR: job postings are not personal data in general. Recruiter or contact names occasionally appear in postings; this actor does not extract them. Output contains only company-level aggregates and job titles.

### Limitations

- Only 7 ATS; you must supply the company slug or URL (no company database).
- Posting dates are not provided by every ATS, so freshness metrics can be based on a subset of roles.
- Keyword classifiers cover English, Swedish and German titles.
- At most 5,000 roles per company are read.

# Actor input Schema

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

Company slugs ("figma"), slugs with ATS ("greenhouse:figma") or career page URLs (e.g. https://jobs.lever.co/spotify). Without an ATS prefix all 7 supported systems are probed (Greenhouse, Lever, Ashby, SmartRecruiters, Recruitee, Teamtailor, Personio). Max 1,000.

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

Upper limit of companies analyzed (and charged) in this run; extra entries are skipped.

## `compareToLastRun` (type: `boolean`):

Store the open roles of each company between runs and report new roles, closed roles and the change in open roles. The first run has no comparison (changeSinceLastRun is null). Schedule the actor weekly to track growth.

## `includeTopTitles` (type: `boolean`):

Add the 10 most common job titles per company (topTitles).

## `departmentKeywordGroups` (type: `object`):

Optional. Maps a function name to title/department keywords, e.g. {"sales": \["account executive", "sdr$"], "climate": \["carbon", "sustainab"]}. An existing name (engineering, sales, marketing, product, design, data_ai, customer_success_support, operations, finance, legal, hr_recruiting, security, executive) replaces its default keywords; a new name is added as an extra key of rolesByFunction and takes priority. Keyword syntax is described in the README.

## `concurrency` (type: `integer`):

Companies fetched in parallel. Keep low to be polite to the job-board servers.

## Actor input object example

```json
{
  "companies": [
    "figma",
    "https://jobs.lever.co/spotify"
  ],
  "maxCompanies": 100,
  "compareToLastRun": true,
  "includeTopTitles": true,
  "departmentKeywordGroups": {
    "climate": [
      "carbon",
      "sustainab"
    ]
  },
  "concurrency": 5
}
```

# Actor output Schema

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

One row per company with open roles, function and seniority mix, locations, posting velocity and signals.

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

Companies not found, errors and per-company totals.

# 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": [
        "figma",
        "greenhouse:stripe",
        "lever:spotify"
    ],
    "departmentKeywordGroups": {}
};

// Run the Actor and wait for it to finish
const run = await client.actor("interconnected_seaport/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": [
        "figma",
        "greenhouse:stripe",
        "lever:spotify",
    ],
    "departmentKeywordGroups": {},
}

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

```

## MCP server setup

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