# Company Hiring Signals API – Hiring Velocity & Tech Stack (`siftsmith/hiring-signals`) Actor

Company hiring signals from Greenhouse, Lever, Ashby, Workday, Workable, SmartRecruiters, Breezy & Personio. Input names/domains (Workday: board URL); get open jobs, 7/30/90d hiring velocity, hiringScore, tech stack from posts, new/closed roles. $0.05/company, misses free. Works in Clay & AI agents.

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

## Pricing

Pay per event

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

## Hiring Signals — Company Job & Tech Stack Intel

Give it a list of companies. Get back who's hiring, how fast, for what, and with which technologies — plus what changed since you last checked.

**One row per company, $0.05, no subscription. Companies not found are free.** Each row has open jobs, 7/30/90-day posting velocity, a 0–100 `hiringScore`, the tech stack from job posts, and new and closed roles since your last run. 1,000 companies cost at most $50; individual job rows are optional at $0.002 each. Covers companies hiring on Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Breezy HR, Personio and Workday. Works as a domain-in, row-out enrichment step for spreadsheets, CRMs, Clay (via Clay's built-in "Run Apify Actor" enrichment) and AI agents (via the Apify API and MCP server).

A pay-as-you-go alternative to TheirStack, Crunchbase hiring data, and manual career-page checking, with no seat fees.

### Quick start

Input:

```json
{ "companies": ["Ramp", "stripe.com", "lever:palantir"] }
```

One report row per company (plus job rows if you turn on individual jobs). A run on 2026-09-29 returned this for `Ramp` (trimmed):

```json
{ "company": "Ramp", "found": true, "type": "company", "ats": "ashby", "slug": "ramp", "matchConfidence": "high",
  "openJobs": 156, "postedLast7d": 18, "postedLast30d": 59, "postedLast90d": 110, "hiringScore": 68,
  "engineeringShare": 0.27, "topTechnologies": [{ "name": "Python", "count": 29 }, { "name": "Salesforce", "count": 25 }] }
```

- **Price:** $0.05 per company found with at least one open job. No start fee. These rows are free: not found, a board naming a different company (`low` match), a failed lookup, a board with zero open jobs, duplicates, and inputs skipped at your max charge. Individual job rows are optional at $0.002 each. Details are under "Pricing".
- **Best input:** a domain (`stripe.com`) or the company's name as it appears on its careers page. If you know the board, pass it (`greenhouse:stripe`) or paste the job board's URL. Workday is never auto-matched: pass `workday:{tenant}:{wdN}:{site}` or the `myworkdayjobs.com` URL.
- **Monitoring:** **Track changes between runs** is on by default. Schedule the same input weekly. From the second run on, each row counts roles opened and closed since the last run (`newSinceLastRun`, `closedSinceLastRun`) and lists up to 50 new ones in `newRoles`. On the first run both counts are `null`.

### What you get per company

| Field | Example |
|---|---|
| `openJobs` | 691 |
| `openJobsCapped` / `openJobsReported` | `false` / `null`. `openJobsCapped` is `true` when the board has more open roles than one report covers (1,000 on Workday and SmartRecruiters); `openJobs` and the posting counts then cover the first 1,000, and `openJobsReported` gives the total the job board itself reports (e.g. `2000` for NVIDIA on Workday; Workday reports at most 2,000, so treat `2000` as 2,000+). `null` when the board doesn't report a separate total. See `hiringScore` for how freshness is scored on a capped board |
| `postedLast7d` / `postedLast30d` / `postedLast90d` | 68 / 274 / 503, by each role's publish date exactly as the job board reports it |
| `hiringScore` (0–100, volume + freshness) | 76. `round(100 × (0.6 × volume + 0.4 × freshness))`: volume is the open-role count, log-scaled so it tops out at 300 roles; freshness is the share of open roles posted in the last 30 days, not counting a bulk re-publish (below). On a capped board (`openJobsCapped: true`) the share is taken of the board's own total, `openJobsReported`, not of the 1,000 roles fetched: Workday and SmartRecruiters list newest roles first, so the first 1,000 over-represent new roles (Bosch's first 1,000 of 4,801 were all under 17 days old, which used to score 100). When the 1,000 fetched roles include older ones, every role from the last 30 days is among them and the share is exact (Salesforce: 843 of 1,522, score 82) |
| `freshnessLowerBound` | `false`. `true` only on a capped board where all 1,000 fetched roles were posted in the last 30 days: the board has at least that many recent roles but may have more, so `postedLast30d` and the freshness part of `hiringScore` are lower bounds (Bosch: at least 1,000 of 4,801, score 68), not a guess extrapolated from the posting rate. One exception: a Workday board reporting `2000` can be bigger than that, which pushes its share the other way (see Limitations) |
| `bulkRepublish` | `false`. `true` when a board with 20+ open roles has more than half of them published within one 24-hour window (any 24 hours, so a burst across midnight UTC counts), the pattern of a board migration or re-import rather than a hiring surge; a brand-new board launched within 24 hours is flagged too (Deliveroo re-published 109 of its 148 roles on one day in September 2026). The re-published roles still count in `openJobs` and `postedLast*`, but not as fresh in `hiringScore` |
| `newSinceLastRun` / `closedSinceLastRun` / `newRoles` | counts + titles and links of new roles |
| `engineeringShare`, `remoteShare` (remote-eligible roles: roles that list remote as an option, e.g. "Remote", "London or Remote", or the job board's remote flag; a role marked only as hybrid doesn't count) | 0.29, 0.16 |
| `departments`, `topLocations`, `seniorityMix` | Account Executives (EMEA) 24, Singapore 50 … |
| `topTechnologies` | SQL 136, Python 84, Java 64, Salesforce 38, AWS 36 … |
| `type` | `company` on every charged report row; job rows are `job` (below). Free rows (not found, skipped, duplicate) have no `type`, so filter on `found: true` or `type: "company"` for reports |
| `ats`, `boardUrl` | greenhouse, boards.greenhouse.io/stripe |
| `boardCompanyName`, `matchConfidence` | Stripe, high (see "How companies are matched") |

Turn on **Also output individual jobs** to get one row per open role with title, department, location, remote flag, seniority, posted date, detected technologies, and URL. If the run's max charge is reached partway through a company's job rows, that company gets one extra free row after them:

| Field | Example |
|---|---|
| `type` | `jobs-truncated` |
| `jobsTruncated` / `jobsDelivered` / `jobsAvailable` | `true` / 120 / 691: how many of the company's job rows were returned out of how many open roles |
| `company` / `ats` / `slug` / `boardUrl` | same as on the company report row, so you can join the two |
| `note` | Why the rest are missing (the run's charge limit). Its company report is complete |

### Use cases

- **Sales & RevOps:** find accounts hiring for the stack you sell into ("companies hiring Snowflake + dbt engineers"). Job posts are the most honest tech-stack signal there is.
- **Recruiting agencies:** spot companies ramping a department before they call anyone.
- **Investors:** track headcount momentum across a portfolio or watchlist.
- **Competitive intel:** see which teams a competitor is building, weekly.
- **Full-stack account intel:** pair with [Tech Enricher](https://apify.com/siftsmith/tech-enricher) for a company's website/email/DNS stack alongside its hiring signal.

### Monitoring

Leave **Track changes between runs** on and put the Actor on a [schedule](https://docs.apify.com/platform/schedules) (e.g. weekly). Each run reports new and closed roles since the previous run, so you can pipe `newRoles` straight into Slack, a CRM, or a sheet via Apify integrations.

### Input

- **Companies:** names (`Ramp`), domains or career-page URLs (`stripe.com`, `https://www.airbnb.com/careers`, matched by the domain), explicit boards (`greenhouse:stripe`, `lever:palantir`, `ashby:ramp`, `workable:huggingface`, `smartrecruiters:experian`, `breezy:attentive`, `personio:deskbird`, `workday:salesforce:wd12:External_Career_Site`), or a job board's own URL, which is read as that explicit board: `https://jobs.lever.co/palantir` (or `jobs.eu.lever.co`, read from Lever's EU API), `https://jobs.ashbyhq.com/resend`, `https://boards.greenhouse.io/stripe` (or `job-boards.greenhouse.io`, `job-boards.eu.greenhouse.io`, `boards-api.greenhouse.io/v1/boards/stripe`, an embed's `?for=stripe`), `https://apply.workable.com/huggingface`, `https://jobs.smartrecruiters.com/BoschGroup` (or an apply link, `jobs.smartrecruiters.com/oneclick-ui/company/BoschGroup/...`), `https://attentive.breezy.hr`, `https://deskbird.jobs.personio.de`, `https://salesforce.wd12.myworkdayjobs.com/en-US/External_Career_Site`. A link to a single job on the board works too. Names and domains are matched automatically against the first seven; Workday is explicit-board or board-URL only (see below).
- **Extra technologies:** add your own keywords on top of ~150 built-in technologies (languages, frameworks, databases, cloud, data tools, AI/LLM tooling, CRMs, compliance frameworks).

If a company isn't found, or its lookup fails with an error, it's returned with `found: false` and an `error` at no charge. A board with zero open jobs counts as not found, including an explicit one (`smartrecruiters:acme`): some job boards, like SmartRecruiters, answer any name with an empty list, so an empty board can't be told apart from a nonexistent one. Some companies use an ATS not yet supported (iCIMS, in-house pages); pass an explicit board if you know it. Personio's public feed is opt-in per customer (Settings > Recruiting > Career page), so a real Personio company with that feed switched off also comes back `found: false`, same as a nonexistent one.

#### How companies are matched

A name or domain is turned into likely board names (`Hugging Face` → `huggingface`, `hugging-face`, …) and tried on every supported ATS except Workday. A guessed board can belong to a different company that happens to use the same name, so each one is checked against the company name the board gives for itself (`boardCompanyName`), and every report has a `matchConfidence`:

| `matchConfidence` | Meaning | Charged? |
|---|---|---|
| `explicit` | You passed the board yourself (`greenhouse:stripe`, or its URL `https://boards.greenhouse.io/stripe`) | Yes |
| `high` | The board's company name matches your input, ignoring case, punctuation, spaces, legal forms (Inc, LLC, Ltd, GmbH, Corp, AG, SE, B.V., S.A., PLC, …) and trailing words like Group/Holding/Technologies/AI/.com. Personio boards are checked against their career page's own title. For a domain input, the board must also point at that domain: the website it lists, or its job links, are on it (`ramp.com`: Ashby's "Ramp" lists ramp.com; `stripe.com`: Stripe's Greenhouse jobs link to stripe.com), or your domain redirects to that website (`notion.so` → notion.com) | Yes |
| `medium` | The match couldn't be fully checked: the board gives no company name (some Ashby boards), or, for a domain input, the name matches but the board lists no website or job links to compare with your domain (many Greenhouse boards, Workable, Breezy HR). Check `boardUrl` | Yes |
| `low` | The board gives a company name and it doesn't match your input; or, for a domain input, the board's website is on a different domain (`linear.com` finds Ashby's "Linear", whose site is linear.app; `gong.io` finds SmartRecruiters' "GONG!", gongsters.com); or it's a Personio board that gives no name at all (seen only on placeholder or dormant boards). Pass `ats:slug` explicitly if it's the right company | **No** |

When several boards are found, a higher confidence wins over a bigger board. A `low` match comes back free as `found: false`, with the board it found (`ats`, `slug`, `boardUrl`, `boardCompanyName`) and a note: if it is the right company, pass that board explicitly (e.g. `personio:finanzguru`) and it's reported normally. This happens when a company's board uses a legal-entity or longer name than the one you typed (Finanzguru's Personio board is "dwins GmbH").

Where the name comes from: Greenhouse's board API, Workable's and SmartRecruiters' job APIs, Breezy HR's job list, Lever's and Personio's hosted career page titles (plus Personio's per-job legal entity), and the company name and website on Ashby's hosted job board. For domain inputs, the website comes from Ashby, the "Home Page" link on Lever's and SmartRecruiters' hosted boards, and the legal-notice link on Personio's; a site that is only a careers site with the company's name in it (spotifyjobs.com) is not counted either way.

#### Workday

Workday's job-board API needs three values that can't be guessed from a company name, so it's not auto-matched like the other seven — pass it explicitly as `workday:{tenant}:{wdN}:{site}`, or paste the careers URL itself. Find all three in the company's own careers URL, which always has the shape `https://{tenant}.{wdN}.myworkdayjobs.com/{site}` (e.g. Salesforce's is `https://salesforce.wd12.myworkdayjobs.com/External_Career_Site` → `workday:salesforce:wd12:External_Career_Site`). The tenant is read in lowercase, as Workday's API expects, so `NVIDIA.wd5.myworkdayjobs.com` works too.

### Pricing

Pay per result:

- **Company report:** $0.05 per company found with at least one open job (a `low`-confidence match, a board that names a different company, is free)
- **Individual job:** $0.002 per job row (only if enabled)

1,000 companies ≈ $50. Companies not found are free. Set a max charge per run in the run options and you're never charged more than that: once it's reached, every remaining input still gets a short free row with `skipped: true` and a `reason`, so no input goes missing from the dataset. A company whose job rows were cut short by the limit gets a free `jobs-truncated` row (see above).

Duplicates are charged once. Inputs are deduplicated on the job board they resolve to (ATS + slug), not on the text you typed: `stripe.com`, `Stripe` and `greenhouse:stripe` all resolve to Greenhouse `stripe`, so one run with all three gives one charged report. Each duplicate still gets its own row, free, with `duplicateOf` set to the input that was reported: the one that comes first in your input list. Exact repeats of the same text are collapsed to one row before processing.

### Data sources & compliance

Uses only the official, public job-board APIs and feeds that Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Breezy HR, Workday, and Personio provide for publishing jobs. No login, no scraping of personal data, no LinkedIn. The output contains job and company information only.

### Limitations

- Technology detection is keyword-based on job text: it shows what a company *asks for*, which strongly correlates with, but isn't proof of, what it runs.
- Company boilerplate is skipped for technology detection: a sentence repeated across at least half of a company's distinct job titles (about-us text, customer lists, benefits) doesn't count, so "trusted by OpenAI and Ramp" in every posting isn't reported as OpenAI. Repeated stack statements ("our stack is Go and Postgres", "proficiency in Python") still count. A custom keyword that appears only in that boilerplate won't be reported.
- SmartRecruiters', Breezy HR's, and Workday's public APIs return only title/department/location per posting (Workday also omits department), not the full job description — technology and seniority detection is weaker there than on the other boards. Personio's feed includes the full job description, on par with Greenhouse/Lever/Ashby/Workable.
- `postedAt` reflects the ATS's first-published or created date; some companies repost roles one at a time, which can inflate freshness (a whole board re-published within 24 hours is caught and marked `bulkRepublish`). Workday only gives a relative age bucket, not a date — roles posted 30+ days ago show `postedAt: null` (excluded from the freshness fields) rather than a guessed date.
- Workday and SmartRecruiters reports cover up to 1,000 open roles per company. A larger board is marked `openJobsCapped: true`, with the job board's own total in `openJobsReported` (Workday reports at most 2,000, so `2000` means 2,000+). `hiringScore`'s freshness is then the 30-day count over `openJobsReported`; that relies on the board listing newest roles first, as both do (if a capped board's roles come back in no date order, the share among the fetched roles is used instead). A Workday board reporting `2000` may be larger, and dividing by 2,000 then makes its freshness read higher than it is.
- Automatic matching checks the board's company name, and for a domain input the site the board points at, not the company itself: two different companies with the same name (e.g. two "Clark"s) both come out `high` for a name input, and a `medium` match is unchecked. A domain input whose company's board lists its website on another company's domain (for example an old domain that doesn't redirect) comes back `low` and free; pass the board explicitly. Careers-only sites, link shorteners and a parent brand's site count neither way. Check `boardCompanyName`/`boardUrl`, or pass an explicit board, for companies with generic names.
- Large enterprises are often not found by name. Many use Workday, SAP SuccessFactors, iCIMS, Taleo or their own careers pages. Of those, only Workday is supported, and only as an explicit `workday:{tenant}:{wdN}:{site}` board (see above).

### Related tools

- [Buyer Intent Signals](https://apify.com/siftsmith/buyer-intent-signals): combines this hiring score with a tech-stack check into one 0–100 intent score per company, scored higher (×1.5, capped at 100) when a hiring company doesn't run the tool category you sell.
- [Tech Enricher](https://apify.com/siftsmith/tech-enricher): a domain's website technologies, email provider and DNS host.

### Support

Open an issue on the Actor's Issues tab or email hello@siftsmith.com. Issues are read daily and fixed promptly.

More tools from Siftsmith (formerly ToolFoundry): [siftsmith.com](https://siftsmith.com/).

# Actor input Schema

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

Company names or domains, auto-matched to Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Breezy and Personio boards. Workday is never auto-matched: give it as 'workday:tenant:wdN:site' or its Workday careers URL. Explicit boards also work: 'greenhouse:stripe', 'lever:palantir', 'ashby:ramp', 'workable:huggingface', 'smartrecruiters:experian', 'breezy:attentive', 'personio:deskbird', or a board URL like 'https://jobs.lever.co/palantir'.

## `includeJobs` (type: `boolean`):

Adds one dataset item per open job (title, department, location, remote, posted date, technologies, URL). Charged per job.

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

Remembers each company's jobs so the next run reports new and closed roles. Ideal for scheduled monitoring.

## `technologies` (type: `array`):

Additional keywords to count in job posts, on top of the built-in list of ~150 technologies.

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

How many companies to process in parallel.

## Actor input object example

```json
{
  "companies": [
    "stripe.com",
    "Ramp",
    "lever:palantir"
  ],
  "includeJobs": false,
  "trackChanges": true,
  "maxConcurrency": 5
}
```

# Actor output Schema

## `results` (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",
        "Ramp",
        "lever:palantir"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("siftsmith/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",
        "Ramp",
        "lever:palantir",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("siftsmith/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",
    "Ramp",
    "lever:palantir"
  ]
}' |
apify call siftsmith/hiring-signals --silent --output-dataset

```

## MCP server setup

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